portal-shell 前端架构总纲(v3.0)
版本:3.0
日期:2026-07-20
状态:P0 已完成(2026-07-22 验收)+ P1 进行中;架构审计完成 + 重设计方案定稿
本文档地位:portal-shell 前端工作的唯一权威指导文档。所有后续 AI/人工在此模块的工作必须先读本文件,以其为准。
关联文档(按效力排序):
- 本文件(v3.0 总纲:审计结论 + 目标架构 + 路线图 + 工作规范)
- README.md(v2.0 模块文档:微内核仪表盘子系统的详细设计,仅其"插件仪表盘"部分继续有效)
- 004 架构影响地图(后端架构唯一源:§5.4 视口四层模型、§11.7 前端数据层)
- 项目规则(强制约束:§3.10 设计令牌、§4 安全、§14 多 AI 协作)
- MIGRATION_GUIDE.md(CICD → Edu 迁移背景)
效力声明:README v2.0 中与本文件冲突的表述(完成度声明、设计方向、"旧 portal 已下线"等)以本文件为准;docs/standards/ui-design-system.md(v1.0,面向已废弃的 4 微前端方案)自本文件发布之日起废止,其有效内容已并入本文件 §8;docs/architecture/0020_portal_shell_architecture.md(v1.0)为历史评审稿,仅作背景参考。
目录
- 现状审计(2026-07-20 快照)
- 问题根因分析
- 目标架构 v3.0
- 认证与身份链
- 数据层架构
- 安全架构(安全边际)
- 信息架构与页面体系
- 前端设计规范(强制)
- 页面迁移总表(~140 页)
- 实施路线图(P0–P6)
- 后续 AI 工作规范(强制)
- 风险登记册
- 附录
1. 现状审计(2026-07-20 快照)
审计方法:全部结论均可复现。每项发现标注证据(文件路径 + 行号 / 可执行命令)。审计环境:Windows 11 + Node 24,分支 feat/architecture-v2.1,工作区干净。
1.1 验收声明 vs 实测结果
README v2.0 声称"P0–P4 全部验证通过、功能验收 23 项全部 [x]"。实测:构建级声明属实,功能级声明不成立——现有门禁只验证"骨架不塌",从不验证"功能可用"。
| README 声明 |
实测结果 |
结论 |
typecheck 0 错误 |
node node_modules/typescript/bin/tsc --noEmit 无输出退出 |
✅ 属实 |
vitest 206/206 通过 |
实测 19 文件 206 用例全部通过(8.12s) |
✅ 属实 |
build 6 路由生成成功 |
实测成功:/、/_not-found、/api/health、/api/log、/api/ready、/shell/[[...route]] |
✅ 属实——但全应用只有 1 条业务路由 |
| "31 个内置插件全部加载正常" |
31 插件全部注册并能渲染(30 REAL + 1 PARTIAL,无 STUB/MOCK);但其 GraphQL 查询严格匹配 0/50(见 §1.3-F2),运行时全部返回错误/空数据 |
⚠️ 代码真实,数据全断 |
| "31 个 widget 旧纸感令牌全部迁移(1104 次替换,零违规)" |
src/widgets/ 仍残留 271 处旧令牌类(text-heading-*/mt-sm/py-xs/p-md 等),这些类在 Tailwind v4 主题中不存在,渲染时无样式 |
❌ 不属实 |
| "M10 旧 portal 下线完成" |
4 个旧 portal 完整保留在 apps/(teacher 143 文件 / student 118 / parent 190 / admin 84),含全部页面源码 |
❌ 不属实(未删 ≠ 已下线;但也未迁移) |
| "三层安全边界 L1/L2/L3 已落地" |
checkRoutePermission 仅被测试文件引用;无 middleware.ts;shell/[[...route]]/page.tsx 不调用任何权限检查。L1/L2 在生产请求路径上是死代码 |
❌ 代码存在但从未接线 |
| "admin 改配置 → 用户刷新生效" |
无后端时 fetchPluginConfig 返回空插件集,仪表盘渲染"暂无可见插件";本地无种子数据/无 mock,开发态开箱即为空壳 |
❌ 链路存在,起点即断 |
ESLint 强制插件隔离与 notify 封装(no-restricted-imports) |
eslint.config.js 仅有 hex 颜色 + 字体名两条规则;无任何 import 限制规则 |
❌ 不属实 |
1.2 一句话诊断
portal-shell 当前是一个"质量门禁全绿、但没有任何可用页面"的空壳:微内核仪表盘骨架(Shell/Registry/SlotRenderer/PluginBoundary/数据层封装)工程质量合格,但它之上既没有登录认证链,也没有一个真实业务页面;31 个卡片插件查询的是后端并不存在的 GraphQL 字段;距离 CICD 原版(35 模块、4 端完整页面体系)的功能差距约为 140 个页面。
1.3 十大断裂点(按严重度排序,证据化)
F1.【致命】没有任何真实业务页面 —— "前端不可用"的第一根因
- 全应用仅 1 条业务路由:
src/app/shell/[[...route]]/page.tsx(catch-all)。ShellPage 不读取 route 参数(src/app/shell/[[...route]]/page.tsx:26-45),/shell/teacher/grades 与 /shell/admin/users 渲染完全相同的内容;业务区分被压缩到 query params(?classId/?termId 等)。
route-permissions.ts 声明的 24 条路由(如 /shell/admin/users、/shell/teacher/lesson-plans)在文件系统中一条都不存在——权限表保护的是幽灵路由(该表从 CICD 项目平移,本意给网关 middleware 用,Next.js 侧从未接线)。
- 对照资产:CICD 原版 155 个页面(auth 4 + onboarding 1 + dashboard 公共 8 + admin 41 + teacher 54 + student 25 + parent 14 + management/grade 5,含 register/privacy/terms);Edu 旧 4 portal 共 140 个 page.tsx(teacher 56 / student 36 / parent 24 / admin 24)已在仓库内但从未接入 portal-shell(旧 portal 本身缺 management/grade 与 register/onboarding 体系,属缺口待补)。
F2.【致命】GraphQL 契约前后端全面脱节 —— 50 个操作严格匹配 0 个
- 前端
operations/*.graphql.ts 定义 50 个操作(32 query + 18 mutation;README 写 51,其中 GetMyChildrenOverview 改名去重后为 50)。
- 合并 schema(
src/lib/api/__generated__/combined-schema.graphql,700 行)仅有 38 个 Query 字段,且完全没有 Mutation 类型。
- 严格匹配数:0/50。唯一 root 字段名命中的是
GetLayoutTemplates→layoutTemplates,但其选择的 availableSlots 字段在 LayoutTemplateGql 上不存在,仍然校验失败。
- 按域不匹配统计:admin 19/19、teacher 6/6、student 8/8、parent 4/4、sidebar 3/3、topbar 3/3、universal 7/7,全部不匹配。典型样例:
- 列表查询不存在:
grades(classId)/homeworks(classId)/exams(classId) → 后端仅有 grade(id)/homework(id)/exam(id) 单查;schedule/attendance/announcements/myClasses/terms/myChildren/users/roles/permissions/auditLogs/invitationCodes/school/lessonPlans/schedulingRules/myLearningPath/electiveCourses/aiTutorSessions/leaveRequests/search 在 schema 中全部不存在。
- 形状不符:
me{id,name,email,role} → iam User 实为 userId/email/name/status/dataScope;exams 选 name/subject/maxScore → Exam 实为 title/totalScore;notifications(limit,offset) 期望 {items,total} 包装 → msg 实为 notifications(userId: ID!) 平铺数组;errorBookItems 底层为 snake_case(question_id 等)。
- 18 个 mutation 全部无契约(schema 无 Mutation 类型):SaveLessonPlan/ApproveLeave/RejectLeave/UpdateUserStatus/UpdateUserRole/UpdateRolePermissions/CreateInvitationCode/RevokeInvitationCode/UpdateSchool/SaveSchedulingRule/EnrollCourse/DropCourse/MarkErrorMastered/SendAiTutorMessage 及 plugin-manager 的 4 个配置 mutation。
- config-service resolver 实测(唯一声称支撑全架构配置的后端):仅实现
plugin/plugins/layoutTemplates/userLayoutOverride/pluginConfig 5 个 Query;plugin-manager 面板依赖的 rolePluginMapping/roleLayoutDefault 及全部 update*/reset mutation 在 schema 与 resolver 双层均未实现——也就是说,即使后端全栈启动,"admin 改配置"这个插件架构的旗舰功能也无法保存。
- 唯一验证通过的查询:仪表盘启动用的
pluginConfig(不经 operations/,内联于 config-fetcher.ts:24-58;PluginConfigLayoutGql 含 availableSlots/layoutSchemaJson,形状吻合)——这就是"后端就绪时仪表盘能出框架、但所有卡片空转"的原因。
- 编译期防线自毁:
codegen.yml:49-56 注释自述"forward-looking spec fields … not yet present in services subgraph SDL … 44 个 'Cannot query field X' 错误",被迫 skipDocumentsValidation: true;同时 codegen 未配置 typescript-operations,没有生成任何 per-operation 类型,lib/api 的返回类型全部手写——编译期 0 报错,运行期全报错。
- 后端真正可用但没有任何 widget 使用的资产:data-ana 的 4 个仪表盘聚合查询(
teacherDashboard/studentDashboard/parentDashboard/adminDashboard)+ warnings/mastery*/diagnosticReports/errorBook* + config-service 5 个配置查询 + iam user/role/dataScope + core-edu/content 按 id 单查。
- 无 GraphQL schema 的服务:
classes、student-bff、teacher-bff、parent-bff、push-gateway(api-gateway 为 router 除外)——classes 域查询当前无处落地。
F3.【致命】认证与身份链断裂 —— 没有登录页,角色写死 teacher
- 无登录/注册页面;
src/app/page.tsx 直接 redirect("/shell")。
ShellPage 从 x-user-id/x-user-role 请求头取身份,缺失时默认 dev-user / teacher(page.tsx:28-31)——任何人直达 portal-shell 都是"教师"。
- Apollo Client 期望从
localStorage["edu_token"] 读 JWT(ApolloProvider.tsx:18-27),但全应用没有任何代码写入这个 token(没有登录流程)→ 所有 GraphQL 请求永远匿名。
- 浏览器直连 apollo-router :3000,绕过 api-gateway(JWT 校验在 gateway,而 gateway 不在浏览器→router 路径上)。
F4.【严重】安全边界是"纸面合规" —— L1/L2 从未接入请求路径
- 无
middleware.ts;checkRoutePermission/batchCheckRoutePermission 仅被 *.test.ts 引用(grep 实测 60+ 处引用全部在 __tests__)。
- 后果:
/shell/admin/users 对 teacher/student/parent 角色直接放行;唯一的真实过滤发生在 config-service(L3),而它过滤的只是"仪表盘上显示哪些卡片"。
- 权限位图
PERMISSION_BITMAP_ORDER 中 GRADE_READ 重复定义两次(permission-bitmap.ts:62 与 :79)——按"顺序不可变"铁律属数据缺陷,趁未签发真实 JWT 前必须去重。
infra/docker-compose.yml 的 portal-shell 服务(含生产 profile)环境变量写入 NEXT_PUBLIC_DEV_MODE: "true"——生产部署也以 dev 模式运行,等于永久绕过认证。
infra/apollo-router/router.yaml 硬编码 router-authorization: dev-router-secret(明文入库)。
F5.【严重】设计系统三方冲突 + 令牌断裂
- 三个互相矛盾的"权威":
docs/standards/ui-design-system.md(纸感米白 + Fraunces/Inter/JetBrains Mono 三字体 + 学术深蓝,面向已废弃的 4 微前端方案);README v2.0(shadcn 标准 + Inter 单字体);实际实现(packages/ui-tokens = shadcn zinc 默认色板)。
- 31 个 widget 残留 271 处旧令牌类(grep:
text-heading-3/mt-sm/py-xs/p-md/space-y-xs 等),这些类在 @theme 中没有定义 → 相关间距/字号渲染为零值,widget 视觉上是坏的。
- 另有批量出现的
border border 重复类(如 grades-widget/index.tsx:32,42)。
src/styles/tokens.css(README 附录自称"旧,待 P1 移除")仍在源码树中。
F6.【严重】31 个插件 ≈ 31 张信息卡片,不是功能
- 全量审计结论(31/31):30 REAL + 1 PARTIAL,0 STUB / 0 MOCK——插件代码是"真"的,问题是薄且数据断供。平均 139 行/个,形态为"一张卡片 + 列表/表单局部"。
- 典型缺陷(审计实测):
question-bank(唯一 PARTIAL):新建题目不接 mutation,仅 push 进本地 state(local-${Date.now()}),刷新即失。
lesson-plan-editor:仅 标题/目标/内容/资源 四字段表单(216 行);对照 CICD 同功能:lesson-preparation 是全仓最大模块(113 文件:结构树 + 纸面编辑器 + AI 建议 + 版本/审核/发布 + xyflow 画布);错误处理 catch { /* toast */ } 空吞(违反 notify 强制)。
quick-actions:导航目标 /homework/new、/schedule、/grades 等全部不存在(缺 /shell 前缀且无对应路由),点击即掉进 catch-all 仪表盘。
global-search:跳转目标 /students/:id 等不存在;user-menu 无登出项;locale-switcher 只切本地 state 不加载任何 i18n;ai-tutor 切会话不加载历史;notifications-widget 分页 offset 固定 0。
- 对比 CICD 同功能页面量级:考试模块 11 页(all/create/new/[id]/build/edit-rich/analytics/proctoring/grading)、作业模块含扫描阅卷、成绩模块 5 页、教案模块 7 页。
- 插件间无页面级导航目标(没有可跳转的详情页),"点击卡片进详情"无从谈起。
F7.【严重】开发体验不可用 —— 无后端 = 空壳
fetchPluginConfig 三级降级的终点是 getDefaultConfig():plugins: [](config-fetcher.ts:193-212)→ 无后端/无种子数据时,仪表盘渲染空壳"暂无可见插件"。
- 仓库内没有本地 mock/seed/MSW 任何一条让前端独立可运行的路径;旧 portal 里的 MSW handlers(
teacher-portal/src/mocks/handlers-p*.ts,覆盖 P4/P5/P7 场景)未被复用。
F8.【中】路由权限命名双轨制混乱
- README 示例用点号权限(
grade.read),实际 PERMISSION_BITMAP_ORDER 用 <RESOURCE>_<ACTION> 大写下划线(GRADE_READ);widget manifest 的 requiredPermissions 字段 31 个插件无一使用(L2 插件级门禁名存实亡)。
F9.【中】基础设施与文档脱节
- README §6.1 说 7 domain API 文件 "Widget 数" 与 §13.2 表格不一致(topbar 4 插件 vs 2 API 的统计口径混乱,实际 admin.ts 有 20 个 hook);§11.3 测试矩阵与附录 B "95 用例" 自相矛盾(实际 206)。
src/shell/Registry.tsx 头注释写"内置插件共 28 个…admin(3)",实际注册 31 个(admin 6)——代码对、注释陈旧。
docs/standards/ui-design-system.md 全文面向"4 个微前端 + Module Federation"——该架构已被 ADR-033 废弃。
MIGRATION_GUIDE.md/根 README 仍写"前端:Next.js + Module Federation(4 微前端)"。
F10.【低】工程细节
next.config.js 同时保留 Turbopack 与 webpack 双份配置(注释已说明,可接受)。
.env.local 入库(含本地配置,虽无密钥,但 .env.example 已存在时应 gitignore)。
tsconfig.tsbuildinfo(286KB 构建缓存)入库。
1.4 可复用资产盘点(不要把婴儿和洗澡水一起倒掉)
| 资产 |
位置 |
状态 |
处置 |
| 微内核仪表盘骨架(Shell/LayoutManager/SlotRenderer/Registry/PluginBoundary/PluginLifecycle/PropsMerger) |
src/shell/ + src/shared/components/plugin-boundary.tsx |
✅ 工程质量合格,测试覆盖 |
保留,收缩为"角色首页仪表盘"专用 |
| 数据层封装(apollo-client/useWidgetQuery/useWidgetMutation/ApiError) |
src/lib/ |
✅ 合格 |
保留,改走同域代理(§5.2) |
| lib/api 7 domain + 50 operations |
src/lib/api/ |
⚠️ 结构合格;50 操作严格匹配 0 个,类型全部手写 |
保留结构,按真实 schema 逐域重写操作并恢复 codegen 全量类型(§5.3) |
| 三级错误边界 + useErrorReport + notify |
src/shared/ + packages/hooks/ |
✅ 合格 |
保留,扩展到页面级 |
| 权限位图 + route-permissions 表 |
packages/shared-ts/ + src/shared/lib/ |
⚠️ 未接线 + GRADE_READ 重复 |
接线到 middleware,位图去重 |
| shadcn 令牌三层 + ui-components 19 件 |
packages/ui-tokens/ + packages/ui-components/ |
✅ 合格 |
保留为唯一设计系统 |
| 4 个旧 portal 的 140 个页面实现 |
apps/{teacher,student,parent,admin}-portal/ |
⚠️ 完整但栈不同(urql + next-intl + MSW + 纸感令牌) |
迁移源:逐页移植 + 换数据层 + 换令牌 |
| 旧 portal 的 MSW handlers(P4/P5/P7 全覆盖) |
apps/teacher-portal/src/mocks/ 等 |
✅ 可用 |
迁移为 portal-shell 开发兜底层 |
| next-intl messages(zh-CN/en) |
旧 portal src/messages/ |
✅ 可用 |
随页面迁移 |
| PQ Manifest + APQ + Router limits 安全栈 |
public/pq-manifest.json + infra/apollo-router/router.yaml |
✅ 链路完整 |
保留,manifest 随操作清单更新重新生成 |
| 测试基座(vitest + 19 文件 206 用例) |
src/**/__tests__/ |
✅ 合格 |
保留,新增页面级测试 |
2. 问题根因分析
为什么"质量门禁全绿"的系统会完全不可用?后续工作必须避免重蹈覆辙,四条根因:
- 验收指标错位:门禁只覆盖 typecheck/lint/单测/build 这些"骨架指标",从未把"页面数、契约匹配率、关键用户流 E2E"纳入验收。于是"206 测试全过"与"没有一个可用页面"同时成立。对策:§10 的每个阶段退出标准必须包含功能指标 + 可复现证据。
- 契约逆向编写:前端 operations 按 spec 文档"超前"编写(50 个操作按设计意图而非后端实现,0 个通过严格校验),后端子图只落地了 38 个只读查询、0 个 mutation。
skipDocumentsValidation: true + 不生成 per-operation 类型,让这种脱节编译期不可见。对策:§5.3 契约纪律——operations 只允许引用真实 schema 字段,mock 数据走 MSW 而不是"假契约"。
- 范围误判:v2.1 把"统一前端"收缩成"统一仪表盘",默认了"页面以后再说",但没有文档记录这个范围缺口,README 反而把仪表盘骨架的完成写成了整个前端的完成。对策:本文件 §7/§9 把页面体系定义为 portal-shell 的一等公民职责。
- 文档激励扭曲:多处"已完成"声明与实测不符(令牌迁移、旧 portal 下线、安全边界),说明验收只看了 PR 描述没看运行态。对策:§11.6 文档同步纪律——验收声明必须附可复现命令输出,Reviewer 运行命令复核。
3. 目标架构 v3.0
3.1 设计原则
| # |
原则 |
含义 |
| P1 |
页面一等公民 |
业务功能落在真实 Next.js 路由页面上(可寻址、可分享、可深链);插件仪表盘只承担"角色首页聚合" |
| P2 |
契约真实 |
前端 GraphQL 操作只允许引用后端真实 schema;后端未就绪的功能用 MSW mock 数据,禁止"假契约" |
| P3 |
fail-closed |
无身份 → 登录页;无权限 → 403 页;配置缺失 → 内置默认(而非空壳);错误 → 显式兜底(而非静默) |
| P4 |
单一设计系统 |
shadcn 标准令牌(@edu/ui-tokens)为唯一视觉权威;任何页面/插件不引入第二套令牌 |
| P5 |
微内核收敛 |
Shell 插件系统收缩为仪表盘专用机制,不再承载"整个应用"的野心;新增功能默认建页面而非建插件 |
| P6 |
迁移优先于重写 |
140 个旧页面是迁移源而非重写对象;移植时换数据层(urql→Apollo hooks)、换令牌(纸感→shadcn)、保留业务交互逻辑 |
| P7 |
证据化验收 |
每个里程碑以可复现命令 + 运行态截图/录屏验收,禁止"声明即完成" |
3.2 总体结构(混合路由模型)
关键决策(v3.0 ADR 摘要,详见 §3.4):
- V3-A1 混合路由模型:真实页面 + 保留仪表盘微内核。废除"一切皆卡片、单 catch-all 承载全站"的极端;catch-all 仅保留给仪表盘(可选路由参数如
?layout=),新页面一律走显式路由。
- V3-A2 认证链闭环:登录页 +
/api/auth/login 代理 + httpOnly cookie + middleware 校验 + /api/graphql 代理。删除 localStorage JWT 方案。
- V3-A3 门禁接线:
checkRoutePermission 移入 middleware.ts 真实执行;新增路由必须在 route-permissions.ts 登记(CI 校验路由表与文件系统一致性)。
- V3-A4 契约纪律:operations 与真实 schema 强绑定;codegen 恢复
typescript-operations 全量类型;skipDocumentsValidation 随后端补齐分域关闭(§5.3)。
- V3-A5 设计系统单源:shadcn 标准(zinc)为唯一方向,废止纸感全局方案;备课编辑器二期可用模块命名空间(
--lp-*)局部纸感化。
- V3-A6 接入 next-intl:复用旧 portal 的 messages,页面迁移不丢失已翻译文案。
- V3-A7 MSW 开发兜底:
NEXT_PUBLIC_MSW=1 时启用,前端无后端可开发/演示/跑 E2E;生产构建永不包含。
- V3-A8 验收门禁改革:CI 增加契约匹配率检查、路由表一致性检查、页面计数、Playwright E2E(P6)。
3.3 与现有架构的关系(保留什么、改变什么)
| 现有(v2.0) |
v3.0 处置 |
理由 |
单 catch-all /shell/[[...route]] 承载一切 |
收缩:仅渲染角色仪表盘;新增显式路由优先于 catch-all(Next.js 路由规则天然支持) |
页面可寻址、RSC 预取、loading/error 按路由生效 |
| LayoutManager 5 模板(classic/focus/split/triple/canvas) |
保留为仪表盘区布局;页面区用固定 AppFrame(TopBar+Sidebar+Main) |
模板化价值在仪表盘;页面需要稳定框架 |
| 31 个 widget 插件 |
保留骨架,内容逐步替换/下线:与页面功能重复的卡片改为"页面入口卡 + 关键摘要",数据接通真实 schema |
卡片应该是页面的摘要与入口,不是功能的全部 |
| config-service 三层插件配置 |
保留,职责收缩为"仪表盘 + 导航可见性"配置 |
页面不再依赖插件配置,配置挂了不影响页面 |
| SWR 配置静默刷新 |
保留于仪表盘 |
低频配置变更无需推送 |
| URL Search Params 跨插件共享 |
保留并扩展为页面间状态契约(classId/childId/termId) |
已验证有效 |
| RSC 流式渲染(use() + Suspense) |
保留并推广到页面级(页面 RSC 预取 + 流式注入) |
收益真实 |
| 三级错误边界 |
保留,扩展:Route(页面 error.tsx)→ Section(页面区块)→ Widget(卡片) |
与页面体系对齐 |
3.4 v3.0 ADR 全文
V3-A1:混合路由模型(真实页面 + 仪表盘微内核)
- 背景:v2.0 用单 catch-all + 插件配置承载全站,导致页面不可寻址(F1)、权限表空转(F4)、功能止步于卡片(F6)。
- 决策:业务功能落在显式 Next.js 路由页面(
/shell/<role>/<module>/...),共享 AppFrame 布局;插件系统收缩为 /shell 角色首页的聚合仪表盘。
- 取舍:放弃"admin 可配置一切页面"的幻想——页面是代码、受版本控制、可 code review;admin 可配置的收缩为"导航可见性 + 仪表盘卡片 + 卡片 props"。这换来:真实 URL、浏览器前进后退、RSC 流式预取、路由级 loading/error、中间件门禁——全部是 Next.js 原生能力,不再自造。
- 插件与页面的关系:卡片 = 页面的摘要 + 入口。点击卡片 →
router.push 到对应页面。universal 插件按 role 渲染不同摘要视图的机制保留。
V3-A2:认证链闭环 + 删除 localStorage JWT
- 背景:F3(无登录页、localStorage token 无人写入、默认角色 teacher)。
- 决策:
/login 页面(迁移自旧 portal login,换 shadcn 令牌)。
/api/auth/login Route Handler:转发凭证到 iam(经 api-gateway),成功后把 JWT 写入 httpOnly + Secure + SameSite=Strict cookie(project_rules §4);JS 永远接触不到 token。
middleware.ts:读取 cookie → 校验(开发态 decode 验签跳过,生产用 iam JWKS RS256 验签)→ 注入 x-user-id/x-user-role/x-user-permissions 请求头供 RSC 读取;无 token → redirect /login?next=<pathname>。
/api/graphql Route Handler:Apollo Client 的 HttpLink 指向同域 /api/graphql;Handler 从 cookie 取 token 加 Authorization: Bearer 转发 apollo-router,响应原样回传(含 APQ hash 请求)。
- 取舍:多一跳同域代理(<5ms 本地开销),换来 token 全程不出 httpOnly cookie——消除 XSS 窃取凭证面;同时修复"浏览器绕过 api-gateway"问题(代理在服务端调 router,与 gateway 同信任域)。
- DEV_MODE:仅本地开发;提供
dev-token 合成身份(middleware 生成 x-user-* 头)。生产环境变量缺失/为 true 时启动直接拒绝(instrumentation.ts 启动校验)。
V3-A3:路由门禁 middleware 接线 + 权限模型修正
- 背景:F4(checkRoutePermission 死代码)、F8(命名双轨)。
- 决策:
middleware.ts 对每个 /shell/** 请求执行 checkRoutePermission(pathname, bitmap, role);拒绝 → /shell/forbidden(403 页,新)。
route-permissions.ts 四表保留,新增/移动路由必须同步登记;CI 运行一致性脚本:扫描 src/app/**/page.tsx 路由 ⇄ 权限表双向核对,未登记即失败。
- 权限点唯一来源
PERMISSION_BITMAP_ORDER;README 的点号示例作废;31 个 manifest 补登 requiredPermissions(L2 插件级,config-service 过滤用)。
GRADE_READ 重复项去重:保留首次出现位(索引 26),删除第二次(索引 31 处),该位标记 _RESERVED_31 占位永不复用;趁系统未签发真实 JWT,现在就改(P0)。
- 边界说明:middleware 是 L1/L2 的第一道防线(性能/体验),不是唯一防线——resolver 级
@RequirePermission(后端)仍是权威。前端门禁防"走错门",后端门禁防"恶意请求"。
V3-A4:GraphQL 契约纪律
- 背景:F2(50 操作 vs 后端 38 查询/0 mutation,严格匹配 0/50)。
- 决策:
- 操作清单真实化:
operations/*.graphql.ts 只允许引用 combined-schema.graphql 真实存在的字段。每新增一个页面,先确认 schema 有所需字段;没有 → 走"契约工单"(§11.4)推动后端补齐,同时用 MSW mock 让页面先行。
- codegen 恢复强校验:分域关闭
skipDocumentsValidation(哪个域后端补齐了就关哪个域),恢复 typescript-operations 生成操作级类型,删除 lib/api 手写 inline 类型(漂移源)。
- schema 同步自动化:
scripts/normalize-schema.ts 保留;新增 CI 步骤:子图 SDL 变更 → codegen diff 检查,operations 引用不存在字段即失败。
- PQ Manifest 随构建更新:
prebuild 已串联,保留。
- 后端已就绪可立即使用的资产:config-service 全量配置查询;data-ana 的 4 个
*Dashboard 聚合查询 + warnings/mastery*/diagnosticReports/errorBook*;iam 的 user/role/dataScope;core-edu/content 的按 id 单查。仪表盘首页应优先改用这些真实查询(P1 任务)。
V3-A5:设计系统单源决议
- 背景:F5(三方冲突 + 271 处断裂类)。
- 决策:
- shadcn 标准令牌(@edu/ui-tokens,zinc 色板)为唯一设计系统,全局适用(dashboard + 全部页面 + 登录页)。
docs/standards/ui-design-system.md 废止;其仍有价值的内容并入本文件 §8(布局/组件/动效/a11y 规范,按 shadcn 令牌重写)。
- 纸感编辑器方向(Fraunces 主文 + 米白纸面)降级为二期备课编辑器模块命名空间(
--lp-*),仅 lesson-plan 工作台内部使用;现在不做。
- 31 widget 令牌清债(271 处)列入 P1:机械替换(
text-heading-3→text-lg font-semibold、mt-sm→mt-2、py-xs→py-1、px-sm→px-2、space-y-xs→space-y-1、p-md→p-4、gap-xs→gap-1、border border→border),arch:scan 增加"未知 Tailwind 类"检测规则。
- 理由:CICD 是已验证的 UX 基线,shadcn 生态(组件、文档、AI 协作默契度)最成熟;纸感全局化在旧 portal 实践中与 shadcn 组件冲突成本高,收敛为局部模块更现实。
V3-A6:next-intl 正式接入
- 背景:旧页面用
useTranslations,messages 资产完整;portal-shell 现有自造 useT() 只有 3 条文案。
- 决策:接入 next-intl(App Router 模式),
messages/zh-CN.json + messages/en.json 从旧 portal 合并迁移;ThemeI18nProvider 删除自造 i18n,保留主题切换;新代码文案一律走 useTranslations,禁止硬编码中文字符串(ESLint 规则 P1 后启用,迁移期 warn)。
- 取舍:增加一个依赖与少量配置,换取不重写全部文案 + 与 CICD/旧 portal 一致的 i18n 习惯。
V3-A7:MSW 开发兜底层
- 背景:F7(无后端 = 空壳)。
- 决策:迁移旧 portal 的 MSW handlers 为 portal-shell 的
src/mocks/(按 domain 分文件);NEXT_PUBLIC_MSW=1 时 browser worker 拦截 /api/graphql 与 /api/v1/*;src/instrumentation.ts(或 middleware 旁路)在 dev + MSW 开启时给 RSC 侧也注入 mock fetcher。生产构建通过 env 条件 import 保证 mocks 零字节进 bundle。
- 双重收益:前端不依赖后端就绪即可开发页面;Playwright E2E 直接复用 MSW 场景数据,测试稳定。
V3-A8:验收门禁改革
- 背景:根因 1(验收指标错位)。
- 决策:CI 质量门禁从 4 项扩为 8 项:
lint + typecheck + vitest + build + 契约校验(codegen diff)+ 路由表一致性 + 页面计数回归(pages 数不得下降)+ E2E 冒烟(P6 起:登录 → 仪表盘 → 每角色 1 条核心流)。每个阶段的 README/本文件验收勾选必须附命令输出或截图链接。
4. 认证与身份链
4.1 目标流程
4.2 组件清单
| 组件 |
文件 |
职责 |
| 登录页 |
src/app/login/page.tsx |
表单 → /api/auth/login;已登录跳转 next;迁移自旧 portal,换 shadcn 令牌 |
| 登录代理 |
src/app/api/auth/login/route.ts |
调 gateway iam 登录 → Set-Cookie;失败归一化错误;限流透传 |
| 登出 |
src/app/api/auth/logout/route.ts |
清 cookie + 调 iam 注销 |
| 中间件 |
src/middleware.ts |
公共路径白名单 → cookie 校验 → 注入身份头 → checkRoutePermission → 重定向 |
| GraphQL 代理 |
src/app/api/graphql/route.ts |
cookie→Bearer 转发;APQ 透传;错误归一化;cache: "no-store" |
| RSC 身份工具 |
src/lib/auth/server.ts |
getServerIdentity(): Promise<{userId, role, permissions}>(读 middleware 注入的头,缺失即抛 → error boundary 跳登录) |
| DEV_MODE 守卫 |
src/instrumentation.ts |
NODE_ENV=production && NEXT_PUBLIC_DEV_MODE=true → 启动即抛错 |
| 403 页 |
src/app/shell/forbidden/page.tsx |
说明 + 返回仪表盘链接 |
4.3 会话细节决策
- cookie 名:
edu_session;Path=/;Max-Age 与 JWT exp 对齐;Secure 仅生产(本地 http 开发豁免,用 env 控制)。
- 验签:middleware 用
jose 的 createRemoteJWKSet(iam JWKS endpoint) 验 RS256;JWKS 端点不可达时 fail-closed(跳登录),dev 模式降级为仅 decode(日志警告)。
- 权限位图来源:登录响应中 iam 返回 permissions → login route 计算 base36 位图 → 写入第二个非 httpOnly cookie
edu_perms(JS 可读,用于客户端按钮级显隐;仅为 UX,不作为安全依据——安全判断一律走后端 resolver + middleware 的 httpOnly 通道)。middleware 同样把位图注入 x-user-permissions 头。
- 角色切换/多角色:iam 返回单主角色(与现
x-user-role 对齐);多角色支持属后端二期,前端不预留。
- 旧 portal 的
lib/auth.ts(localStorage 方案):迁移时删除,不按原样搬运。
5. 数据层架构
5.1 分层(保留 v2.0 四层,修正两端)
- RSC 服务端:页面 RSC 用
createApolloClient()(已有)经内网直连 router(服务端不经代理,直接带 middleware 解析出的身份头);客户端组件经 /api/graphql 代理。
- SWR/轮询:仪表盘配置保留 SWR;业务数据默认 Apollo
fetchPolicy: "cache-first",需要实时性的(通知铃铛)用 pollInterval;SSE 通知二期经 realtime-gateway。
5.2 GraphQL 同域代理(/api/graphql)
- 单文件 Route Handler,
POST only;透传 body(APQ hash 或 query);注入 Authorization;响应状态/JSON 原样返回。
- 不缓存(
export const dynamic = "force-dynamic")。
- 错误归一化:网络错误 →
{ errors: [{ message: "UPSTREAM_UNAVAILABLE" }] },客户端 ApiError 统一。
- 保留直连开关:
APOLLO_ROUTER_URL(服务端 RSC 用),客户端永远只用 /api/graphql。
5.3 契约纪律(frontend ⇄ backend)
- schema 唯一源:
src/lib/api/__generated__/combined-schema.graphql 由 scripts/normalize-schema.ts 从 services/*/src/graphql/generated/schema.graphql 生成;后端 SDL 变更后必须重跑 codegen,CI 做 diff 检查。
- operations 真实性:每个 operation 引用字段必须存在于 combined schema;恢复
typescript-operations 生成类型;lib/api 的手写 interface 逐步删除(P1 起按域推进:config → data-ana → core-edu → content → msg → iam)。
- 后端未就绪的功能:页面允许先上,数据走 MSW(
src/mocks/),并在页面头部注释 @contract-pending: <工单号>;契约工单(§11.4)登记后端待补字段,每周同步一次状态。
- 禁止事项:禁止在 widget/页面内联 gql;禁止手写与 schema 冲突的类型;禁止为迁就前端瞎改 normalize 脚本(当前对 ai 子图的 String 改写保留,单独登记 TD)。
5.4 MSW 兜底层
- 位置:
src/mocks/(browser.ts/server.ts/handlers/<domain>.ts),自旧 portal src/mocks/handlers-p*.ts 迁移并按 50+ 操作重组。
- 启用:
NEXT_PUBLIC_MSW=1(仅 dev/test);src/app/layout.tsx 条件 import("@/mocks/browser")(动态 import,production env 下该 import 永不执行 → tree-shaken)。
- 数据原则:使用与种子用户一致的语义数据(teacher2@edu.test 的班级/学生/成绩),数据量足够展示分页/空态/超长文本三种边界。
- E2E 复用:Playwright webServer 以
NEXT_PUBLIC_MSW=1 启动,用例不依赖真实后端。
5.5 后端已就绪查询的立即利用(P1 必做)
| 真实查询(schema 存在且 resolver 已实现) |
用于 |
注意 |
pluginConfig(userId, role) / plugins / plugin(pluginId) / userLayoutOverride(userId) |
仪表盘配置(已接,保留) |
✅ 全字段吻合 |
layoutTemplates |
admin 模板列表 |
⚠️ 仅 layoutId/displayName/description/isActive,无 availableSlots——前端选择集必须裁剪 |
teacherDashboard / studentDashboard / parentDashboard / adminDashboard |
角色首页仪表盘主数据源(替换现有卡片的假契约查询) |
✅,底层字段 snake_case 需在 lib/api 层映射 |
warnings / masterySummary / studentMastery / masteryDistribution |
学情预警卡片 |
✅,同上 |
diagnosticReports / errorBookItems / errorBookStats |
诊断/错题卡片与页面 |
✅,字段形状与现 widget 期望不同,需重写 hook |
user(userId) / role(roleId) / dataScope(userId) |
用户菜单、权限显隐 |
⚠️ User 字段为 userId/email/name/status/dataScope(无 id/role) |
exam(id) / homework(id) / grade(id) / classInfo(id) / textbook(id) / question(id) / chapter(id) / knowledgePoint(id) |
详情页首版(列表点击带 id 进入时可真实查询) |
⚠️ 字段名需按真实类型重写(如 Exam.title/totalScore 而非 name/maxScore) |
notifications(userId) |
通知中心首版 |
⚠️ 平铺数组,无分页包装 |
lessonPlanStatus |
ai 子图唯一查询 |
仅状态轮询可用 |
6. 安全架构(安全边际)
6.1 威胁模型与防线矩阵
| 威胁 |
防线 |
位置 |
现状 |
v3.0 处置 |
| 未认证访问业务页 |
middleware cookie 校验 |
src/middleware.ts |
❌ 不存在 |
P0 实现,fail-closed |
| 越权访问路由(teacher 进 admin 页) |
L1 角色 + L2 权限点 |
middleware + route-permissions 表 |
❌ 死代码 |
P0 接线 + CI 一致性 |
| 越权访问数据 |
resolver @RequirePermission + DataScope |
后端子图 |
✅(审计过 50 resolver) |
保持;前端不兜底也不替代 |
| 凭证被 XSS 窃取 |
httpOnly cookie,JS 零接触 |
/api/auth/login |
❌ localStorage 方案 |
P0 替换 |
| GraphQL 任意查询探测 |
APQ + PQ Manifest 白名单 |
Apollo link + router |
✅ 链路完整(require_manifest 生产需开) |
保持;部署 env 列入 P6 检查单 |
| 深度/成本/批量 DoS |
router limits |
router.yaml |
✅ 已配 |
保持 |
| 绕过 router 直调子图 |
RouterAuthGuard + router-authorization |
子图 |
⚠️ 密钥明文 dev-router-secret 入库 |
P6 换 env 注入;生产 secret 轮换 |
| CSRF |
SameSite=Strict cookie + 自定义头要求 |
cookie + 代理 |
⚠️ 依赖 cookie 属性 |
/api/graphql 要求 content-type: application/json(预检),登录接口 SameSite=Strict |
| 生产 DEV_MODE 误开 |
启动自检 |
instrumentation.ts |
❌ compose 生产 profile 写 true |
P0:compose 改 false + 启动校验抛错 |
| 错误上报刷爆 |
sessionStorage digest 节流 |
useErrorReport |
✅ 已实现 |
保持 |
| XSS 富文本 |
禁 dangerouslySetInnerHTML(grep 实测 src 内 0 处) |
ESLint + 规范 |
✅ |
保持规则;富文本编辑器(二期)必须 DOMPurify |
| 敏感信息入仓 |
.env.local 已入库(无密钥) |
仓库 |
⚠️ |
P0:移出 + gitignore 补规则 |
6.2 三层安全边界(接线后真实形态)
| 层 |
机制 |
执行点 |
失败行为 |
| L1 角色门禁 |
requiredRoles |
middleware(每个 /shell/** 请求) |
302 → /shell/forbidden |
| L2 权限点 |
requiredPermissions/anyOfPermissions(AND/OR,位图校验) |
middleware(路由级)+ config-service(卡片级) |
302 → 403 页 / 卡片不渲染 |
| L3 数据范围 |
DataScope 6 级 |
后端 resolver(权威);前端仅 UX 显隐 |
后端拒绝 / 空态 |
边界纪律(防"安全边际"幻觉):
- 前端门禁是体验层,后端 resolver 是权威层;任何"前端已挡"不得成为后端不加
@RequirePermission 的理由。
edu_perms 可读 cookie 只用于按钮显隐;禁止用它做任何数据请求的放行判断。
- middleware 校验失败一律 fail-closed;
checkRoutePermission 未匹配的路由默认拒绝(当前实现默认放行,P0 改为:/shell/** 下未登记路由默认拒绝,公共路径显式白名单)。
- JWT 过期统一处理:
/api/graphql 收到 401 → 清 cookie → 返回 { errors: [{extensions:{code:"UNAUTHENTICATED"}}] };客户端 Apollo errorLink 拦截 → location.href = "/login"。
7. 信息架构与页面体系
7.1 路由树(目标态)
- URL 前缀
/shell/<role>/ 与 route-permissions 表语义对齐(PREFIX 表按前缀批量保护)。
- 仪表盘
/shell 按角色渲染不同插件集(现有机制),同时是各功能卡的入口枢纽。
- 跨角色同构页面(如 notifications/settings)放共享路径,权限表按角色开放。
7.2 AppFrame(页面框架)
src/app/shell/layout.tsx(新):
- TopBar:复用现有 4 个 topbar 插件(它们本就是全站级组件),不再走插件配置(改为静态挂载 + 权限显隐),仪表盘配置不再能"关掉导航"导致页面失联。
- Sidebar 导航菜单(新组件):导航项 = 静态注册表
src/shared/lib/navigation.ts(label/i18n key/icon/href/requiredRoles/requiredPermissions),渲染前用 batchCheckRoutePermission 过滤(对应 004 §5.4 视口 L1);当前路由高亮;收纳现有 4 个 sidebar 插件(class-selector/child-selector/term-switcher/quick-actions)于菜单下方"上下文区"。
- Main:仪表盘路由渲染微内核仪表盘;业务路由渲染页面。页面内可用
DashboardSection/SectionErrorBoundary 组织区块。
- RSC 流式:layout 保持轻量(导航静态),页面各自 RSC 预取 + Suspense;现有 ClientShell 流式机制迁移为页面级工具(
src/lib/streaming.tsx 导出 StreamedPage 封装,避免每页重写 Provider/Suspense 样板)。
7.3 页面四种类型与模板(新页面必须套模板)
| 类型 |
适用 |
结构骨架 |
示例 |
| 列表页 |
集合浏览 + 筛选 + 分页 |
PageHeader + FilterBar + DataTable/列表 + Pagination + 空态/骨架/错误三态 |
exams、homework、users |
| 详情页 |
单实体查看 + 关联操作 |
PageHeader(标题+操作) + 信息区 + Tabs/分区 + 关联列表 |
exams/[id]、textbooks/[id] |
| 表单页 |
创建/编辑 |
PageHeader + 表单(react-hook-form + zod)+ 提交/取消 + 错误摘要 |
exams/new、lesson-plans/new |
| 工作台页 |
多栏协同复杂任务 |
三栏(树/画布/属性)复合组件(Workbench 模式) |
lesson-plans/[id]/edit、exams/[id]/build |
模板代码位置:src/shared/components/page-templates/{list-page,detail-page,form-page,workbench-page}.tsx(P1 建)。新页面一律 import 模板而非手排布局——保证 140 页视觉/交互一致,也是 AI 批量迁移时的统一落点。
7.4 页面级数据获取模式(统一)
- 列表/详情页:RSC 预取首屏 →
use() 流式注入 → 客户端交互(筛选/翻页)走 Apollo hooks。
- 表单页:客户端组件 +
useWidgetMutation;成功后 router.refresh()。
- 每页必须实现三态:
loading.tsx(骨架,复用 PluginSkeleton 变体)/ error.tsx(Route 级边界,已有模式)/ 空态(empty-state.tsx)。
8. 前端设计规范(强制)
本章是 portal-shell 唯一的视觉/交互权威,废止 docs/standards/ui-design-system.md。所有新页面/插件/组件必须遵守;迁移页面时按本章改造。违反本节任意"铁律"的 PR 不得合入。
8.1 设计令牌(三层,唯一来源)
| 层 |
位置 |
业务可用性 |
| L1 Primitive |
packages/ui-tokens/src/primitive.css |
❌ 禁止直接引用 |
| L2 Semantic |
packages/ui-tokens/src/semantic-{light,dark}.css |
✅ 经 hsl(var(--*)) |
| L3 Tailwind |
packages/ui-tokens/src/tailwind-theme.css(@theme inline) |
✅ 首选:bg-background/text-foreground/bg-card/border 等 |
铁律(ESLint + code review 双重执行):
- 禁
#hex 字面量(现有规则保留)。
- 禁字体名字面量(现有规则保留)。
- 禁
font-size: Npx;字号用 Tailwind 阶梯(text-xs/sm/base/lg/xl/2xl)或 text-size-*。
- 禁 Tailwind 任意值(
w-[137px]);间距用默认阶梯(p-2/p-4/p-6/gap-2/gap-4)或 --space-*。
- 禁引入第二套令牌/第二套 CSS 变量体系(纸感
--paper-*/--ink-* 一律不得回潮;迁移旧页面时必须清除)。
- 暗色主题:仅经
.dark class + 语义令牌响应;禁组件内写死亮色值。
- 圆角:
rounded-xl(卡)/rounded-md(控件)/rounded-full(头像徽章);阴影克制:浮层 shadow-sm,卡片默认无阴影。
8.2 字体与排版
- 单一字体族 Inter(
next/font 已挂载 --font-inter → font-sans);数字密集表格列用 font-mono(Tailwind 默认等宽)右对齐。
- 标题层级:页面标题
text-2xl font-semibold;区块标题 text-lg font-semibold;卡片标题 text-base font-medium;正文 text-sm;辅助 text-xs text-muted-foreground。
- 行高:正文
leading-relaxed;标题 leading-tight。
- 中文界面默认
lang="zh-CN"(已配置);i18n 文案经 next-intl。
8.3 组件使用
- 基础组件一律来自
src/shared/components/ui/*(shadcn 标准件)与 packages/ui-components/*(data-table/form/modal/chart/calendar 等 19 件);缺组件先扩库再使用,禁止页面内手写第三套按钮/输入框。
- 业务复合件:
page-templates 四件套(§7.3)+ page-header/stat-card/stats-grid/empty-state/filter-bar(已有)。
- Toast 一律
notify.*(src/shared/lib/notify.ts);禁 import { toast } from "sonner"(P0 补 ESLint no-restricted-imports 真正落地);禁空 catch(现有 catch { /* toast */ } 反例必须改)。
- 图标:
lucide-react 单一来源;图标按钮必须 aria-label。
- 表格:数据列数字
font-mono 右对齐;行分隔 divide-y divide-border;禁大面积色块。
8.4 布局与间距
- 页面容器:
px-6 py-6(桌面),最大宽度不设限(工作台型产品),列表页可 max-w-7xl。
- 区块间距
space-y-6;卡片内边距 p-4 或 p-6。
- 三栏工作台仅在"编辑类"页面使用(备课/组卷),比例 260px / 1fr / 380px(Workbench 模板内置)。
8.5 动效
- 交互反馈 ≤ 200ms;页面过渡 ≤ 300ms;遵守
prefers-reduced-motion(globals.css 已有)。
- 加载一律骨架屏(PluginSkeleton 五变体 / 页面 loading.tsx),禁居中 spinner 长转。
- 禁装饰性持续动画;hover 反馈用
bg-muted/bg-accent,禁 scale-105。
8.6 可访问性(WCAG AA)
- 对比度:正文 ≥ 4.5:1(zinc 令牌天然满足,禁自调浅色)。
- 所有交互元素可 Tab 聚焦;
focus-visible:ring-2 focus-visible:ring-ring。
- 语义化标签(
nav/main/aside/table);表单控件必须 <label> 或 aria-label;动态更新区 aria-live="polite"(骨架组件已带,页面复用即可)。
- 右键/下拉菜单支持键盘(Radix 组件默认满足,勿破坏)。
8.7 文案与 i18n
- 一律
useTranslations;命名空间按页面域(exams.*/homework.*/common.*)。
- 占位/空态文案必须给出"下一步行动"(如"暂无考试 → 新建考试"按钮),禁裸"暂无数据"。
9. 页面迁移总表(~140 页)
功能全景基准:CICD 原版 155 页(Next.js 16 + Server Actions + Drizzle,无 GraphQL;auth 4 + onboarding 1 + dashboard 公共 8 + admin 41 + teacher 54 + student 25 + parent 14 + management/grade 5)。
迁移源:仓库内 4 个旧 portal(apps/*-portal/,共 140 个 page.tsx),其页面实现完整但栈为 urql + next-intl + MSW + 纸感令牌。
缺口说明:旧 portal 未覆盖 CICD 的 management/grade 年级组维度(5 页)与 register/privacy/terms/onboarding(4+ 页)——这两块列为新增(N),排在对应批次末尾。
迁移动作统一定义:M = 迁移改造(移植交互逻辑 + 换 Apollo 数据层 + 换 shadcn 令牌 + 套页面模板);N = 新建(旧版没有);R = 替换(现有 widget 升级为页面)。
批次即 §10 路线图的阶段:B1=先行骨架(P1)、B2=教师(P2)、B3=学生(P3)、B4=家长(P4)、B5=管理(P5)。
契约列:✅=schema 已有(注意字段形状差异,见 §5.5);🟡=部分(单查有/列表无);❌=schema 无,需契约工单 + MSW 先行。
9.1 教师域(teacher-portal 56 页 → /shell/teacher/*,B1+B2)
| 源路由(teacher-portal) |
目标路由 |
动作 |
契约 |
批次 |
login |
/login |
M(全站唯一登录页) |
✅ |
B1 |
(app)/dashboard |
/shell(教师仪表盘) |
R(仪表盘改接 teacherDashboard 真实查询) |
✅ |
B1 |
(app)/exams |
/shell/teacher/exams |
M 列表页 |
🟡 |
B2 |
(app)/exams/new |
/shell/teacher/exams/new |
M 表单页 |
❌ mutation |
B2 |
(app)/exams/[id] |
/shell/teacher/exams/[id] |
M 详情页 |
✅ exam(id) |
B2 |
(app)/exams/[id]/build |
/shell/teacher/exams/[id]/build |
M 工作台页 |
❌ |
B2 |
(app)/exams/[id]/edit-rich |
/shell/teacher/exams/[id]/edit |
M 工作台页 |
❌ |
B2 |
(app)/exams/[id]/analytics |
/shell/teacher/exams/[id]/analytics |
M 详情页(图表) |
🟡 assignmentAnalysis |
B2 |
(app)/exams/[id]/proctoring |
/shell/teacher/exams/[id]/proctoring |
M(二期,WS) |
❌ |
B2 末 |
(app)/homework、/new、/[id]、/submissions(共 7 页) |
/shell/teacher/homework/*(同构 7 页) |
M |
🟡/❌ |
B2 |
(app)/grades、/entry、/analytics、/stats、/report-card(5 页) |
/shell/teacher/grades/*(5 页) |
M |
🟡/❌ |
B2 |
(app)/lesson-plans、/new、/library、/calendar、/heatmap、/[planId]/edit(6 页) |
/shell/teacher/lesson-plans/*(6 页) |
M(edit 为工作台页) |
❌ |
B2 |
(app)/questions |
/shell/teacher/questions |
M 列表页 |
🟡 question(id) |
B2 |
(app)/textbooks、/[id](2 页) |
/shell/teacher/textbooks/*(2 页) |
M |
🟡 textbook(id) |
B2 |
(app)/attendance、/sheet、/report、/stats(4 页) |
/shell/teacher/attendance/*(4 页) |
M |
❌ |
B2 |
(app)/classes、/[id]、/schedule(3 页) |
/shell/teacher/classes/*(3 页) |
M |
🟡 classInfo(id) |
B2 |
(app)/students |
/shell/teacher/students |
M |
❌ |
B2 |
(app)/course-plans、/[id](2 页) |
/shell/teacher/course-plans/*(2 页) |
M |
❌ |
B2 |
(app)/elective、/create、/[id]/edit(3 页) |
/shell/teacher/elective/*(3 页) |
M |
❌ |
B2 |
(app)/error-book |
/shell/teacher/error-book |
M |
✅ errorBookItems/Stats |
B2 |
(app)/diagnostic、/class/[classId](2 页) |
/shell/teacher/diagnostic/*(2 页) |
M |
✅ diagnosticReports |
B2 |
(app)/analytics、/[studentId](2 页) |
/shell/teacher/analytics/*(2 页) |
M |
🟡 data-ana 多查询 |
B2 |
(app)/ai-assist、/ai-lesson-plan、/ai-report(3 页) |
/shell/teacher/ai/*(3 页) |
M |
❌(ai 子图仅 lessonPlanStatus) |
B2 末 |
(app)/knowledge-graph |
/shell/teacher/knowledge-graph |
M |
🟡 knowledgePoint(id) |
B2 末 |
(app)/practice |
/shell/teacher/practice |
M |
❌ |
B2 |
(app)/schedule-changes |
/shell/teacher/schedule-changes |
M |
❌ |
B2 |
(app)/leave |
/shell/teacher/leave |
M |
❌ |
B2 |
(app)/notifications |
/shell/notifications(共享) |
M |
✅ notifications(userId) |
B1 |
(app)/settings |
/shell/settings(共享) |
M |
✅ |
B1 |
9.2 学生域(student-portal 36 页 → /shell/student/*,B3)
| 源路由 |
目标路由 |
动作 |
契约 |
批次 |
dashboard、/trend、/weakness |
/shell(学生仪表盘)+ 2 详情页 |
R + M |
✅ studentDashboard/learningTrend/studentWeakness |
B3 |
my-grades、/report-card |
/shell/student/grades/*(2 页) |
M |
❌(列表) |
B3 |
my-exams、/[id]/result、/[id]/take |
/shell/student/exams/*(3 页) |
M(take 为作答工作台) |
🟡/❌ |
B3 |
my-homework、/[id]/submit、/[id]/analysis |
/shell/student/homework/*(3 页) |
M |
🟡/❌ |
B3 |
schedule |
/shell/student/schedule |
M |
❌ |
B3 |
my-attendance |
/shell/student/attendance |
M |
❌ |
B3 |
my-classes |
/shell/student/classes |
M |
❌ myClasses |
B3 |
courses、/[id] |
/shell/student/courses/*(2 页) |
M |
❌ |
B3 |
course-plans、/[id] |
/shell/student/course-plans/*(2 页) |
M |
❌ |
B3 |
lesson-plans、/[id]/view |
/shell/student/lesson-plans/*(2 页) |
M |
❌ |
B3 |
textbooks、/[id]/chapters |
/shell/student/textbooks/*(2 页) |
M |
🟡 |
B3 |
error-book |
/shell/student/error-book |
M |
✅ |
B3 |
learning、learning-path |
/shell/student/learning、/learning-path |
M |
❌ |
B3 |
practice、/[sessionId] |
/shell/student/practice/*(2 页) |
M |
❌ |
B3 |
elective、/[id] |
/shell/student/elective/*(2 页) |
M |
❌ |
B3 |
ai-tutor |
/shell/student/ai-tutor |
M |
❌ |
B3 末 |
announcements、/[id] |
/shell/announcements/*(共享 2 页) |
M |
❌(列表) |
B3 |
messages |
/shell/messages |
M |
❌ |
B3 末 |
notifications、settings |
共享路由 |
M |
✅ |
B1/B3 |
leave |
/shell/student/leave |
M |
❌ |
B3 |
9.3 家长域(parent-portal 24 页 → /shell/parent/*,B4)
| 源路由 |
目标路由 |
动作 |
契约 |
批次 |
parent/dashboard、/trend、/weakness |
/shell(家长仪表盘)+ 2 页 |
R + M |
✅ parentDashboard |
B4 |
parent/children/[studentId] |
/shell/parent/children/[studentId] |
M |
❌ myChildren |
B4 |
parent/grades、/report-card |
/shell/parent/grades/*(2 页) |
M |
❌ |
B4 |
parent/exams、/[id]/result |
/shell/parent/exams/*(2 页) |
M |
🟡/❌ |
B4 |
parent/homework |
/shell/parent/homework |
M |
❌ |
B4 |
parent/attendance |
/shell/parent/attendance |
M |
❌ |
B4 |
parent/classes |
/shell/parent/classes |
M |
❌ |
B4 |
parent/course-plans、/[id] |
/shell/parent/course-plans/*(2 页) |
M |
❌ |
B4 |
parent/lesson-plans、/[planId]/view |
/shell/parent/lesson-plans/*(2 页) |
M |
❌ |
B4 |
parent/error-book |
/shell/parent/error-book |
M |
✅ |
B4 |
parent/diagnostic |
/shell/parent/diagnostic |
M |
✅ |
B4 |
parent/learning-path |
/shell/parent/learning-path |
M |
❌ |
B4 |
parent/practice |
/shell/parent/practice |
M |
❌ |
B4 |
parent/elective |
/shell/parent/elective |
M |
❌ |
B4 |
parent/leave |
/shell/parent/leave |
M |
❌ |
B4 |
parent/notifications、/settings、/preferences |
共享 + /shell/parent/preferences |
M |
✅/❌ |
B4 |
9.4 管理域(admin-portal 24 页 → /shell/admin/*,B5)
| 源路由 |
目标路由 |
动作 |
契约 |
批次 |
admin/dashboard |
/shell(管理仪表盘) |
R |
✅ adminDashboard |
B5 |
admin/users |
/shell/admin/users |
R(现有 widget 升级整页) |
❌ users 列表 |
B5 |
admin/roles、admin/permissions |
/shell/admin/roles、/permissions(2 页) |
R/M |
❌ |
B5 |
admin/audit-logs + 3 子页 |
/shell/admin/audit-logs/*(4 页) |
M |
❌ |
B5 |
admin/invitation-codes |
/shell/admin/invitation-codes |
R |
❌ |
B5 |
admin/school + 5 子页 |
/shell/admin/school/*(6 页) |
M |
❌ |
B5 |
admin/classes |
/shell/admin/classes |
M |
❌ |
B5 |
admin/students、admin/teachers |
/shell/admin/students、/teachers(2 页) |
M |
❌ |
B5 |
admin/organization |
/shell/admin/organization |
M |
❌ |
B5 |
admin/announcements |
/shell/admin/announcements |
M |
❌ |
B5 |
admin/files |
/shell/admin/files |
M |
❌ |
B5 |
admin/ai-settings |
/shell/admin/ai-settings |
M |
❌ |
B5 |
admin/system |
/shell/admin/system |
M |
❌ |
B5 |
admin/viewports |
/shell/admin/viewports |
M(对齐 004 §5.4 视口配置) |
❌ |
B5 |
| —(新)插件管理 |
/shell/admin/plugins |
R(现有 plugin-manager 升级整页) |
✅ config-service |
B5 |
9.5 计数与节奏
| 域 |
页面数 |
契约已就绪(✅/🟡) |
批次 |
| 共享(login/notifications/settings/announcements/messages/forbidden) |
~7 |
4 |
B1/B3 |
| 教师 |
~50 |
~14 |
B1+B2 |
| 学生 |
~34 |
~8 |
B3 |
| 家长 |
~21 |
~5 |
B4 |
| 管理 |
~22 |
~3 |
B5 |
| 缺口新增(management/grade 5 页 + register/onboarding/privacy/terms ~5 页,CICD 有而旧 portal 无) |
~10 |
0 |
B2/B5 末(N) |
| 合计 |
~144 |
~34 |
— |
节奏原则:契约就绪页先行;❌ 页用 MSW 先上 UI,契约工单跟踪后端补齐后切换真实查询(切换 = 改 hook 的 fetcher 指向,页面不动)。
10. 实施路线图(P0–P6)
每阶段列出:目标 / 范围 / 验收标准(可复现命令 + 运行态证据)。阶段内任务按 §11 规范拆给多 AI 并行。未完成上一阶段退出标准不得进入下一阶段。
P0 · 地基止血(1 周)— ✅ 已完成(2026-07-22 验收)
目标:消除"空壳即不可用 + 认证裸奔"两大致命伤。
| # |
任务 |
验收 |
状态 |
| P0-1 |
/login 页 + `/api/auth/login |
logout+edu_session` httpOnly cookie |
本地起 iam/gateway,错误密码报错、正确密码进 /shell;cookie 在 DevTools Application 可见且 httpOnly |
| P0-2 |
src/middleware.ts:cookie 校验 + 身份头注入 + checkRoutePermission 接线 + /shell/forbidden |
无 cookie 访问 /shell/** → 302 /login;student 访问 /shell/admin/users → 302 /shell/forbidden(截图) |
✅ |
| P0-3 |
/api/graphql 代理 + Apollo Client 指向同域 + 删除 localStorage token 方案 |
DevTools Network 面板无 localhost:3000 直连;grep -r "localStorage" src/providers 无 token 读取 |
✅ |
| P0-4 |
配置兜底改进:getDefaultConfig() 返回内置默认仪表盘插件集(按角色静态定义),config-service 不可用时仪表盘仍有内容 |
停掉 config-service 与 router,启动前端,/shell 显示默认卡片而非"暂无可见插件"(截图) |
✅ |
| P0-5 |
DEV_MODE 生产守卫:instrumentation.ts 启动校验 + docker-compose.yml 生产 profile 改 NEXT_PUBLIC_DEV_MODE: "false" |
NODE_ENV=production NEXT_PUBLIC_DEV_MODE=true pnpm start 启动即报错退出 |
✅ |
| P0-6 |
权限位图修正:GRADE_READ 去重(保留索引位,二见位改 _RESERVED_)+ 单测更新 |
vitest run permission-bitmap 通过;validateRoutePermissionConfigs() 返回空 |
✅ |
| P0-7 |
ESLint 补齐:no-restricted-imports(禁 widget 跨目录 import、禁直 import sonner、禁页面绕过 lib/api 直 import @apollo/client) |
pnpm lint 对预埋反例报错(附输出) |
✅ |
| P0-8 |
仓库卫生:.env.local 移出版本控制 + .gitignore 补 tsconfig.tsbuildinfo |
git status 干净 |
✅ |
P0 验收证据(2026-07-22):
P1 · 框架与数据源接通(1–2 周)
目标:AppFrame + 导航 + 真实数据仪表盘 + 页面模板 + MSW + i18n,页面迁移的"流水线"建成。
| # |
任务 |
验收 |
| P1-1 |
src/app/shell/layout.tsx AppFrame(TopBar 静态挂载 + Sidebar 导航菜单 navigation.ts + 权限过滤) |
4 角色各见各菜单;导航项与 route-permissions 表一致(单测) |
| P1-2 |
仪表盘改接真实查询:teacherDashboard/studentDashboard/parentDashboard/adminDashboard + warnings/errorBookStats |
起全栈,4 角色仪表盘显示真实聚合数据(截图);widget 假契约查询(grades/homeworks/schedule/attendance/exams/announcements)全部下线或改造 |
| P1-3 |
页面模板四件套(list/detail/form/workbench)+ 三态规范 |
Storybook 或示例页 4 张(/shell/_dev/templates/*,仅 dev 可见) |
| P1-4 |
next-intl 接入 + messages 合并迁移 |
切换 locale 页面文案切换(截图);useT 自造函数删除 |
| P1-5 |
MSW 兜底层(迁移旧 handlers,覆盖 dashboard/users/exams/grades 四域起步) |
NEXT_PUBLIC_MSW=1 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks |
| P1-6 |
31 widget 令牌清债(271 处机械替换)+ border border 去重 + 未知类检测进 arch:scan |
grep -c "text-heading-|mt-sm|py-xs|p-md" src/widgets = 0;pnpm lint:tokens 通过 |
| P1-7 |
codegen 恢复 typescript-operations(config + data-ana 两域先行关闭 skipDocumentsValidation) |
生成操作级类型;lib/api 对应域删除手写 interface;typecheck 通过 |
| P1-8 |
CI 增补:路由表一致性脚本 + 页面计数 + codegen diff 检查 |
CI 对预埋违规报红(附 pipeline 链接) |
P2 · 教师域页面(2–3 周,可与 P3 部分并行)
- 范围:§9.1 全表(~50 页)。顺序建议:exams → homework → grades → lesson-plans → questions/textbooks → attendance/classes/students → diagnostic/error-book/analytics → elective/course-plans → ai-* → practice/schedule-changes/leave。
- 每模块退出:列表/详情/表单页齐 + 三态 + MSW 场景 + 单测(数据变换函数)+ 页面计数更新。
- 契约工单:exams CRUD mutation、homework CRUD、grades 录入/列表、lessonPlans CRUD、attendance、classes 列表等,随模块开工即登记。
P3 · 学生域页面(1.5–2 周)
- 范围:§9.2 全表(~34 页)。先行:dashboard 详情(trend/weakness 已 ✅)→ error-book(✅)→ grades/exams/homework 只读 → exams/take 作答台 → practice → ai-tutor。
P4 · 家长域页面(1–1.5 周)
- 范围:§9.3 全表(~21 页)。多为只读视图,复用学生域组件;重点:
myChildren 契约 + child-overview 聚合。
P5 · 管理域页面(1.5–2 周)
- 范围:§9.4 全表(~22 页)。先行:users(❌ 列表契约优先推)→ roles/permissions → audit-logs → school 体系 → invitation-codes → plugins(✅)→ viewports(对齐 004 §5.4)。
- 收尾:旧 4 portal 目录归档(
apps/_archive/)或删除(决策点:待全量验收后执行,单独 PR)。
P6 · 硬化与验收(1–2 周)
- Playwright E2E:登录 → 各角色核心流 1 条(教师建考试/学生交作业/家长看成绩/管理员建用户)+ 三级错误边界 + 门禁越权。
- 视觉回归:5 布局 + 每域代表页截图基线。
- 性能:首屏 JS ≤ 300KB(gzip)、LCP < 2s(本地 Docker 压测,附报告)。
- 生产检查单:
APOLLO_REQUIRE_PQ_MANIFEST=true、APOLLO_ROUTER_INTROSPECTION=false、NEXT_PUBLIC_DEV_MODE=false、ROUTER_AUTH_SECRET env 化、CSP 头(next.config.js headers())、/api/log 换生产端点。
- 文档:README v3.0 重写(本文件内容沉淀回模块 README + 本文件标记"已并入 README")。
11. 后续 AI 工作规范(强制)
本章是给所有后续在本模块工作的 AI/人的操作规程。多 AI 协作同时遵守 project_rules §14(模块单一负责制:本模块只改 apps/portal-shell/,跨模块变更按 §14.4 顺序)。
11.1 开工前必读(按序)
- 本文件(v3.0 总纲)
project_rules.md §3.4/§3.10/§4/§14
- 所接任务的页面源(旧 portal 对应 page.tsx)与目标模板(§7.3)
11.2 目录与文件约定
- 页面文件瘦身:
page.tsx 只做 RSC 预取 + 组装,实现代码放 features/<domain>/;单文件 ≤ 300 行(超出即拆分)。
- features 域清单:exams / homework / grades / lesson-plans / questions / textbooks / attendance / classes / students / course-plans / elective / error-book / diagnostic / analytics / practice / schedule-changes / leave / ai / notifications / settings / users / rbac / audit / school / invitations / plugins / organization / files / system / viewports / children / messages / announcements。新增域先在本文件 §9 登记。
11.3 每页硬性清单(DoD)
11.4 契约工单流程(前端缺 schema 字段时)
- 在
docs/architecture/issues/contracts/<service>_contract.md 追加需求(字段名/类型/权限点/使用页面)。
- 本文件 §9 对应行"契约"列标注工单号。
- 页面用 MSW 先行;后端落地后:重跑 normalize + codegen → 该域关
skipDocumentsValidation → 切换 fetcher → 删 mock → 工单关闭。
- 禁止为绕过校验把查询写得与 schema 不符后开启 skip。
11.5 多 AI 并行分工建议
| AI 角色 |
负责 |
边界 |
| 框架 AI |
P0/P1(middleware/代理/AppFrame/模板/MSW/i18n) |
独占 src/{app/shell/layout.tsx,middleware.ts,shared/,lib/,mocks/} |
| 教师域 AI |
§9.1 |
独占 src/app/shell/teacher/** + src/features/{exams,homework,grades,lesson-plans,...} |
| 学生域 AI |
§9.2 |
独占 src/app/shell/student/** + 对应 features |
| 家长域 AI |
§9.3 |
独占 src/app/shell/parent/** + 对应 features |
| 管理域 AI |
§9.4 |
独占 src/app/shell/admin/** + 对应 features |
| 公共区(login/notifications/settings) |
框架 AI |
防止撞车 |
- 跨域共享组件必须先提"共享申请"(改
src/shared/ 或 packages/ui-components/),由框架 AI 评审合入;禁止各自复制粘贴。
- 分支:每域一个 feature 分支(
feat/portal-shell-<domain>),按 project_rules §14.3。
11.6 文档与验收纪律
- 每个 PR 更新:本文件 §9 对应行状态 +(如涉及)README 对应章节。
- 验收勾选必须附可复现命令输出(typecheck/lint/test/build/截图),禁止"已完成"裸声明——本文件 §1.1 的对照表即为反面教材。
- 每阶段结束运行
pnpm run arch:scan 更新 arch.db,并同步 004 相关章节。
11.7 禁止事项(红线)
- 禁止新增 widget 插件实现业务功能(冻结
src/widgets/ 新增;新功能 = 新页面)。
- 禁止恢复 localStorage 存 token;禁止 JS 读取
edu_session。
- 禁止在页面/widget 内联 gql 或绕过
lib/api 直接 useQuery。
- 禁止引入第二套设计令牌 / 第二套组件库 / 第二套 toast。
- 禁止"默认放行"式的权限代码(未匹配路由必须拒绝)。
- 禁止跳过
@contract-pending 登记直接写假查询。
- 禁止删除/弱化现有测试来让门禁变绿。
12. 风险登记册
| # |
风险 |
等级 |
缓解 |
| R1 |
后端契约补齐速度成为页面切换瓶颈(~100 页 ❌) |
高 |
MSW 先行解耦;契约工单周同步;优先推 mutations 框架(后端 0 mutation 是系统性缺口) |
| R2 |
140 页迁移量大,AI 并行产出不一致 |
高 |
模板四件套强制 + 共享组件申请制 + 每模块 DoD 清单 + CI 一致性脚本 |
| R3 |
旧页面纸感令牌清理遗漏导致视觉混杂 |
中 |
迁移 DoD 含令牌检查;arch:scan 未知类检测;P6 视觉回归 |
| R4 |
middleware 验签引入 JWKS 可用性依赖 |
中 |
JWKS 缓存(5min)+ 故障 fail-closed + 启动自检 |
| R5 |
next-intl 与 RSC 流式模式冲突 |
低 |
P1 先做技术验证(一个页面跑通再推广) |
| R6 |
MSW 数据与真实 schema 漂移 |
中 |
mock 数据类型从 codegen types 导入;契约切换时删 mock |
| R7 |
旧 portal 删除时发现有未迁移边角功能 |
低 |
P5 删除前做路由 diff 清单评审;先归档后删除 |
| R8 |
config-service 单点故障影响仪表盘 |
低 |
P0-4 内置默认配置已兜底;页面不依赖插件配置 |
13. 附录
13.1 命令手册
13.2 关键文件速查
| 主题 |
文件 |
| 微内核仪表盘 |
src/shell/{Shell,ClientShell,LayoutManager,SlotRenderer,Registry,PropsMerger,PluginLifecycle,PluginStore}.tsx? |
| 数据层 |
src/lib/{apollo-client,useWidgetQuery,useWidgetMutation,config-fetcher,usePluginConfig}.ts + src/lib/api/ |
| 安全 |
src/shared/lib/route-permissions.ts + packages/shared-ts/src/permission-bitmap.ts +(新)src/middleware.ts |
| 错误处理 |
src/shared/components/{plugin,route,section}-boundary.tsx + packages/hooks/src/use-error-report.ts |
| 设计令牌 |
packages/ui-tokens/src/{primitive,semantic-light,semantic-dark,tailwind-theme}.css |
| 组件库 |
src/shared/components/ui/* + packages/ui-components/src/* |
| 迁移源 |
apps/{teacher,student,parent,admin}-portal/src/app/**/page.tsx + apps/teacher-portal/src/mocks/handlers-p*.ts |
| 后端 schema 源 |
services/*/src/graphql/generated/schema.graphql → src/lib/api/__generated__/combined-schema.graphql |
13.3 参考文档
13.4 术语表
| 术语 |
定义 |
| AppFrame |
全站页面框架(TopBar + Sidebar + Main),src/app/shell/layout.tsx |
| 微内核仪表盘 |
/shell 角色首页的插件聚合区(v2.0 遗产,保留收缩) |
| 页面模板四件套 |
list / detail / form / workbench 四种页面骨架(§7.3) |
| 契约工单 |
前端缺 schema 字段时的后端需求登记单(§11.4) |
| MSW 兜底层 |
src/mocks/ 开发态模拟后端(§5.4) |
@contract-pending |
页面头注释,标记数据来自 MSW 待契约切换 |
| fail-closed |
无身份/无权限/服务不可用时一律拒绝并显式提示,禁止静默放行 |
| 视口四层 |
004 §5.4:导航 L1 / 路由 L2 / 组件 L3 / 数据 L4 |
本文件自发布之日起为 portal-shell 前端工作唯一权威。任何"已完成"声明必须能以 §1.1 同等严格度复现。下一阶段开工前,先完成 P0 并回填本文件 §9/§10 状态。