# 模块架构设计文档 — student-portal

> AI：ai07（TS/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-portal（Shell 宿主）"]
        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-portal（Remote）"]
        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-gateway（P5）"]
        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 的职责分工：

| 职责                             | Shell（teacher-portal） | Remote（student-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.js（Remote）
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  | 失效策略                                                  |
| ------------------------- | ------------------------- | ---- | --------------------------------------------------------- |
| Session（token + user）   | localStorage + Zustand    | —    | 复用 Shell：access 15min / refresh 7day，401 自动 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 30s，ExamPublished 事件 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 30s，WebSocket 事件 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.ts（P5 实现）
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 负责 contracts，ai07 负责调用）。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 + 结构化，生产环境 → Sentry（P6）。必含字段：`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 Metrics（Web 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 Tracer（OTel browser SDK，P6）

复用 `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`                         | 全局 Modal（ModalRoot + 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()`          | 全局 toast（Zustand 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 不直接消费**：前端不调用 gRPC，BFF 把 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 提供上下文                         | 优先 CSR，SSR 仅用于首屏 dashboard；MF 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 client（urql/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 自用）

### P3（student-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 接入 WebSocket（push-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
