# 已知问题速查

> 索引式速查手册：场景 → 技术/规则映射。不写代码示例，不写流水日志，不标注 AI 身份。
> 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../.trae/rules/project_rules.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）

| 场景                   | 技术/规则                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| P2 鉴权（RS256）       | `shared-go/jwks.Fetcher` 拉 IAM `/.well-known/jwks.json` 公钥，TTL 5min 缓存，`jwt.Parse` 用公钥验签          |
| 路由转发               | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy`                                                 |
| 路径重写               | 去掉 `/api/v1` 前缀后转发到下游服务（方案 B：Gateway 不改路径，各服务 Controller 内加 `/v1`）                 |
| 用户上下文注入         | `c.Request.Header.Set("x-user-id/x-user-roles/x-data-scope", ...)` 传递给下游                                 |
| 请求 ID                | Gateway 生成或透传 `X-Request-Id`（`req-<uuid-v4>` 格式），`c.Set("request_id", ...)` + `c.Header(...)`       |
| 限流                   | per-IP 令牌桶（`sync.Map`），超限 429 `GW_RATE_LIMITED`；P6 迁 Redis 支持多副本                               |
| 熔断                   | `gobreaker v2` 共享 downstream 实例，5xx > 50% 触发 OPEN，返回 503 `GW_CIRCUIT_OPEN` + Retry-After            |
| 错误码                 | 统一 `GW_` 前缀（W1），响应体 ActionState 信封 `{success,error:{code,message}}`（W2）                         |
| 结构化日志             | 标准库 `log/slog`（W3），`slog.Info`/`slog.Error` 带 key-value；未接入 shared-go/logger（zap，待评估）        |
| 健康检查               | `GET /healthz`（liveness，无鉴权）+ `GET /readyz`（readiness，并行 ping 9 下游 /healthz，软失败规则，W4）     |
| Metrics                | 7 个业务指标（promauto），`GET /metrics` 暴露（W5）：requests/duration/circuit/ratelimit/proxy/jwks/authfail  |
| Tracer 资源属性        | `resource.New` 补 service.name/version/deployment.environment/host.name（W6，4 项完整）                       |
| DevMode 生产防护       | 启动时 `DevMode=true && ENV=production` panic 拒绝启动（W7，config.go Load 校验）                             |
| 尾斜杠重定向循环       | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")`                       |
| 开发模式鉴权旁路       | `DEV_MODE=true` 时接受 `Bearer dev-token`，注入固定身份；生产必须 `false`                                     |
| 路由注册位置           | 真实路由在 `main.go` 的 `api.Group` 内注册，`internal/routing/routing.go` 若未被 main 引用即为死代码          |
| 多服务路由扩展         | 新增服务代理时用 `registerProxy(api, prefix, targetURL)` helper 注册两组路由，统一管理                        |
| DEV_MODE 环境变量      | Go 不自动加载 .env，`DEV_MODE` 必须在启动前 export 或写入系统环境变量，否则 DevMode=false 导致 dev-token 被拒 |
| go.work workspace 解析 | `go.work` 列 `use ./packages/shared-go`，本地解析 shared-go 依赖，无需 `go mod tidy` 拉远程                   |
| go.work 版本冲突       | go.work 的 `go` 版本须 ≥ workspace 内所有模块的最高版本（push-gateway 要求 1.25.0，故 go.work 用 1.25.0）     |
| CORS 配置              | `CORS(cfg *config.Config)` 从 Config 读白名单，未配置时用开发默认白名单（localhost:3000,3001）                |
| 请求体限制             | `RequestBodyLimit` 先检 Content-Length 再用 `http.MaxBytesReader`，超限 413 `GW_REQUEST_TOO_LARGE` 信封       |

### 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    | `exam.published` / `homework.graded` / `grade.recorded`                                                                                    |
| 不引入 Saga    | 跨服务一致性用 Outbox + 最终一致性                                                                                                         |
| 批改后联动     | 批改完成 → 发 `homework.graded` 事件 → 下游消费（DataAna/Msg）                                                                             |
| Temporal 试点  | 仅 1 个工作流（考试发布编排：创建作业→通知）                                                                                               |
| schema 表      | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events                    |
| 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，启动后会重试                                                          |

### 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+ 连接（ISSUE-003），业务服务调 /internal/push 或消费 Kafka                                                       |
| 心跳协议                        | RFC 6455 控制帧（Ping/Pong），30s 间隔，SetReadDeadline(60s) 超时断开；禁止文本消息 ping/pong                                   |
| 单用户连接数限制                | 默认 5，超限返 PUSH_TOO_MANY_CONNECTIONS + close frame 1008                                                                     |
| 跨实例同步                      | Redis Pub/Sub 订阅 `edu:push:channel:user:<userID>` + `edu:push:channel:broadcast`；在线 SET `edu:push:online:<userID>` 60s TTL |
| Redis SET 启动重建（ISSUE-058） | 启动时 SREM 旧 instanceID 成员，基于内存连接 ForEachUser 重新 SADD，避免 Pod 重启后残留幽灵在线状态                             |
| 离线消息                        | 仅推在线用户；离线消息落库由 msg 服务负责（ISSUE-005），push-gateway 无 DB                                                      |
| 并发写修复                      | send chan + 单写协程模式，避免 gorilla/websocket 并发写竞争                                                                     |
| 内部 API 鉴权                   | `X-Internal-Token` 头 + `INTERNAL_API_TOKEN` 环境变量（ISSUE-002，原名 X-Internal-Key 已废弃）                                  |
| Kafka topic 命名（ISSUE-053）   | `edu.notification.requested`（非 edu.notification.events，遵循 G16 `<domain>.<aggregate>.<action>` 规则）                       |
| Kafka 幂等性                    | 基于 event_id Redis SETNX 去重（24h TTL），at-least-once + 消费失败 3 次入 DLQ                                                  |
| /readyz 软失败（ISSUE-058/006） | Redis/Kafka 不可达返 200 + `degraded: true`，仅 Hub.CloseAll 触发 closing 时返 503；避免 K8s 在依赖抖动时驱逐 Pod               |
| 无 gRPC（ISSUE-007）            | 仅 HTTP + WebSocket，不暴露 gRPC 服务                                                                                           |
| shared-go 复用（ARB-015 §17.5） | tracer/logger/jwks/env 走 shared-go 包，不重复实现                                                                              |
| 错误码前缀                      | `PUSH_*`（PUSH_UNAUTHORIZED/PUSH_INVALID_REQUEST/PUSH_TOO_MANY_CONNECTIONS 等）                                                 |
| 多阶段 Dockerfile               | golang:1.25-alpine 构建 → alpine:3.20 运行，CGO_ENABLED=0 + ldflags -s -w + non-root user + wget healthcheck                    |
| slog 结构化日志                 | log/slog（JSON prod / text dev），替代标准 log 包；字段含 request_id/trace_id/user_id/conn_id/event                             |
| DEV_MODE 鉴权                   | DEV_MODE=true 时接受 dev-token（HS256 fallback），生产环境必须 JWT RS256（JWKS 5min 缓存）                                      |
| 广播端点                        | POST /internal/broadcast，body {event, data}，调用 hub.Broadcast                                                                |
| gorilla/websocket API 陷阱      | SetReadLimit 返回 void（不可 `_ =` 赋值）；SetPongHandler 签名为 `func(appData string) error`（非 `func(string, error) error`） |
| sync.RWMutex 重入死锁           | 持写锁（Lock）的 goroutine 不可再调用同结构体的 RLock 方法（Go RWMutex 非重入）；CloseAll/Broadcast 内部计数改为内联遍历        |
| httptest.Server.Close() 死锁    | 测试 handler 阻塞 ReadMessage 时 Close() 会死锁；用 done channel 模式：cleanup 先 close(done) → cli.Close() → srv.Close()       |

### 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` 等）                                     |
