Files
Edu/apps/student-portal/docs/02-architecture-design.md
SpecialX e691cd267d docs(teacher-portal): ai07 阶段1+2 拆分到4端portal的docs目录
删除合并版README,按portal拆分8份文档(每端01-understanding+02-architecture-design)

teacher-portal(shell/P2)+student-portal(remote/P3)+parent-portal(remote/P4)+admin-portal(remote/P6)

AI Agent: ai07 (4 portals)

Branch: docs/portals-stage1-stage2-design-ai07
2026-07-09 18:23:27 +08:00

598 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — student-portal
> AIai07TS/React · 学习场景域前端 remote
> 阶段:阶段 2 交付物
> 日期2026-07-09
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[pending-features P3](../../../docs/architecture/roadmap/pending-features.md)、[teacher-portal 阶段 2 架构设计](../../teacher-portal/docs/02-architecture-design.md)
> 状态:待 coord 交叉审查
---
## 1. 模块内部分层图student-portal Remote 视角)
```mermaid
graph TB
subgraph Browser["浏览器(学生)"]
URL[URL 路由 /student/*]
end
subgraph Shell["teacher-portalShell 宿主)"]
AppShell[AppShell<br/>左栏导航 + 主内容区]
RootLayout[RootLayout<br/>字体/令牌/i18n/Query Provider]
SharedDeps["共享依赖暴露<br/>react/react-dom/@tanstack/react-query/zustand/nuqs<br/>ui-components/ui-tokens/contracts/hooks"]
end
subgraph RemoteStudent["student-portalRemote"]
StudentPages["学习场景页面<br/>dashboard/homework/submit/exams/take<br/>diagnostic(P4)/weakness(P4)/notifications(P5)"]
ExamTaking[ExamTaking 组件<br/>倒计时 + 自动保存]
StudentApiClient[ApiClient 实例<br/>注入 student scope token]
end
subgraph Shared["共享层packages/"]
UITokens[ui-tokens<br/>三层设计令牌]
UIComponents[ui-components<br/>shadcn + A11y + ErrorBoundary]
Contracts[contracts<br/>Permissions 常量 + 类型]
Hooks[hooks<br/>usePermission/useAuth/useA11y]
LibTS[shared-ts<br/>ApiClient/Logger/通用工具]
end
subgraph Gateway["api-gateway"]
GW[Gin 路由/鉴权/限流]
end
subgraph PushGW["push-gatewayP5"]
WS[WebSocket /ws]
end
Browser --> URL
URL --> RootLayout
RootLayout --> AppShell
AppShell -->|动态加载 Remote| RemoteStudent
RemoteStudent -->|复用| SharedDeps
RemoteStudent --> ExamTaking
RemoteStudent --> StudentApiClient
Shell --> UITokens
Shell --> UIComponents
Shell --> Contracts
Shell --> Hooks
Shell --> LibTS
RemoteStudent --> UITokens
RemoteStudent --> UIComponents
RemoteStudent --> Contracts
RemoteStudent --> Hooks
StudentApiClient -->|fetch /api/v1/student/*| GW
StudentApiClient -->|fetch /api/v1/iam/*| GW
StudentApiClient -->|fetch /api/v1/notifications/*| GW
RemoteStudent -.->|P5 WebSocket| WS
WS -.->|推送事件| RemoteStudent
```
### 1.1 Remote 角色定位
student-portal 作为 **Remote 子应用**,由 teacher-portal Shell 动态加载。与 Shell 的职责分工:
| 职责 | Shellteacher-portal | Remotestudent-portal |
| -------------------------------- | ----------------------- | ------------------------ |
| RootLayout字体/令牌/Provider | ✅ 提供 | ❌ 复用 |
| AppShell左栏 + 主内容区) | ✅ 提供 | ❌ 复用 |
| 共享依赖暴露singleton | ✅ 提供 | ❌ 消费 |
| 登录页 | ✅ 提供 | ❌ 复用 |
| 路由表 `/student/*` | ❌ 由 Remote 暴露 | ✅ 提供 |
| 学习场景页面 | ❌ | ✅ |
| ApiClient 实例 | ❌ 提供 ApiClient 类 | ✅ 注入 student scope |
| 业务组件ExamTaking 等) | ❌ | ✅ |
### 1.2 MF 配置student-portal/next.config.js
```javascript
// student-portal/next.config.jsRemote
const NextFederationPlugin = require("@module-federation/nextjs-mf");
const remotes = (isServer) => ({
teacher: `teacher_app@http://localhost:3000/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
});
module.exports = {
reactStrictMode: true,
webpack(config, { isServer }) {
config.plugins.push(
new NextFederationPlugin({
name: "student_app",
filename: "static/chunks/remoteEntry.js",
remotes: remotes(isServer),
exposes: {
"./pages": "./src/pages",
"./ExamTaking": "./src/components/ExamTaking",
},
shared: {
react: { singleton: true, requiredVersion: "^18.3.0" },
"react-dom": { singleton: true, requiredVersion: "^18.3.0" },
"@tanstack/react-query": { singleton: true },
zustand: { singleton: true },
nuqs: { singleton: true },
},
extraOptions: { exposePages: false },
}),
);
return config;
},
async rewrites() {
return [
{
source: "/api/v1/:path*",
destination: `${process.env.API_GATEWAY_URL || "http://localhost:8080"}/api/v1/:path*`,
},
];
},
};
```
**关键点**
- `name: 'student_app'`Remote 应用名,与 Shell 中 `remotes.student` 对应
- `exposes`:暴露页面入口 `./pages` 和 student 特有组件 `./ExamTaking`
- `remotes.teacher`:引用 Shell 提供的 AppShell 和 shared-deps
- `shared`:与 Shell 一致的 singleton 策略,保证 React/Query/Zustand 单例
- `rewrites`:与 Shell 一致的 `/api/v1/*` 代理到 api-gateway
## 2. 领域模型(前端视角)
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。Session/Viewport/Permission 三模型与 Shell 共享,详见 [teacher-portal 阶段 2 §2](../../teacher-portal/docs/02-architecture-design.md#2-领域模型前端视角)。
### 2.1 会话状态Session
复用 Shell 的 Session 模型student-portal 不重复定义。详见 teacher-portal 文档 §2.1。
### 2.2 视口模型Viewport
复用 Shell 的 ViewportItem 模型,但 `scope` 字段值为 `'student'`。来源:`GET /api/v1/student/viewports`student-bff 聚合 iam 视口配置。AppShell 按 `scope='student'` 过滤渲染学生端导航。
### 2.3 权限模型Permission
复用 Shell 的 PermissionState 模型。来源:`GET /api/v1/iam/effective-permissions``{ permissions, viewports, dataScope }`。学生 dataScope 固定为 L0仅本人
### 2.4 student-portal 特有视图模型
```typescript
// 考试作答草稿Zustand L3断网恢复用
interface ExamTakingDraft {
examId: string;
answers: Record<string, AnswerInput>; // questionId → answer
startedAt: number;
lastSavedAt: number | null;
durationSeconds: number; // 考试时长(秒)
}
// 作业提交表单react-hook-form L5
interface HomeworkSubmitForm {
homeworkId: string;
answers: AnswerInput[];
attachments?: File[]; // 附件(图片/PDF
note?: string; // 学生备注
}
```
## 3. 数据模型(前端缓存策略)
前端无数据库仅有缓存层。student-portal 缓存策略偏短(学生数据实时性要求高):
| 数据类型 | 存储 | TTL | 失效策略 |
| ------------------------- | ------------------------- | ---- | --------------------------------------------------------- |
| Sessiontoken + user | localStorage + Zustand | — | 复用 Shellaccess 15min / refresh 7day401 自动 refresh |
| 权限列表 | TanStack Query cache | 5min | 复用 Shell角色变更事件 invalidate |
| 视口列表scope=student | TanStack Query cache | 5min | 复用 Shell |
| 学生 Dashboard 聚合数据 | TanStack Query cache | 30s | staleTime 30s事件触发 invalidate |
| 我的作业列表 | TanStack Query cache | 30s | staleTime 30s提交后 invalidate |
| 我的考试列表 | TanStack Query cache | 30s | staleTime 30sExamPublished 事件 invalidate |
| 考试作答草稿 | Zustand L3 + localStorage | — | 卸载不销毁,自动保存(每 30s + blur 时) |
| 学情诊断数据 | TanStack Query cache | 30s | staleTime 30s实时性由 BFF 决定) |
| 错题本列表 | TanStack Query cache | 30s | staleTime 30s |
| 通知列表P5 | TanStack Query cache | 30s | staleTime 30sWebSocket 事件 invalidate |
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
| 表单临时态(作业提交) | react-hook-form | — | 卸载即销毁 |
## 4. API 设计(前端 → 后端)
前端不设计后端 API仅声明消费的端点。详见 [01-understanding.md §3.1](./01-understanding.md)。
### 4.1 统一 API 请求层(复用 Shell ApiClient
student-portal **不重复实现 ApiClient 类**,复用 `packages/shared-ts/src/api-client.ts`(详见 [teacher-portal 阶段 2 §4.1](../../teacher-portal/docs/02-architecture-design.md#41-统一-api-请求层libapits))。
student-portal 仅注入不同 baseUrl/token 和 scope 标记:
```typescript
// apps/student-portal/src/lib/api.ts
import { ApiClient } from "@edu/shared-ts";
import { useAuth } from "@edu/hooks";
export function useStudentApi(): ApiClient {
const { getToken, logout } = useAuth();
return useMemo(
() =>
new ApiClient({
baseUrl: "", // 走 Next.js rewrites
getToken,
onUnauthorized: logout,
scope: "student", // 日志/trace 标记
}),
[getToken, logout],
);
}
```
**职责**(与 Shell 一致,由 shared-ts ApiClient 承担):
- 自动注入 `Authorization: Bearer ${token}`
- 401 自动 refresh token 一次,失败调 `onUnauthorized`
- 解析 `ActionState`success=false 抛 `ApiError`
-`error.code` 前缀路由 i18n key
- 全局错误 toast除 401
- 请求/响应 trace_id 透传(从响应头 `X-Request-Id` 提取)
### 4.2 TanStack Query 约定
```typescript
// Query Key 命名:[scope, resource, ...args]
queryKey: ["student", "dashboard"];
queryKey: ["student", "homework", { status, page }];
queryKey: ["student", "exams", { status }];
queryKey: ["student", "exam-taking", examId];
queryKey: ["student", "diagnostic"];
queryKey: ["student", "weakness", { knowledgePointId }];
queryKey: ["student", "notifications", { unreadOnly }];
queryKey: ["session", "effective-permissions"]; // 复用 Shell
queryKey: ["session", "viewports", "student"]; // 复用 Shell
// Mutation 约定
const submitHomework = useMutation({
mutationFn: (input) =>
api.post(`/api/v1/student/homework/${input.homeworkId}/submit`, input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["student", "homework"] });
toast.success(t("homework.submitSuccess"));
},
onError: (e: ApiError) => toast.error(e.message),
});
const saveExamAnswer = useMutation({
mutationFn: (input) =>
api.post(`/api/v1/student/exams/${input.examId}/answers`, input),
onSuccess: (data) => {
// 更新本地草稿的 lastSavedAt
examTakingStore.updateLastSavedAt(data.savedAt);
},
onError: (e: ApiError) => {
// 草稿保留在本地,下次 blur/定时重试
logger.warn("exam answer save failed", { examId: e.details });
},
});
```
## 5. 事件设计
前端不发布 Kafka 事件,仅消费 WebSocket 推送P5
### 5.1 WebSocket 推送P5
| 事件 | 触发 | student-portal 前端动作 |
| ----------------------------- | -------------- | --------------------------------------------------------------- |
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 |
| `ExamPublished` | 教师发布考试 | toast + `["student","exams"]` invalidate + dashboard invalidate |
| `GradeRecorded` | 教师录入成绩 | toast + `["student","grades"]` invalidate |
| `HomeworkDeadlineApproaching` | 作业截止前提醒 | toast + 作业列表高亮 + `["student","homework"]` invalidate |
### 5.2 WebSocket 连接管理P5
```typescript
// apps/student-portal/src/lib/ws.tsP5 实现)
export function useStudentWebSocket() {
const { getToken } = useAuth();
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket(
`${process.env.NEXT_PUBLIC_PUSH_GATEWAY_URL}/ws?token=${getToken()}`,
);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data) as PushMessage;
switch (msg.type) {
case "NotificationRequested":
toast.info(t(msg.payload.title));
queryClient.invalidateQueries({
queryKey: ["student", "notifications"],
});
break;
case "ExamPublished":
toast.info(t("exam.published", { name: msg.payload.examName }));
queryClient.invalidateQueries({ queryKey: ["student", "exams"] });
queryClient.invalidateQueries({ queryKey: ["student", "dashboard"] });
break;
case "GradeRecorded":
toast.success(t("grade.recorded"));
queryClient.invalidateQueries({ queryKey: ["student", "grades"] });
break;
case "HomeworkDeadlineApproaching":
toast.warning(
t("homework.deadlineApproaching", {
name: msg.payload.homeworkName,
}),
);
queryClient.invalidateQueries({ queryKey: ["student", "homework"] });
break;
}
};
return () => ws.close();
}, [getToken, queryClient]);
}
```
### 5.3 考试作答自动保存SSE / HTTP 轮询)
> 考试作答的自动保存采用 HTTP POST每 30s + blur 时),不使用 SSE。SSE 仅 teacher-portal 用于 AI 流式出题student-portal 不涉及。
## 6. 横切关注点对齐清单
### 6.1 权限(前端等价)
| 路由 | requiredPermission |
| ------------------------------ | ------------------------ |
| `/student/dashboard` | `STUDENT_DASHBOARD_VIEW` |
| `/student/homework` | `HOMEWORK_READ_OWN` |
| `/student/homework/:id/submit` | `HOMEWORK_SUBMIT` |
| `/student/exams` | `EXAMS_READ_OWN` |
| `/student/exams/:id/take` | `EXAMS_TAKE` |
| `/student/diagnostic` | `DIAGNOSTIC_READ_OWN` |
| `/student/weakness` | `WEAKNESS_READ_OWN` |
| `/student/notifications` | `NOTIFICATION_READ_OWN` |
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`待建立coord 负责 contractsai07 负责调用。L3 组件级视口用 `<RequirePermission perm="HOMEWORK_SUBMIT"><Button>提交作业</Button></RequirePermission>`。权限点后缀 `_OWN` 强调学生仅能操作自己的数据DataScope L0
### 6.2 错误码清单(前端 i18n 路由)
| 前缀 | 来源服务 | i18n key 模式 |
| -------------- | ----------- | ------------------------- |
| `IAM_` | iam | `iam.error.{{code}}` |
| `CORE_EDU_` | core-edu | `coreEdu.error.{{code}}` |
| `EXAMS_` | core-edu | `exams.error.{{code}}` |
| `HOMEWORK_` | core-edu | `homework.error.{{code}}` |
| `GRADES_` | core-edu | `grades.error.{{code}}` |
| `BFF_STUDENT_` | student-bff | `bff.error.{{code}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
### 6.3 Logger
复用 `packages/shared-ts/src/logger.ts`(同 teacher-portal开发环境 console + 结构化,生产环境 → SentryP6。必含字段`trace_id``user_id``scope=student``path`
```typescript
// 使用示例student-portal 内)
import { createLogger } from "@edu/shared-ts";
const logger = createLogger({ scope: "student" });
logger.info("exam taking started", { examId, durationSeconds });
logger.error("homework submit failed", { homeworkId, error: err.code });
```
### 6.4 MetricsWeb Vitals
| 指标 | 类型 | 上报 |
| ----------------------------- | ---- | --------------------------------------------------- |
| `student_portal_lcp_seconds` | LCP | `next/web-vitals``POST /api/v1/admin/web-vitals` |
| `student_portal_cls` | CLS | 同上 |
| `student_portal_fid_seconds` | FID | 同上 |
| `student_portal_ttfb_seconds` | TTFB | 同上 |
P6 接入P3-P5 暂缓。
### 6.5 TracerOTel browser SDKP6
复用 `packages/shared-ts/src/tracer.ts`(同 teacher-portal。BatchSpanProcessor → OTLP exporter → collector → Tempo。自动埋点fetch、XMLHttpRequest、document load、user interaction。
### 6.6 健康检查
| 端点 | 用途 | 实现 |
| ----------------- | ---------------------- | -------------------------------------------------------------- |
| `GET /api/health` | Dockerfile HEALTHCHECK | Next.js Route Handler返回 `{ status: 'ok', ts: Date.now() }` |
| `GET /api/ready` | K8s readinessProbe | 检查 `process.env.API_GATEWAY_URL` 可达 + 内存 < 阈值 |
### 6.7 优雅关闭
Next.js 无长连接(除 WS无需特殊处理。WS 在 P5 由 push-gateway 管理,前端断线自动重连(指数退避,最多 5 次)。
## 7. 共享组件库(复用 Shell 暴露 + student 特有)
### 7.1 复用 Shell 暴露的组件packages/ui-components/
| 组件 | 用途 | 来源 |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------ |
| `AppShell` | 左侧栏 + 主内容区布局 | teacher-portal Shell 暴露 |
| `RequirePermission` | L3 组件级视口控制(无权限不渲染 children | Shell 暴露 |
| `ErrorBoundary` | React 渲染异常兜底fallback UI | Shell 暴露 |
| `Loading` | 骨架屏Skeleton | Shell 暴露 |
| `Empty` | 空态(插画 + 文案 + CTA | Shell 暴露 |
| `Modal` / `Dialog` | 全局 ModalModalRoot + Zustand ui-store | Shell 暴露shadcn/ui |
| `Toast` | 全局 toast错误/成功/警告) | Shell 暴露shadcn/ui sonner |
| `Button` / `Input` / `Select` / `Textarea` | 基础表单 | Shell 暴露shadcn/ui |
| `DataTable` | 表格(排序/分页/筛选) | Shell 暴露 |
| `Chart` | 图表封装recharts | Shell 暴露 |
| `A11y` 工具集 | useA11yId / mergeA11yProps / describeInput / focus-trap / skip-link / visually-hidden / aria-status | Shell 暴露 |
| `Form` | react-hook-form + zodResolver 封装 | Shell 暴露 |
### 7.2 student-portal 特有组件(不暴露给 Shell
| 组件 | 用途 | 来源 | 是否暴露给 Shell |
| ----------------- | --------------------------------------------- | ---- | ------------------------------------------- |
| `ExamTaking` | 考试作答(倒计时 + 自动保存 + 断网恢复) | 新建 | ✅ 暴露 `./ExamTaking`(供 Shell 路由复用) |
| `HomeworkSubmit` | 作业提交表单react-hook-form + zodResolver | 新建 | ❌ 内部使用 |
| `DiagnosticChart` | 学情诊断图表recharts 多维雷达 + 趋势线) | 新建 | ❌ 内部使用 |
| `WeaknessList` | 错题本列表(按知识点聚合 + 掌握度标签) | 新建 | ❌ 内部使用 |
### 7.3 不使用的组件(与 teacher-portal 差异)
- **不使用 `RichTextEditor`Tiptap**:学生不作答富文本,作业提交用表单
- 不使用 `SSEViewer`:学生不参与 AI 出题
- 不使用 `ChildSwitcher`:学生无多子女切换(家长端特有)
- 不使用 `UserManagementTable`:学生不管理用户
## 8. 共享 Hooks复用 Shell 暴露)
| Hook | 职责 | 来源 |
| --------------------- | --------------------------------------------------- | ---------- |
| `useAuth()` | 会话状态user/token/refresh/login/logout | Shell 暴露 |
| `usePermission()` | 权限查询hasPermission/hasAny/hasAll + dataScope | Shell 暴露 |
| `useViewports(scope)` | 视口列表(按 scope 过滤) | Shell 暴露 |
| `useApi()` | ApiClient 实例(注入 token + 401 处理) | Shell 暴露 |
| `useA11yId()` | 唯一 ARIA ID 生成 | Shell 暴露 |
| `useAriaLive()` | aria-live 区域管理 | Shell 暴露 |
| `useToast()` | 全局 toastZustand ui-store | Shell 暴露 |
## 9. 设计令牌三层(复用 Shell 提供的 packages/ui-tokens/
student-portal **不独立维护设计令牌**,复用 Shell 暴露的 `packages/ui-tokens/`(详见 [teacher-portal 阶段 2 §9](../../teacher-portal/docs/02-architecture-design.md#9-设计令牌三层packagesui-tokens待建立ai07-维护))。
**强制规则**project_rules §3.10,与 Shell 一致):
- 禁止 `#hex` 字面量ESLint `no-restricted-syntax`
- 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量ESLint `design-tokens/no-hardcoded-fonts`
- 禁止 `font-size: Npx`(用 `var(--font-size-1~9)`
- 禁止 Tailwind 任意值 `w-[Npx]`(用 `--space-*` 或默认阶梯)
student-portal 的 ESLint flat config 与 Shell 共用,确保规则一致。
## 10. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
| ------ | ------------ | ------------------ | ------------------------------------ | ---------------------------- | ---- |
| 调用 | api-gateway | HTTP/REST | `/api/v1/*` 代理 | 全部业务请求 | P1+ |
| 调用 | push-gateway | WebSocket | `ws://push-gateway/ws` | 实时推送 | P5 |
| 被调用 | — | — | — | 前端不暴露接口给其他服务 | — |
| 消费 | student-bff | HTTP经 Gateway | `GET /student/viewports` 等 | 学生场景聚合 | P3+ |
| 消费 | iam | HTTP经 Gateway | `/iam/*` | 登录/权限/视口 | P2+ |
| 消费 | core-edu | HTTP经 Gateway | `/exams/*` `/homework/*` `/grades/*` | 教学核心(学生视角) | P3+ |
| 消费 | data-ana | HTTP经 Gateway | `/analytics/*` | 学情分析 | P4+ |
| 消费 | msg | HTTP经 Gateway | `/notifications/*` | 通知中心 | P5+ |
| 依赖 | coord 维护 | — | `packages/shared-proto` | TS 类型(仅 contracts 部分) | P1+ |
| 依赖 | coord 维护 | — | `packages/shared-ts`(待建) | ApiClient/Logger/通用工具 | P3+ |
| 依赖 | ai07 维护 | — | `packages/ui-tokens`(待建) | 三层设计令牌 | P3+ |
| 依赖 | ai07 维护 | — | `packages/ui-components`(待建) | shadcn + 共享组件 | P3+ |
| 依赖 | ai07 维护 | — | `packages/hooks`(待建) | usePermission/useAuth 等 | P3+ |
| 依赖 | coord 维护 | — | `packages/contracts`(待建) | Permissions 常量 + 类型 | P3+ |
> **proto 不直接消费**:前端不调用 gRPCBFF 把 gRPC 聚合为 REST 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 11. 风险与假设
### 11.1 假设
1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**:包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立ai07 自行在 `apps/student-portal/src/shared/` 内实现,后续提取到 packages。
2. **假设 ai04 student-bff 提供 `GET /student/viewports`、`GET /student/dashboard`、`GET /student/homework`、`POST /student/homework/:id/submit`、`GET /student/diagnostic`**:返回 `ActionState` 结构。错误码前缀 `BFF_STUDENT_`(待 ai04 确认)。
3. **假设 teacher-portal Shell 已就绪**AppShell + 共享依赖暴露 + MF 配置P2 收尾完成。student-portal 作为 Remote 才能挂载。
4. **假设 Next.js 14+ Module Federation 2.0 稳定**`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell重复实现 AppShell
### 11.2 技术风险
| 风险 | 影响 | 缓解 |
| ----------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- |
| MF Remote SSR 对齐复杂 | Remote 在 SSR 时需 Shell 提供上下文 | 优先 CSRSSR 仅用于首屏 dashboardMF 2.0 支持 SSR |
| 共享依赖版本漂移 | Remote 与 Shell 的 react/react-dom 版本不一致导致运行时错误 | MF `shared.singleton: true` + CI 检查版本对齐 |
| 考试作答断网丢失 | 学生作答过程中断网,草稿丢失 | Zustand L3 + localStorage 双写,每 30s + blur 时自动保存,重连后重试 |
| 考试倒计时不准 | 学生端时间与服务器时间偏差 | 倒计时基于服务器返回的 `expiresAt`,前端仅做展示,提交以服务器时间为准 |
| 缓存陈旧导致看到旧作业 | 学生看到已截止的作业 | staleTime 30s + 截止时间客户端校验 + 提交时服务端二次校验 |
| Token 刷新竞态 | 多请求同时 401 触发多次 refresh | ApiClient 全局单例 + refresh promise 复用(复用 Shell |
| 权限缓存陈旧 | 角色变更后前端 5min 内仍用旧权限 | iam 角色变更发 Kafka 事件 → msg 推送 WebSocket → 前端 invalidate |
| TanStack Query 缓存膨胀 | 长时间使用后缓存项过多 | `gcTime` 5min + `staleTime` 按数据类型分级 |
### 11.3 未决设计决策(需 coord 仲裁)
与 teacher-portal 相同的 4 项(详见 [teacher-portal 阶段 2 §11.3](../../teacher-portal/docs/02-architecture-design.md#113-未决设计决策需-coord-仲裁)
1. **packages 归属**`ui-tokens` / `ui-components` / `hooks` 是 ai07 维护还是 coord 维护建议ai07 维护前端专属coord 仅维护 `shared-ts` / `contracts`(跨语言/跨服务)。
2. **GraphQL vs REST**student-bff 是 REST 还是 GraphQL前端 API 请求层是否需要 GraphQL clienturql/apollo建议P3-P4 用 REST若 BFF 切 GraphQL 再引入 urql。
3. **i18n key 命名**`error.{{service}}.{{code_snake_case}}` 还是其他模式?建议:与 teacher-portal 对齐。
4. **MF 暴露粒度**Shell 暴露整个 AppShell 还是更细粒度的组件?建议:暴露 AppShell 整体 + 各 Remote 自行决定内部布局。
## 12. coord 交叉审查所需信息
### 12.1 端口矩阵
| 端 | dev 端口 | 生产端口 | 备注 |
| -------------- | -------- | -------- | ------------- |
| student-portal | 3001 | 3001 | Remote 子应用 |
> 与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐。
### 12.2 依赖的共享包(需 coord 建立)
| 包 | 路径 | 维护方 | 内容 |
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、通用工具 |
| `contracts` | `packages/contracts/` | coord | Permissions 常量、ActionState 类型、UserInfo 类型 |
| `ui-tokens` | `packages/ui-tokens/` | ai07建议 | 三层设计令牌 |
| `ui-components` | `packages/ui-components/` | ai07建议 | shadcn + ErrorBoundary + RequirePermission |
| `hooks` | `packages/hooks/` | ai07建议 | usePermission、useAuth、useViewports |
### 12.3 依赖的后端契约(需对应 AI 确认)
| 契约 | 提供方 | 当前状态 |
| ------------------------------------------------------------------ | ------------------ | --------------- |
| `POST /iam/login``GET /iam/effective-permissions``GET /iam/me` | iam | ✅ 已实现 |
| `GET /student/viewports``GET /student/dashboard` 等 | student-bff | 📐 待 ai04 设计 |
| `/exams/*` `/homework/*` `/grades/*` | core-edu | ✅ 已实现P3 |
| `/analytics/*` | data-ana | ✅ 已实现P4 |
| `/notifications/*` + WebSocket 推送 | msg + push-gateway | 📐 待 P5 |
### 12.4 错误码前缀(前端 i18n 路由依赖)
前端不产生错误码,仅消费。需各服务确认错误码前缀不重叠:
| 前缀 | 服务 | 状态 |
| ------------------------------ | ----------- | --------------- |
| `IAM_` | iam | ✅ 已用 |
| `EXAMS_`/`HOMEWORK_`/`GRADES_` | core-edu | ⚠️ 待确认 |
| `BFF_STUDENT_` | student-bff | ⚠️ 待 ai04 确认 |
| `GW_` | api-gateway | ✅ 已用 |
| `NETWORK_` | 前端 | ai07 自有 |
### 12.5 不产生 Kafka 事件
前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。
## 13. 实施路线ai07 自用)
### P3student-portal 起步)
1.`apps/student-portal/`Remote 角色)
2. 配置 MF`exposes: { './pages', './ExamTaking' }``remotes: { teacher: ... }`
3. 实现 Dashboard + 我的作业 + 提交作业 + 我的考试 + 作答考试
4. 复用 Shell 的 AppShell + 共享组件RequirePermission/ErrorBoundary/Loading/Empty/DataTable/Form/Chart
5. 考试作答自动保存HTTP POST 每 30s + blur 时Zustand L3 + localStorage 双写草稿)
6. 配置 Dockerfile 多阶段构建 + `/api/health` + `/api/ready` route
7. 补 Vitest 单测 + Playwright E2E覆盖率 ≥ 80%
### P4学情诊断 + 错题本)
1. 实现学情诊断页面DiagnosticChart 多维雷达 + 趋势线)
2. 实现错题本页面WeaknessList 按知识点聚合 + 掌握度标签)
### P5推送 + 通知中心)
1. student-portal 接入 WebSocketpush-gateway
2. 实现通知中心页面(通知列表 + 未读数 + 标记已读)
3. 处理 4 类推送事件NotificationRequested/ExamPublished/GradeRecorded/HomeworkDeadlineApproaching
### P6硬化
1. Web Vitals + OTel browser SDK 接入
2. A11y WCAG 2.2 AA 审计
3. 性能优化MF shared 单例验证、bundle 分析)
---
**AI Agent**: ai07 (student-portal remote)
**Branch**: docs/student-portal-stage1-stage2-design-ai07
**Coordinator**: coord-ai