包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
84 KiB
已知问题速查
索引式速查手册:场景 → 技术/规则映射。不写代码示例,不写流水日志,不标注 AI 身份。 架构规则见 ../architecture/004_architecture_impact_map.md 与 ../../.trae/rules/project_rules.md AI 查阅时只读自己负责模块的分区(§二 对应小节),禁止读其他 AI 模块分区,避免被历史记录或他人方案误导。 经验沉淀不标注 AI 身份:所有记录只写"场景→技术/规则"映射,不得出现 AI 标识(如 ai01/ai02/coord 等),避免新同名 AI 将前任记录误认为当前事实。模块内经验沉淀在各自服务 README。
一、全局经验
1.1 多语言 monorepo 配置
| 场景 | 技术/规则 |
|---|---|
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
| 根 package.json scripts | 封装多语言命令入口:pnpm dev / pnpm lint / pnpm test / pnpm build |
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/、services/classes、bff/、apps/、scripts/),Go/Python 不入 |
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 tsx 直接运行,无需编译 |
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
| ESLint 9 flat config | P6 硬化:创建 eslint.config.js(flat config),lint 脚本去掉 --ext .ts,lint-staged 恢复 eslint --fix |
| next lint 交互式初始化 | teacher-portal 无 .eslintrc.json 时 next lint 触发 Strict/Base 选择提示,CI 中需预置配置或改 eslint src |
1.2 Docker Compose 基础设施
| 场景 | 技术/规则 |
|---|---|
| 日常开发启动 | 用 docker-compose.minimal.yml 仅起 MySQL+Redis |
| 全量启动内存不足 | 按 profiles 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) |
| 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g |
| 按阶段启用容器 | P1 仅 MySQL+Redis;P3 加 Kafka+Zookeeper;P4 加 Debezium+CH+Neo4j;P5 加 ES;P6 加 Consul+Istio |
| MySQL 初始化 | init-sql/01-init.sql 挂载到 /docker-entrypoint-initdb.d:ro |
| healthcheck | MySQL 用 mysqladmin ping,Redis 用 redis-cli ping |
| Windows 下卷挂载 | init-sql 用绝对路径或确保相对路径正确 |
| 容器名固定 | container_name: edu-mysql 便于服务连接配置 |
| Kafka 双 listener | INSIDE (kafka:29092) 容器间互访 + OUTSIDE (localhost:9092) 主机访问,避免 Debezium 拿到 localhost metadata 后切回连不上 |
| ClickHouse 远程访问 | 默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 clickhouse/users.d/custom-users.xml 覆盖密码+任意 IP |
| Debezium Connect 镜像源 | daocloud 禁用 debezium/*,用 quay.io/debezium/connect:2.7 替代 |
| Debezium 跨网络访问 MySQL | MySQL 容器在 edu-minimal_default 时,docker network connect edu-full_default edu-mysql 让 Debezium 同时可达 |
| CDC 注册 connector | POST :8083/connectors,配置 topic.prefix/database.include.list/schema.history.internal.kafka.topic |
1.3 protobuf + buf 契约
| 场景 | 技术/规则 |
|---|---|
| 契约唯一源 | packages/shared-proto/proto/*.proto,禁止 REST/gRPC 混用 |
| breaking change 检测 | buf breaking --against '.git#branch=main' CI 强制,必须升版本号 |
| proto 版本化 | 包名带版本 xxx.v1、xxx.v2,新旧版本共存 |
| buf lint | buf lint 使用 STANDARD 规则集,可按需 except(如 PACKAGE_VERSION_SUFFIX) |
| 三端代码生成 | buf.gen.yaml 配置 TS/Go/Python 插件,pnpm proto:gen 统一生成 |
| 契约先行硬约束 | 任何服务间通信必须先改 proto,CI 检测 breaking change 阻止违规合并 |
| 事件 schema 版本化 | Kafka 事件带 schema_version 字段,消费端按版本处理 |
| 错误码定义在 proto | 错误码三端共享,格式 {服务前缀}_{错误类型}_{资源}(如 IAM_PERM_USER) |
1.4 NestJS 服务开发(TS)
| 场景 | 技术/规则 |
|---|---|
| 服务标准结构 | src/{main.ts,app.module.ts,config,middleware,<domain>,shared,generated} 黄金模板复制 |
| 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 |
| 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 |
| ApplicationError 基类 | 统一错误体系:ValidationError(400)/NotFoundError(404)/PermissionDeniedError(403)/ConflictError(409) 等 |
| 响应信封 | { success: boolean, data?: T, error?: { code, message, details, traceId } } 统一跨服务 |
| 权限校验 | requirePermission(perm) 装饰器/中间件,admin 角色拥有全部权限 |
| DataScope 过滤 | 6 级(all/grade_managed/class_taught/class_members/children/owned),repository 下推 WHERE 条件 |
| 结构化日志 | pino + traceId/spanId/userId/service 字段,禁止 console.* |
| Metrics endpoint | prom-client 暴露 /metrics(QPS/延迟/错误率) |
| OpenTelemetry trace | OTel SDK 初始化,W3C TraceContext header 跨服务传播 |
| 健康检查 | /health endpoint 返回 { status, service, version } |
| 优雅关闭 | SIGTERM → app.close() → shutdownTracing() → process.exit(0) |
| i18n 错误码映射 | 错误码 → i18n key 映射表,后端返回 key + 参数,前端翻译 |
| Drizzle ORM | 参数化查询,禁止字符串拼接 SQL |
| Zod 输入校验 | controller 层 schema.safeParse(body),失败抛 ValidationError |
1.5 Go Gateway 开发
| 场景 | 技术/规则 |
|---|---|
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
| JWT claims | { user_id, roles[], registered_claims },校验后注入 x-user-id/x-user-roles 头转发 |
| 请求 ID 注入 | Gateway 生成或透传 X-Request-ID,全链路传递 |
| 健康检查 | GET /health 无需鉴权,返回 { status, timestamp } |
| 包结构 | internal/{config,middleware,proxy},P6 扩展 handler/service/repository |
| error 处理 | 显式处理,禁止 _ 忽略;gin.AbortWithStatusJSON 统一错误响应 |
| 配置加载 | 环境变量 + 默认值(getEnv(key, fallback)),P6 引入 viper |
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 |
1.6 可观测性(OTel + Prometheus + Loki)
| 场景 | 技术/规则 |
|---|---|
| P1 最小可观测集 | 每服务结构化日志 + /metrics + OTel SDK 初始化(不引入完整后端) |
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
| Prometheus 指标 | http_request_duration_seconds(Histogram)+ http_requests_total(Counter) |
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
| 日志参数顺序 | log.error({ err: error, userId, traceId }, "操作描述"),错误对象字段名用 err |
| P6 /metrics 端点 | NestJS 用 app.getHttpAdapter().get('/metrics', handler),不能用 app.get()(会被解析为 DI 容器 get) |
| P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager |
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) |
| P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 edu-* 前缀容器日志,避免无关日志 |
| P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 |
| P6 collectDefaultMetrics | prom-client 的 collectDefaultMetrics({ register }) 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 |
| P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 .inc()/.observe(),需后续补 HTTP 中间件自动埋点或业务埋点 |
| P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 @opentelemetry/auto-instrumentations。已补全:NestJS 6 服务用 getNodeAutoInstrumentations();Python 2 服务用 FastAPIInstrumentor.instrument_app(app);Go 2 服务用 otelgin.Middleware() |
| P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 docker.m.daocloud.io/ 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 |
| P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 |
| P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 --no-deps + 显式指定服务名,避免重建依赖容器 |
1.7 微前端 Module Federation
| 场景 | 技术/规则 |
|---|---|
| 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 |
| P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation |
| 视口驱动渲染 | 侧边栏由 viewports.L1 驱动渲染,路由由 viewports.L2 控制 |
| 共享翻译资源包 | next-intl 微前端共享,BFF/服务返回 i18n key 不返回翻译文本 |
| Module Federation 配置 | P2 起在 next.config.js 配置,P1 用单一 Next.js 应用 |
1.8 从旧项目迁移的通用经验
| 场景 | 技术/规则 |
|---|---|
| React 19 乐观更新 | useOptimistic 替代手动 isPending,配合 useTransition 自动管理回滚 |
| Zustand 细粒度选择器 | 单字段 selector 优于 useShallow 多字段包装 |
| Tiptap SSR | 必须 immediatelyRender: false 避免 hydration mismatch |
| 请求级去重 | React cache() 包装优于跨请求缓存(权限数据易变,不用 unstable_cache) |
| 批量 SQL | INSERT batch db.insert().values([...]) + UPDATE CASE WHEN 单次执行,禁止循环单条 |
| 动态导入重 UI 库 | xxx-inner.tsx + xxx.tsx lazy wrapper(tiptap / @xyflow/react / AI SDK) |
| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 |
| 递归删除 | 批量收集后代 ID + inArray 单次删除,禁止递归逐个 |
| 统计走 SQL 聚合 | COUNT(*)/SUM(CASE WHEN ...) 替代拉全表内存循环 |
| 高频子组件 React.memo | AssignmentCard / StatusBadge / EmptyState 等 |
| 虚拟化大表 | @tanstack/react-virtual |
| 破坏性操作独立权限 | AUDIT_LOG_PURGE ≠ AUDIT_LOG_READ,沿用旧项目规则 |
| i18n key 命名 | 禁止包含 .,动态 key 必须包含完整嵌套路径(t(\type.${type}`)`) |
| i18n 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key |
| 类型守卫优先 | JSON.parse 结果经 unknown 中转 + 守卫;禁止 as 断言(除非从 unknown 转换) |
| 函数返回值显式标注 | 特别是 Promise<T> |
| 仅类型导入 | import type |
1.9 架构工具与验证命令
| 场景 | 命令/规则 |
|---|---|
| arch.db 扫描 | pnpm run arch:scan(多语言:TS+Go+Python+Proto) |
| arch.db 查询 | pnpm run arch:query -- <command> |
| 查服务依赖 | pnpm run arch:query -- deps <module> |
| 查 proto 契约 | pnpm run arch:query -- sql "SELECT * FROM contracts" |
| 查 Kafka 事件 | pnpm run arch:query -- sql "SELECT * FROM events" |
| 查架构违规 | pnpm run arch:query -- violations(骨架,P1 后期补全) |
| proto lint | cd packages/shared-proto && pnpm exec buf lint |
| proto breaking 检测 | pnpm exec buf breaking --against '.git#branch=main' |
| proto 代码生成 | pnpm proto:gen |
| Go 编译验证 | cd services/api-gateway && go build ./... && go vet ./... |
| TS 类型检查 | pnpm -r exec tsc --noEmit |
| 全量 lint | pnpm lint |
| 全量测试 | pnpm test |
| 启动最小基础设施 | docker compose -f infra/docker-compose.minimal.yml up -d |
1.10 coord 交叉审查仲裁审查(跨模块)
AI 在执行 coord 仲裁整改清单前,必须以 ai-allocation.md §3.2 为唯一源交叉验证责任方,并检查裁决内部自洽性。
| 场景 | 技术/规则 |
|---|---|
| 仲裁责任方错配(content→ai05) | content 模块由 ai09 负责(ai-allocation §3.2 L80),coord 整改清单若标 ai05 则为错配;AI 必须以 ai-allocation §3.2 为准,提请 coord 修正而非自行执行 |
| 仲裁责任方错配(msg→ai05) | msg 模块由 ai10 负责(ai-allocation §3.2 L81),coord 整改清单若标 ai05 则为错配;同上 |
| 仲裁责任方错配(iam→ai02) | iam 模块由 ai06 负责(ai-allocation §3.2 L77),ai02=push-gateway;coord 整改清单若标 ai02 则为错配;同上 |
| 仲裁责任方错配(push-gateway→ai01) | push-gateway 由 ai02 负责(ai-allocation §3.2 L73),ai01=api-gateway;coord §4.2/§5.6/整改第 21 项若标 ai01 则为错配;同上 |
| 仲裁责任方错配(teacher-portal→ai07) | teacher-portal 由 ai13 负责(ai-allocation §3.2 L84),ai07=classes(黄金模板);coord §2.4/§4.1/§5.5/整改第 2 项若标 ai07 修正 teacher-portal 则为错配;同上 |
| 仲裁责任方错配(parent-portal→ai07) | parent-portal 由 ai15 负责(ai-allocation §3.2 L86),ai07=classes;coord §2.4 第 4 项若标 ai07 修正 parent-portal 端点则为错配;同上 |
| 仲裁责任方错配(core-edu→ai03) | core-edu 由 ai08 负责(ai-allocation §3.2 L79),ai03=teacher-bff;coord §4.2/整改第 14 项/整改第 23 项若标 ai03 则为错配;同上 |
| 仲裁责任方错配(parent-bff→ai07) | parent-bff 由 ai05 负责(ai-allocation §3.2 L76),ai07=classes;coord §5.2 "ai03/ai04/ai07" 若指 BFF 前缀迁移则 ai07 应为 ai05;同上 |
| 仲裁责任方错配系统性(5 组认知错位) | coord 存在 5 组系统性 AI 编号映射错位(ai07→teacher-portal/ai05→content-msg/ai02→iam/ai01→push-gateway/ai03→core-edu),疑似沿用之前 session 错误认知;整改前必须以 ai-allocation §3.2 全量交叉验证 |
| 仲裁裁决内部冲突(topic 保留 vs 废弃) | coord-cross-review.md §3.5 保留 edu.notification.events,但 parent-bff 02 §0.1 C5 废弃改为 edu.notification.sent/read/recalled/failed;coord 自身前后不一致时,以正式审查报告(coord-cross-review.md)为准或提请 coord 统一 |
| 仲裁裁决内部冲突(GraphQL 引入时机) | coord-cross-review.md §2.5 裁决"P2-P3 用 REST",但 parent-bff 02 §0.1 U3 用户仲裁"GraphQL P2 立即引入";coord §2.5 与用户 U3 仲裁直接冲突,用户仲裁优先级高,应以 U3 为准,提请 coord 修正 §2.5 |
| 仲裁现状误判(project_rules §5) | coord §2.1 裁决"修订 project_rules §5 包名规范为 next_edu_cloud.*",但 project_rules §5 L214 已是目标态;整改前应先验证现状,避免多余整改 |
| 仲裁审查三维度 | ① 责任方是否匹配 ai-allocation §3.2 ② 裁决是否自洽(无内部冲突)③ 现状是否误判;三维度全过方可执行 |
| 仲裁问题处理流程 | 发现问题记录到 known-issues 工作日志 + 提请 coord 修正,禁止自行修改仲裁文档或按错误责任分配执行 |
| coord 代笔文档仲裁 vs 正式审查报告 | coord 代笔文档(如 parent-bff 02)中的仲裁决策(如 C5)可能与 coord 正式审查报告(coord-cross-review.md)不一致;以正式审查报告为准,代笔文档中的仲裁需 coord 在正式报告中确认 |
| 仲裁审查覆盖面要求 | 首次审查覆盖面不足(仅查 3 组错配遗漏 4 组),二次审查应严格逐条核对全部 24 项整改项,禁止抽样核对;系统性错配需按 ai-allocation §3.2 全量交叉验证 |
| 用户仲裁 vs coord 仲裁优先级 | 用户仲裁(U1-U4)优先级高于 coord 仲裁;coord 裁决与用户仲裁冲突时(如 §2.5 GraphQL vs U3),以用户仲裁为准,提请 coord 修正其裁决以保持一致 |
| 整改清单整体可执行性判断 | 若整改清单责任方错配占比超阈值(本次 71%,17/24 项),则整改清单整体不可直接执行,必须等 coord 重新发布修正版整改清单后方可推进 |
二、模块经验
2.1 api-gateway(Go)
| 场景 | 技术/规则 |
|---|---|
| P1 鉴权 | Gateway 内置 HS256 JWT,jwt.ParseWithClaims + SigningMethodHMAC 校验 |
| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM |
| 路由转发 | gin.Group("/api/v1") + httputil.NewSingleHostReverseProxy |
| 路径重写 | 去掉 /api/v1 前缀后转发到下游服务 |
| 用户上下文注入 | c.Request.Header.Set("x-user-id", claims.UserID) 传递给下游 |
| 请求 ID | Gateway 生成或透传 X-Request-ID,c.Set("request_id", ...) + c.Header(...) |
| P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) |
| 健康检查 | GET /health 无需鉴权 |
| 尾斜杠重定向循环 | r.RedirectTrailingSlash=false + 同时注册 Any("/classes") 与 Any("/classes/*path") |
| 开发模式鉴权旁路 | DEV_MODE=true 时接受 Bearer dev-token,注入固定身份;生产必须 false |
| 路由注册位置 | 真实路由在 main.go 的 api.Group 内注册,internal/routing/routing.go 若未被 main 引用即为死代码 |
| 多服务路由扩展 | 新增服务代理时在 main.go 注册两组路由:Any("/x") + Any("/x/*path"),与 classes 一致 |
| DEV_MODE 环境变量 | Go 不自动加载 .env,DEV_MODE 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 |
2.2 classes(TS/NestJS,P1 黄金模板)
| 场景 | 技术/规则 |
|---|---|
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
| 黄金模板复制流程 | cp -r services/classes services/xxx → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
| 错误处理 | ApplicationError 基类 + 子类,错误码 CLASSES_* 前缀 |
| 可观测 | pino logger + prom-client metrics + OTel tracer |
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
| 配置 | 三层配置 + Zod 校验 env |
| i18n | ERROR_CODES 映射表(错误码 → i18n key) |
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
| Drizzle schema | mysqlTable + varchar/timestamp + index |
| ID 生成 | @paralleldrive/cuid2 的 createId() |
| 响应转换 | repository 返回 Date,service 转换为 createdAt: number(时间戳) |
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
| 健康检查依赖 | readyz 用 Drizzle getDb().execute(sql\SELECT 1`)` 校验,不要依赖 typeorm DataSource DI |
| AppModule 注册 | HealthModule 必须在 app.module.ts imports 数组显式声明,否则 NestFactory 不扫描 HealthController |
| 增量编译陷阱 | tsconfig.json 显式 "incremental": false 覆盖 base,避免 .tsbuildinfo 导致 nest watch 不 emit |
2.3 iam(TS/NestJS,P2)
| 场景 | 技术/规则 |
|---|---|
| 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 |
| RBAC | 角色/权限/角色-权限 CRUD + getEffectivePermissions(userId) API |
| 视口配置 | 4 层模型(导航/路由/组件/数据),role_viewports 表 |
| DataScope 解析 | 6 级数据范围,注入 JWT payload |
| JWT payload | { userId, roles, permissions(bitmap), dataScope, exp } |
| Token TTL | access 15min / refresh 7day,refresh 用 Redis 黑名单失效 |
| 权限缓存 | getEffectivePermissions 结果 Redis 缓存 TTL 5 分钟,角色变更主动失效 |
| schema 表 | users / roles / permissions / role_permissions / role_viewports / parent_student_relations / class_subject_teachers |
| ESM 模式 DI | providers: [IamService, IamRepository] + 构造器 @Inject(IamRepository) 显式注入,避免 undefined 运行时错误 |
| Drizzle ORM API | inArray(col, vals) 替代不存在的 .in();select 返回字段名按 schema 定义(如 r.iam_roles 而非 r.roles) |
| 健康检查依赖 | readyz 用 db.execute(sql\SELECT 1`)` 校验连接,不要依赖 typeorm DataSource(IAM 用 Drizzle,无 typeorm) |
| Gateway 身份传递 | Controller 直接读 req.headers['x-user-id'] / x-user-roles,不要依赖未注册的 AuthMiddleware 的 AuthenticatedRequest |
| DEV_MODE 登录 | DEV_MODE=true 时 Gateway 接受 Bearer dev-token 注入固定身份,IAM 仍支持真实 JWT(HS256,P2 应改 RS256) |
| P2 公开路径白名单 | Gateway publicPaths map 含 /iam/register//iam/login//iam/refresh,AuthMiddleware 跳过鉴权避免死锁 |
| P2 视口过滤 | getUserViewports 按 requiredPermission 过滤 + sortOrder 字典序排序,无权限要求的视口全员可见 |
| P2 JWT payload | HS256 签名含 sub/email/roles/dataScope/type,register 自动分配 teacher 角色(TEACHER_ROLE_ID 固定 UUID) |
2.4 core-edu(TS/NestJS,P3)
| 场景 | 技术/规则 |
|---|---|
| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 |
| Outbox 模式 | 业务事务同写 outbox_events 表,后台 relay worker 投递 Kafka |
| Outbox relay | Go 写独立服务 services/outbox-relay/,轻量高吞吐 |
| Kafka topic | edu.teaching.exam.created / edu.teaching.homework.graded / edu.teaching.grade.recorded(ISSUE-002/004 仲裁统一前缀) |
| 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 |
| 批改后联动 | 批改完成 → 发 homework.graded 事件 → 下游消费(DataAna/Msg) |
| Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) |
| schema 表 | exams / exam_questions / exam_submissions / homework / homework_submissions / homework_answers / grades / grade_formulas / attendance / courses / lessons / schedules / classes / teacher_associations / outbox |
| datetime 列 | Drizzle datetime 列需 Date 对象,HTTP 请求体里是 ISO 字符串,service 层必须 new Date(input) 转换否则报 toISOString is not a function |
| Kafka 非阻塞 | 开发环境 Kafka 未启动时 connectKafka() 必须 try/catch 且 main 中用 void 调用,否则阻塞服务启动 |
| 相对 import | src/<domain>/ 下文件回溯一级用 ../,不要用 ../../(NestJS ESM 模块) |
| 身份头读取 | Controller 用 @Req() req 从 req.headers['x-user-id'] 读 createdBy/gradedBy,不依赖未注册的 AuthMiddleware |
| 健康检查 | health.controller 用 Drizzle db.execute(sql\SELECT 1`)`,不用 typeorm DataSource |
| Outbox 验证 | 业务事务同写 core_edu_outbox 表,Kafka 未启动时事件 status=failed,启动后会重试 |
| Drizzle 索引语法 | drizzle-orm 0.31+ 的 mysqlTable 第三参数回调必须返回对象({ idx: index(...).on(...) }),不能返回数组 |
| Drizzle datetime defaultNow | datetime 列类型不支持 defaultNow(),需改用 timestamp 列类型或在 service 层显式传入 new Date() |
| KafkaJS 连接状态 | KafkaJS Producer 类型无 isConnected() 方法,需监听 producer.connect/producer.disconnect 事件自行维护布尔标志 |
| 状态机类型转换 | DB 查询返回的 status 字段为 string,传入状态机函数前需 as ExamStatus 转换(drizzle 不支持 enum 字面量类型推导) |
| Controller createdBy | Controller 构造 CreateExamInput/AssignHomeworkInput 时需显式从 req.userId 赋值 createdBy 字段,Zod schema 不含此字段 |
| Redis 分布式锁 | 作业提交幂等用 Redis SET NX EX,Redis 不可用时降级到 DB 唯一索引兜底 |
| 课表冲突检测 | findOverlappingSchedules 查 teacher + class 两个维度的时间重叠,ISSUE-006 #2 裁决 room_id 仅 P3 预留不校验冲突 |
| 成绩公式优先级 | scope 优先级 class(3) > subject(2) > school(1),ISSUE-006 #3 裁决;gradeFormulas 表按 effectiveFrom/effectiveTo 时间窗口匹配 |
| 作业宽限期 | 默认 grace_period=300 秒(ISSUE-006 #4 裁决),提交时 dueDate + gracePeriod 判断是否迟到 |
| 考试归档 | ISSUE-006 #7 裁决 archived 仅软删除(设 status + archivedAt),不物理删除 |
| 事件 payload | buildEvent 统一封装 event_id/occurred_at/metadata.schema_version,ISSUE-006 #6 裁决 exam.submitted 事件只带 submission_id+studentId |
| IAM 事件消费 | iam-consumer 订阅 edu.iam.teacher.assigned/edu.iam.class.created,幂等 upsert 到 teacher_associations/classes 表 |
2.5 content(TS/NestJS,P4)
| 场景 | 技术/规则 |
|---|---|
| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) |
| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 |
| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 |
| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j |
| API 字段名 | 请求体用 TS schema 字段名(如 order),非 DB 列名(如 order_num) |
| Neo4j 不可用 | 未设置 NEO4J_URL 时 driver=null,getNeo4jSession 返回 null,业务正常落库 MySQL |
| Neo4j 连接超时 | driver 配置 connectionTimeout:3000,避免 Neo4j 不可用时拖慢 HTTP 响应 |
| Drizzle int | drizzle-orm/mysql-core 导出 int() 不是 integer() |
| db 常量导出 | database.ts 导出 db 常量替代 getDb() 函数,与 core-edu/classes 黄金模板对齐 |
2.6 data-ana(Python/FastAPI,P4)
| 场景 | 技术/规则 |
|---|---|
| 学情诊断 | ClickHouse 宽表查询,5s 内返回 |
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka(edu-cdc.next_edu_cloud.<table>)→ DataAna 消费写 ClickHouse |
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
| CDC 消费者实现 | cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 启动 asyncio.create_task 后台运行 |
| ClickHouse 写入 | clickhouse_client.upsert_student_dashboard() 用 client.insert() 写宽表,client 为 None 时降级返回 False |
| Debezium 事件解析 | before/after/source/op/ts_ms 五字段,op=r(快照)/c(新增)/u(更新)/d(删除) |
| 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件),grades 事件触发时查缓存填充宽表 class_id |
| Consumer offset 重置 | kafka-consumer-groups --reset-offsets --to-earliest --execute 需先停消费者让 group 处于 Empty 状态 |
| 5 张宽表对齐 | student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log(ai-allocation §5 强制) |
| ActionState 信封对齐 | Python 服务 {success, data, degraded} 必须改 {success, data, error:{code, message, details?, traceId?}},degraded 作 details 子字段(004 §11.5 P0) |
| gRPC server 启用 | P4 启用 data-ana gRPC server 端口 50055(004 §4.2 矩阵),HTTP 3006 保留作 Gateway 直连降级 |
| DataScope 6 级过滤 | Repository 层注入 WHERE(SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL),iam GetEffectiveDataScope gRPC P4 补全(004 §15.3 #5) |
| Outbox 豁免 | 派生数据事件(MasteryUpdated / WarningTriggered / AIUsageRecorded)豁免 Outbox,允许直接 Kafka producer(004 §12.2 + §15.3 #6) |
| 掌握度算法 | WEIGHTED_MOVING_AVG(w_i=0.6^i)+ FORGETTING_CURVE(半衰期 30 天)取 max,近 5 次不足降级 SIMPLE_AVG |
| 预警阈值设计 | LOW_MASTERY(<0.4) / CRITICAL_LOW(<0.2) / SCORE_DROP(下降≥20%) / ABSENT_FREQUENT(≥3次/周) / TREND_DECLINE(连续3次降) |
| 预警去重 | 同一 (target_id, warning_type) 24h 内只触发一次,Redis 位图 data_ana:warning:{target_id}:{type}:{date} TTL 25h |
| CDC 水平扩展 | P4 单实例 + 内存 ExamCache LRU → P6 多实例 + Redis ExamCache + partition 扩容 + HPA |
| CDC 手动 commit | v2 改进:enable_auto_commit=False 手动 commit(at-least-once),commit 前确保 ClickHouse 写入成功 |
| ClickHouse 容量规划 | 5 宽表年增量 ~2.3GB(5000 学生×20 次/月),单节点可承载;P6 按 class_id hash 分片 + ReplicatedMergeTree |
| ClickHouse FINAL | ReplacingMergeTree 查询必须加 FINAL 或用 argMax 聚合,否则读到重复版本(当前实现未加,P0 整改) |
| 缓存键命名 | data_ana:datascope:{user_id} 5min / data_ana:exam:{exam_id} 30day / data_ana:dedup:{event_id} 7day |
| 4 端 Dashboard | teacher/student/parent/admin 各自 Dashboard API + 权限点 ANALYTICS_*_DASHBOARD(v2 新增 4 端权限) |
| SubscribeMasteryUpdate | gRPC server-streaming RPC 为 P5+ AI 个性化推荐预留实时推送通道(analytics.proto v2 提案) |
| GDPR 数据删除 | ClickHouse 无 UPDATE/DELETE,用 ALTER TABLE ... DELETE WHERE student_id=? 异步 mutation(P6 实现) |
| 字段脱敏 | student_id 在日志中 hash 化(structlog processor),PII 不入日志 |
| 事件版本化 | 事件 payload 加 schema_version: "v1" 字段,破坏性变更发 v2,consumer 双消费 6 个月过渡 |
| API 版本化 | HTTP /analytics/v1/* + gRPC next_edu_cloud.analytics.v1,破坏性变更新增 v2,旧版保留 6 个月 |
| Projection 演进 | P4 CDC 直连 → P4 后期双消费(CDC + 领域事件)→ P6 ClickHouse MaterializedView 自动聚合 |
| 测试策略 | pytest + Testcontainers(ClickHouse/Kafka 容器化)+ 单元 ≥80% + 集成 ≥60% + 契约 pact-python + E2E |
| Dockerfile 多阶段 | builder(uv sync --frozen)+ runtime(python:3.12-slim < 200MB)+ HEALTHCHECK 集成 |
| 配置项完整清单 | pydantic-settings 30+ 配置项,env_prefix="DATA_ANA_",含 ClickHouse/Kafka/iam gRPC/Redis/OTel/算法参数/预警阈值 |
| 降级模式 4 场景 | ClickHouse/Kafka/Redis/iam gRPC 不可用时返回骨架 + error.details.degraded=true,4 错误码 DATA_ANA_*_UNAVAILABLE |
| Python gRPC 实现 | grpc.aio + betterproto + ServerInterceptor 透传 W3C trace context,与 HTTP 共享同一 Application Service |
| structlog API | 24.x 用 make_filtering_bound_logger(level),旧版 make_filtering_logger 已废弃 |
2.7 messaging(TS/NestJS,P5)
| 场景 | 技术/规则 |
|---|---|
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
| 通知批量化 | createBatch(items) 单次 INSERT,沿用旧项目 dispatcher 模式 |
| 多渠道策略 | ChannelStrategy 接口 + InApp/Email/Sms/Push 实现,新增渠道不改核心(ai10 设计) |
| fan-out 分页 | listByUserWithPagination(userId, page, pageSize) 分页查询 |
| ES 降级(单向) | ES_URL 未设置时 esClient=null,safeIndex/safeSearch 跳过返回空结果 |
| DB→ES 降级读 | DB 故障时 fallback 到 ES 读模型,响应头标 X-Read-Model: es-degraded(ai10 设计) |
| ES→DB 降级检索 | ES 故障时 search() fallback 到 DB LIKE,降级计数打 metrics(ai10 设计) |
| Push 推送降级 | PUSH_GATEWAY_URL 未设置或连接失败时 try/catch 跳过,不影响 DB 写入;PushChannel 失败降级 in_app |
| Kafka 消费幂等 | event_id → Redis SETNX msg:idem:{event_id} TTL 7d;Redis 不可用降级 msg_idempotency 表唯一键(ai10 设计) |
| 三层幂等防线 | L1 Redis SETNX / L2 msg_idempotency 表 / L3 notifications.source_event_id 唯一索引(ai10 设计) |
| 已读/未读位图 | Redis Bitmap msg:unread:{userId},BITCOUNT 计数;Redis 不可用降级 DB COUNT(ai10 设计) |
| Outbox 强制(待 ai10 确认) | 004 §7.2 列 msg 生产 edu.notification.events,按 004 §12.2 强制 Outbox,P5 必须实现 msg_outbox_events 表 + Publisher worker |
| 12 类事件消费清单(待 ai10 确认) | 消费 iam(user.created/updated/deleted/role_changed + role.created/updated)+ core-edu(exam.published/assignment.submitted/assignment.graded/grade.recorded/attendance.recorded)+ data-ana(mastery.updated)共 12 类事件触发通知 |
| 4 类发布事件(待 ai10 确认) | 发布 NotificationSent / NotificationRead / NotificationRecalled / NotificationFailed 4 类事件,消费者 push-gateway + data-ana |
| 通知状态机(待 ai10 确认) | pending → sent → delivered → read / failed / recalled,含 6 状态转换 + 撤回场景预留 |
| NotificationTemplate 模板渲染(待 ai10 确认) | titleTemplate/contentTemplate 含 {{variable}} 占位符,RenderTemplate RPC 根据 variables 替换渲染,支持 locale 多语言 |
| NotificationPreference 用户偏好(待 ai10 确认) | 渠道开关 channels[] + 频率限制 frequency_limit + 静默时段 quiet_hours,5 维度配置矩阵 |
| Push Gateway 调用方向澄清(待 ai10 确认) | 004 §4.1 表述"PushGW→Msg 推送通道建立"歧义,实际方向是"msg → push-gateway (gRPC PushService.Push)",建议 coord 修正 004 §4.1 表述 |
| 频率限流(待 ai10 确认) | Redis 计数 msg:ratelimit:{userId}:{type}:{yyyyMMddHH} TTL 1h,按 NotificationPreference.frequency_limit 限制;Redis 不可用降级跳过限流 |
| 优雅关闭顺序(待 ai10 确认) | HTTP server → Kafka consumer → Outbox publisher → ChannelDispatcher → Redis → ES → DB pool → Tracer,8 步序列保证在途消息处理完成 |
| events.proto message 缺失阻塞(待 ai10 确认) | events.proto 缺 UserEvent/RoleEvent/NotificationEvent/MasteryEvent 4 类 message,msg 消费 iam/data-ana 事件阻塞,提请 coord 补齐 proto |
| 死信队列 | 消费失败超 3 次投递 edu.notification.dlq,告警人工介入(ai10 设计) |
| Outbox 事件发布 | 事务内写 msg_outbox + 业务表,独立 publisher 投递 edu.notification.events(参照 core-edu) |
| 模板渲染 | TemplateService + {{var}} 替换 + i18n locale;渲染失败降级默认文案记 MSG_TEMPLATE_RENDER_ERROR |
| 静默时段 | QuietHours 过滤非紧急通知,URGENT 优先级绕过静默(ai10 设计) |
| 短信限流配额 | Redis 滑动窗口限流 + 配额计数,超限返 MSG_RATE_LIMITED/MSG_QUOTA_EXCEEDED(ai10 设计) |
| 优雅关闭 | 统一到 LifecycleService,顺序 HTTP→Kafka→Outbox→ES→Redis→DB→Tracer(ai10 修订) |
| db 常量导出 | database.ts 导出 db 常量替代 getDb() 函数 |
| db 常量导出 | database.ts 导出 db 常量替代 getDb() 函数 |
2.8 push-gateway(Go,P5)
| 场景 | 技术/规则 |
|---|---|
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需调 /internal/push |
| 跨实例同步 | Redis PubSub(RedisURL 配置,预留 P6 实现) |
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
| 并发写修复 | send chan + 单写协程模式,避免 gorilla/websocket 并发写竞争 |
| DEV_MODE 鉴权 | DEV_MODE=true 时接受 dev-token,生产环境必须 JWT 校验 |
| 广播端点 | POST /internal/broadcast,body {event, data},调用 hub.Broadcast |
2.9 ai-gateway(Python/FastAPI,P5)
| 场景 | 技术/规则 |
|---|---|
| LLM Provider 适配 | OpenAI 兼容 REST API(httpx 异步),不引入 openai SDK |
| 降级模式 | API key 为空或调用失败时返回骨架响应,标记 degraded: true |
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
| 路由前缀 | 业务路由加 /ai 前缀(APIRouter prefix="/ai"),Gateway 代理 /ai |
| dev_mode tracer | dev_mode=true 时跳过 OTel exporter 初始化 |
2.10 shared-proto(契约包)
| 场景 | 技术/规则 |
|---|---|
| 目录结构 | proto/*.proto + buf.yaml + buf.gen.yaml |
| P1 契约 | 仅 classes.proto,iam.proto/core_edu.proto 占位 |
| 代码生成输出 | TS → shared-ts/generated,Go → shared-go/,Python → services/*/generated/ |
2.11 arch-scan(多语言扫描器)
| 场景 | 技术/规则 |
|---|---|
| TS 扫描 | regex 提取(function/class/interface/UPPER_CASE const),避免 ts-morph 对未安装依赖文件解析失败 |
| Go 扫描 | 正则提取(行首锚定 ^func/^type),P2 起替换为 tree-sitter-go AST |
| Python 扫描 | 正则提取(行首锚定 ^def/^class),P4 起替换为 tree-sitter-python AST |
| arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 |
| 全量扫描 | 先清空旧数据再扫描,避免残留 |
| 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 |
| 扫描后验证 | npm run arch:query -- violations 输出 0 为合格 |
| Windows 路径 | 自定义 ESLint 规则加载用 path.join,不用 path.posix.join |
2.12 teacher-portal(微前端宿主,P1 测试页)
| 场景 | 技术/规则 |
|---|---|
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
| API 调用 | fetch(${API_BASE}/api/v1/classes) + Authorization: Bearer ${TEST_JWT} |
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 |
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
| P2 路由组 + AppShell | app/(app)/layout.tsx 用 AppShell 包裹受保护页;/login 与 / 不套壳 |
| P2 真实 JWT | 登录后 token 存 localStorage,authHeaders() 读 Bearer ${getToken()} |
| P2 视口驱动侧边栏 | AppShell fetch /teacher/viewports 渲染左侧导航,active 路由高亮 |
| P2 根路径重定向 | app/page.tsx 客户端组件 router.replace(isAuthenticated() ? '/dashboard' : '/login') |
| fetch headers 类型 | authHeaders(): Record<string, string> 显式标注,避免 {} 与 HeadersInit 不兼容 |
| P2 MF Shell+Remote 架构 | teacher-portal:4000 为 Shell,student:4001/parent:4002/admin:4003 为 Remote(004 §1.2 + coord final §F1);MF 暴露 AppShell 整体(coord final §F10),各 Remote 自行决定内部布局 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState {success,data,error:{code,message,details?,traceId}} 解析 + 错误码前缀路由 i18n + trace_id 透传;P2 起对接 GraphQL(urql/apollo,coord final §F9/B1) |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState {success,data,error} 解析 + 错误码前缀路由 i18n,替代页面级 authHeaders()+fetch 重复 |
| P2 设计令牌三层模型 | primitive(原始色板)→ semantic-light/dark(语义)→ tailwind-theme(@theme inline 暴露 bg-*);ESLint no-restricted-syntax 禁 #hex + design-tokens/no-hardcoded-fonts 禁字面量字体(§3.10) |
| P2 4 端错误码前缀对齐 | BFF_TEACHER_=teacher-bff / BFF_STUDENT_=student-bff / BFF_PARENT_=parent-bff(coord final §B5,统一 BFF_XXX_ 风格);admin-bff 待 P6 定;前端按前缀路由 i18n key |
| P2 4 端错误码前缀对齐 | TP_=teacher-bff / SP_=student-bff / PP_=parent-bff / AP_=admin-bff,前端按前缀路由 i18n key |
| P2 i18n key 命名 | 统一 error.<service>.<code_snake>(如 error.iam.invalid_credentials、error.bffTeacher.validation_error,coord final §F4);翻译文件按域分 namespace |
| P2 权限点命名风格 | 统一 <RESOURCE>_<ACTION>(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ),数据范围用后缀 _OWN/_CHILD(如 GRADE_READ_CHILD,coord final §F7) |
| P2 Token 存储 | P2 用 localStorage(accessToken + refreshToken,coord final §F12);httpOnly Cookie + CSRF Token P6 单独评估(非分阶段迁移,P2 直接采用最终方案的当前阶段形态) |
| P2 packages 归属 | ai13 维护 packages/ui-tokens、ui-components、hooks(coord final §F8);coord 仅维护 shared-ts、contracts;ai13 需建立这 3 个共享包 |
| P2 GraphQL 客户端 | P2 起前端用 urql 或 apollo(coord final §F9),对接 teacher-bff GraphQL Yoga + DataLoader;废弃 REST fetch 模式;ApiClient 适配 GraphQL 操作类型(query/mutation/subscription) |
| P2 视口端点对齐 | GET /iam/permissions/effective(非 /iam/effective-permissions,coord final §F6/I8)+ GET /iam/viewports + GET /iam/roles;parent 子女切换 POST /parent/children/:childId/select |
2.13 parent-portal(家长端微前端 Remote,P4)
parent-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 parent-portal 特有的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
|---|---|
| P4 端口分配 | parent-portal = 4002(004 §1.2 强制 4 端 4000-4003;ai07 初稿误用 3002 与 iam 冲突,ai15 修订) |
| P4 MF Remote 角色 | next.config.js 配 @module-federation/nextjs-mf 暴露 ./ChildSwitcher 等家长端特有组件,复用 Shell 提供的 AppShell/usePermission/ApiClient/RequirePermission |
| P4 DataScope CHILDREN | 家长仅能查看自己绑定子女的数据(004 §5.4 L0 SELF 变体),前端通过 ApiClient 透传 userId + childId,下游服务按 DataScope 过滤 |
| P4 多子女切换 ChildSwitcher | Zustand slice(selectedChildId)+ 切换后 queryClient.invalidateQueries({ queryKey: ['child', prevId] });多子女 Tab(≤3)或下拉(≥4);空态/单子女/多子女三态分支 |
| P4 多子女数量边界 | 0 子女(EmptyChildState 引导绑定)→ 1 子女(隐藏切换器)→ 2-3(Tab Bar)→ 4-10(下拉)→ >10(搜索 + 列表);超出 10 触发性能预警 |
| P4 通知偏好三维矩阵 | Record<childId, Record<eventType, Record<channel, boolean>>>;react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 |
| P4 跨标签同步 | BroadcastChannel API(parent-portal-sync)+ localStorage 持久化 + LWW(Last-Write-Wins)冲突解决;同步 selectedChildId / 通知偏好 / 已读状态 |
| P4 隐私合规(COPPA/FERPA/PIPL) | 未成年人数据最小化采集;通知偏好默认 opt-in;数据保留 7 年后自动归档;前端不缓存敏感字段到 localStorage(仅 childId + 已读位图) |
| P4 i18n 5 语言含 RTL | next-intl 支持 zh-CN / en-US / zh-TW / ja-JP / ar-SA;ar-SA 用 dir="rtl" + Tailwind rtl: 变体;翻译文件按域分 namespace(parent/grades/notifications/preferences) |
| P4 PWA + Service Worker | manifest.json(name/short_name/icons/theme_color)+ Service Worker 缓存策略(App Shell precache + API stale-while-revalidate + 离线兜底页);P5+ 支持离线查看子女成绩缓存 |
| P4 API 契约版本管理 | 主版本 URL /api/v1/* → /api/v2/* + 子版本响应头 X-API-Version + Deprecation/Sunset 头埋点 + 低带宽场景 X-Fields 字段裁剪(GraphQL 风格,BFF REST 透传) |
| P4 性能预算 | Bundle ≤ 80KB JS / 20KB CSS / 200KB 总;recharts 懒加载;MF shared 单例避免 React 重复打包;dynamic import 非首屏组件;预加载子女数据 |
| P4 监控与降级 | 10 项前端指标(MF 加载失败率/ApiClient 错误率/Web Vitals/切换延迟等)+ 9 项降级策略(MF 失败回退独立页/BFF 5xx 降级缓存/WS 重试指数退避等) |
| P4 模块演化与解耦 | 拆分触发条件(Bundle > 120KB / 团队 > 3 人 / 独立部署需求);拆分后 MF 配置从 Shell 暴露;状态管理迁移路径(Zustand → Server Component + Server Actions);MF 升级路径(MF 2.0 → 3.0) |
| P4 iam 家长-学生关联 P0 阻塞 | iam 缺 GetChildrenByParent proto RPC + GET /iam/children REST + iam_student_guardians 表;ai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试 |
| P4 BFF 错误码前缀 | BFF_PARENT_=parent-bff(coord final §B5,统一 BFF_XXX_ 风格);前端按 BFF_PARENT_* 前缀路由 i18n key,与 BFF_TEACHER_/BFF_STUDENT_ 对齐 |
2.14 parent-bff(TS/NestJS,P4)
parent-bff 是 BFF 聚合层,无 DB,无聚合根,无 @RequirePermission(U4 仲裁豁免)。本节记录 parent-bff 特有的"场景→技术"映射。
| 场景 | 技术/规则 |
|---|---|
| BFF 聚合层定位 | 只聚合不持状态,无 DB,无聚合根,无 @RequirePermission(U4 仲裁豁免,权限由 Gateway JWT + 下游服务负责) |
| GraphQL Yoga 端点 | POST /graphql(GraphQL over HTTP),对前端 GraphQL,对下游 gRPC(U3 仲裁 GraphQL P2 立即引入,parent-bff P4 直接用 GraphQL) |
| DataScope=CHILDREN 越权校验 | ChildGuard 在 Resolver 执行前校验 requestedChildId ∈ iam.GetChildrenByParent(parentId),不绑定抛 BFF_PARENT_CHILD_NOT_BOUND(403) |
| 多子女切换方案 A | 前端管理 childId(Zustand slice),BFF 无状态,ParentSession 仅请求级值对象不持久化;selectChild mutation 仅记录审计日志 |
| 并行 gRPC 编排 | Promise.allSettled + 降级策略(iam 失败返回 user=null,core-edu 失败返回 grades=null),部分失败标记 partial=true |
| DataLoader 批量去重 | per-request 实例,按 parentId 批量取孩子列表、按 studentId 批量取成绩/作业/考试,防 N+1 |
| Redis 聚合缓存 | 30s TTL(coord 推断 60s,ai05 调整为 30s + 主动失效),key=bff:parent:{userId}:{queryHash},singleflight 防击穿 |
| 错误码前缀 | BFF_PARENT__(coord 仲裁 C1,对齐 004 §11.4 BFF 在前风格,不是 PARENT_BFF__) |
| 端口分配 | HTTP 3010,不暴露 gRPC(coord 仲裁 C2,BFF 对下游走 gRPC,对上游仅 HTTP/GraphQL) |
| 下游依赖 | iam + core-edu + data-ana + msg(coord 仲裁 C6 扩展,与 student-bff 对齐) |
| P0 阻塞 | iam 缺 GetChildrenByParent RPC + iam_student_guardians 表,ai02 补全前 parent-bff 无法启动 |
| push-gateway 调用 | 走 HTTP /internal/push(U2 仲裁豁免 gRPC,msg 调用方改用 HTTP) |
| Kafka topic 命名 | edu.notification.sent/read/recalled/failed(coord 仲裁 C5,废弃 edu.notification.events 抽象名) |
| /readyz 下游探针 | P4 添加并行 ping iam + core-edu + data-ana + msg 的 /healthz(coord 推断 BFF 直接 ok,ai05 调整为 P4 加下游探针) |
| GraphQL complexity 防御 | depth limit(≤7)+ query cost analysis(≤1000)+ persisted queries(P6 预留) |
| 优雅关闭 | HTTP server → Redis → gRPC clients → Tracer(BFF 无 DB/Kafka,关闭顺序简化) |
| ParentSession 值对象 | parentId/roles/dataScope:CHILDREN/requestedChildId/traceId,每次请求重建不跨请求(004 §12.1 无状态约束) |
| 通知偏好配置 | NotificationPreference(渠道开关 channels[] + 频率限制 frequency_limit + 静默时段 quiet_hours),P4 通过 msg 服务管理 |
2.15 student-portal(学生端微前端 Remote,P3)
student-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 student-portal 特有的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
|---|---|
| P3 端口分配 | student-portal = 4001(004 §1.2 强制 4 端 4000-4003;ai07 初稿误用 3001 与 iam 冲突,ai14 修订) |
| P3 MF Remote 角色 | next.config.js 配 @module-federation/nextjs-mf(name: student_app,port 4001)暴露 ./pages + ./ExamTaking,复用 Shell 的 AppShell/usePermission/ApiClient |
| P3 DataScope SELF | 学生仅看自己数据(004 §5.4 L0),前端无班级/年级/学校切换器(与 teacher-portal L1-L5 核心差异),路由参数不含 classId/gradeId/schoolId |
| P3 错误码前缀统一 | core-edu 子域统一 CORE_EDU_(移除 EXAMS_/HOMEWORK_/GRADES_ 独立前缀);BFF 前缀 BFF_STUDENT_(coord §5.2 裁决,从 STUDENT_BFF_ 迁移) |
| P3 考试防作弊 useAntiCheat | visibilitychange 切屏检测 + blur 失焦 + copy/paste/contextmenu 禁止 + 违规计数(maxViolations=3)+ 超限自动交卷;前端层仅辅助,最终权威由后端考试时长 + 服务端时间校准保证 |
| P3 服务器时间同步 | useServerTimeSync Hook:RTT 计算 offset = (t1 + rtt/2) - serverTime + 5min 重同步周期;CountdownTimer 用 role="timer" + aria-live="polite" 朗读剩余时间 |
| P3 考试作答状态机 | NotStarted→InProgress→Paused(切屏警告)→AutoSubmitting(倒计时归零)→Submitting→Submitted;Zustand L3 + localStorage 持久化草稿,自动保存每 30s + blur 时保存 |
| P3 考试页面 CSP iframe 防嵌套 | 考试路由 frame-ancestors 'none' + X-Frame-Options: DENY;其他路由允许 Shell 同源嵌入 |
| P3 考试 chunk 性能预算 | 考试作答 chunk < 200KB(动态 import 隔离);总 Bundle < 300KB;LCP < 2.5s / CLS < 0.1 / INP < 200ms |
| P3 移动端一等公民 | 学生端高频移动场景:Tailwind 响应式断点 + viewport meta + 触摸优化(按钮 ≥ 44px);考试页面支持横屏锁定 |
| P3 A11y 考试专项 | WCAG 2.2 AA:键盘导航作答(Tab/Enter/方向键)+ aria-live 倒计时 + 焦点陷阱(考试中不可 Tab 到地址栏)+ 屏幕阅读器兼容 |
| P3 考试草稿多设备同步预留 | 当前 localStorage 单设备;P5+ 预留草稿同步(LWW 冲突解决 + WebSocket 推送 + version 字段) |
| P3 Feature Flag 阶段化 | P3-P5 用环境变量(NEXT_PUBLIC_FEATURE_*);P6+ 预留 Consul 配置中心动态开关 |
| P3 PWA 离线壳预留 | P5+ Service Worker 缓存考试壳,断网时仍可作答,恢复后自动提交 |
| P3 MF 降级与版本协商 | Remote remoteEntry.js 加载失败 → fallback 独立页面 + 错误提示;共享依赖版本不匹配 → 运行时协商降级 |
| P3 AI 答疑 SSE 流式(P5) | POST /student/ai/stream-chat SSE 流式响应 + streamingText 逐字渲染 + 打字机效果 + AbortController 取消 |
| P3 权限点命名 | 与 student-bff §3.2 对齐 STUDENT_*_READ 风格(STUDENT_DASHBOARD_READ/STUDENT_EXAM_READ/STUDENT_HOMEWORK_SUBMIT/STUDENT_GRADE_READ 等) |