Files
Edu/docs/troubleshooting/known-issues.md
SpecialX 81a539b9ab
Some checks failed
CI / quality-ts (push) Failing after 6s
CI / quality-go (push) Failing after 25s
CI / quality-proto (push) Failing after 6s
CI / deploy (push) Has been skipped
chore(deps): 统一依赖管理 - pnpm 11 + node:22 + golang:1.25 + python:3.12 + shared-* 集中化
- Node.js 统一到 node:22-alpine,Go 统一到 golang:1.25-alpine,Python 统一到 python:3.12-slim

- pnpm 升级到 11.13.0(corepack),新增 allowBuilds 白名单解决 ERR_PNPM_IGNORED_BUILDS

- 新增 packages/shared-py 集中 Python 共享依赖,shared-ts 补充 graphql-yoga/prom-client

- api-gateway 修复 go.mod 的 shared-go 依赖 + Dockerfile 改用 repo 根作 context

- Python 服务(data-ana/ai)Dockerfile 改用 repo 根作 context + 声明 uv workspace sources

- 16 个服务的 Dockerfile + CI + docker-compose.tools.yml 全部对齐版本矩阵

- known-issues.md 沉淀 9 条 pnpm 11 / uv workspace / Go shared-go 迁移经验

- 验证:4 服务完全成功(api-gateway /healthz 200),其余 install 成功(build 失败为预存 TS 错误)
2026-07-14 12:04:49 +08:00

689 lines
128 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 已知问题速查
> 索引式速查手册:场景 → 技术/规则映射。不写代码示例,不写流水日志,不标注 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 workspaceTS+ go.workGo+ pyproject.toml/uv workspacePython三套并存 |
| 根 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+prettiercommit-msg 校验 Conventional Commits |
| .editorconfig 多语言缩进 | Go 用 tabPython 用 4 空格TS/默认用 2 空格 |
| ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`flat configlint 脚本去掉 `--ext .ts`lint-staged 恢复 `eslint --fix` |
| next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json``next lint` 触发 Strict/Base 选择提示CI 中需预置配置或改 `eslint src` |
| uv.lock 解析失败 | `uv.lock``grpcio-health-checking``source` 字段且匹配多包,导致 `uv run pytest` 全 workspace Python 服务不可用,需重建 lock`uv lock` |
| pnpm 11 allowBuilds | pnpm 11 默认拒绝 build scriptspostinstall需在 `pnpm-workspace.yaml``allowBuilds: { pkg: true }` 对象格式声明白名单(非 `package.json``pnpm.onlyBuiltDependencies`,已废弃) |
| pnpm 11 独立安装 | Dockerfile 仅 COPY `package.json`(无 `pnpm-workspace.yaml`pnpm 11 无法读取 `allowBuilds`,需加 `--ignore-scripts` 跳过 build scripts@nestjs/core/protobufjs 的 postinstall 非功能必需) |
| pnpm 11 无 lockfile 安装 | 服务目录无独立 `pnpm-lock.yaml`Dockerfile 用 `--no-frozen-lockfile` 替代 `--frozen-lockfile`(如 classes 服务) |
| uv workspace 依赖声明 | 服务 `pyproject.toml` 依赖 workspace 内包(如 `edu-shared-py`)时,必须声明 `[tool.uv.sources]\nedu-shared-py = { workspace = true }`,否则 `uv sync` 报 "missing an entry in tool.uv.sources" |
| uv workspace .venv 路径 | uv workspace 在根目录创建 `.venv`非服务目录Dockerfile runtime stage 的 `COPY --from=builder /app/.venv` 路径需指向 `/app/.venv` |
| Go 服务 Dockerfile 上下文 | 依赖 `shared-go` 的 Go 服务api-gateway/push-gatewayDockerfile 必须用 repo 根 `.` 作为 build context并显式 COPY `packages/shared-go/`(非 `services/xxx/` 上下文) |
| Go 服务 shared-go 依赖 | `go.mod` 必须 `require github.com/edu-cloud/shared-go v0.0.0` + `replace github.com/edu-cloud/shared-go => ../../packages/shared-go`,否则编译报 "no required module provides package" |
| Python 服务 Dockerfile 上下文 | 依赖 `shared-py` 的 Python 服务 Dockerfile 必须用 repo 根 `.` 作为 build contextCOPY `pyproject.toml` + `uv.lock` + `packages/shared-py/`(非 `services/xxx/` 上下文) |
### 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+RedisP3 加 Kafka+ZookeeperP4 加 Debezium+CH+Neo4jP5 加 ESP6 加 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` 统一生成 |
| 契约先行硬约束 | 任何服务间通信必须先改 protoCI 检测 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/ownedrepository 下推 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 改 RS256IAM 签发,公钥校验) |
| 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.25-alpine 构建 → alpine:3.20 运行CGO_ENABLED=0依赖 shared-go 时用 repo 根作 build context |
### 1.6 可观测性OTel + Prometheus + Loki
| 场景 | 技术/规则 |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
| 三支柱 | Logspino/winston/zap+ Metricsprom-client+ TracesOTel SDK |
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
| P6 完整后端 | Loki日志+ Grafana仪表盘+ Jaegertrace+ Prometheusmetrics |
| Prometheus 指标 | `http_request_duration_seconds`Histogram+ `http_requests_total`Counter |
| 采样策略 | P1 全量 traceP6 引入采样率降低开销 |
| 日志参数顺序 | `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.ymlalerting 关联 alertmanager |
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus默认和 Lokiuid=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-instrumentationsHttpInstrumentation/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 wrappertiptap / @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 L80coord 整改清单若标 ai05 则为错配AI 必须以 ai-allocation §3.2 为准,提请 coord 修正而非自行执行 |
| 仲裁责任方错配msg→ai05 | msg 模块由 ai10 负责ai-allocation §3.2 L81coord 整改清单若标 ai05 则为错配;同上 |
| 仲裁责任方错配iam→ai02 | iam 模块由 ai06 负责ai-allocation §3.2 L77ai02=push-gatewaycoord 整改清单若标 ai02 则为错配;同上 |
| 仲裁责任方错配push-gateway→ai01 | push-gateway 由 ai02 负责ai-allocation §3.2 L73ai01=api-gatewaycoord §4.2/§5.6/整改第 21 项若标 ai01 则为错配;同上 |
| 仲裁责任方错配teacher-portal→ai07 | teacher-portal 由 ai13 负责ai-allocation §3.2 L84ai07=classes黄金模板coord §2.4/§4.1/§5.5/整改第 2 项若标 ai07 修正 teacher-portal 则为错配;同上 |
| 仲裁责任方错配parent-portal→ai07 | parent-portal 由 ai15 负责ai-allocation §3.2 L86ai07=classescoord §2.4 第 4 项若标 ai07 修正 parent-portal 端点则为错配;同上 |
| 仲裁责任方错配core-edu→ai03 | core-edu 由 ai08 负责ai-allocation §3.2 L79ai03=teacher-bffcoord §4.2/整改第 14 项/整改第 23 项若标 ai03 则为错配;同上 |
| 仲裁责任方错配parent-bff→ai07 | parent-bff 由 ai05 负责ai-allocation §3.2 L76ai07=classescoord §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-gatewayGo
| 场景 | 技术/规则 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| P2 鉴权RS256 | `shared-go/jwks.Fetcher` 拉 IAM `/.well-known/jwks.json` 公钥TTL 5min 缓存,`jwt.Parse` 用公钥验签 |
| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` |
| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务(方案 BGateway 不改路径,各服务 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/loggerzap待评估 |
| 健康检查 | `GET /healthz`liveness无鉴权+ `GET /readyz`readiness并行 ping 9 下游 /healthz软失败规则W4 |
| Metrics | 7 个业务指标promauto`GET /metrics` 暴露W5requests/duration/circuit/ratelimit/proxy/jwks/authfail |
| Tracer 资源属性 | `resource.New` 补 service.name/version/deployment.environment/host.nameW64 项完整) |
| DevMode 生产防护 | 启动时 `DevMode=true && ENV=production` panic 拒绝启动W7config.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 classesTS/NestJSP1 黄金模板)
| 场景 | 技术/规则 |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| 黄金模板定位 | 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+ E2EPlaywright |
| 覆盖率门槛 | 领域逻辑 ≥ 80%Handler ≥ 60%,整体 ≥ 60% |
| Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` |
| ID 生成 | `@paralleldrive/cuid2` 的 `createId()` |
| 响应转换 | repository 返回 Dateservice 转换为 `createdAt: number`(时间戳) |
| 阶段特有模式回写 | OutboxP3/ CDCP4/ 长连接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 iamTS/NestJS
| 场景 | 技术/规则 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 双入口 | REST `/v1/iam/*` + gRPC 50052package `next_edu_cloud.iam.v1`),同一 IamService 实例共用Hybrid app `connectMicroservice` |
| 认证 | RS256 非对称签名,私钥本地文件(`IAM_PRIVATE_KEY_PATH`),公钥通过 JWKS 端点暴露给 Gateway |
| JWT payload | `{ sub, email, roles, dataScope, type, iss, aud, jti(仅 refresh), kid(header) }`register 自动分配 teacher 角色 |
| Token TTL | access 15min / refresh 7dayrefresh 含 jti轮换时旧 jti 加入 Redis 黑名单 |
| RBAC | 角色/权限/角色-权限查询 + `getEffectivePermissions(userId)` API三层角色模型system/organization/temporarylevel 0/1/2 |
| 视口配置 | 4 层模型(导航/路由/组件/数据),`iam_role_viewports` 表含 `level` ENUM(admin/teacher/student/parent) |
| DataScope 解析 | 6 级self/subject/class/grade/school/allSUBJECT 替代 DISTRICT注入 JWT payload |
| 权限校验 | PermissionGuardAPP_GUARDDB 驱动 + Redis 缓存 TTL 5min`data_scope=all` 直放行Key `iam:perm:{userId}` |
| Token 黑名单 | Redis Key `iam:bl:{jti}` TTL 与 refresh_token 剩余有效期对齐logout/refresh 时写入 |
| 审计日志 | `iam_user_audit_log` 表 + `AuditCreated` Kafka 事件,登录/角色变更/密码修改等关键操作记录 |
| 家长-学生关系 | `iam_student_guardians` 表unique(studentId, guardianId)`GetChildrenByParent` RPC + `GET /v1/iam/children` REST |
| schema 表 | iam_users / iam_roles / iam_user_roles / iam_permissions / iam_role_permissions / iam_refresh_tokens / iam_role_viewports / iam_student_guardians / iam_user_audit_log / iam_password_history |
| Outbox | shared-ts `OutboxModule.forRoot({ config, db, kafkaProducer })`,表名 `iam_outbox`topic `edu.iam.user.events`/`edu.iam.role.events`/`edu.iam.audit.created` |
| AuthMiddleware 注册 | `app.module.ts` `configure()` 注册于 me/logout/viewports/permissions/roles/audit/children/change-password/users/totp 路由,解析 `x-user-*` 头;漏注册会报 "Missing user identity" |
| 公开路径 | register/login/refresh/jwks.json 无 `@RequirePermission()`PermissionGuard 旁路 |
| ESM 模式 DI | `providers: [IamService, IamRepository]` + 构造器 `@Inject(IamRepository)` 显式注入,避免 `undefined` 运行时错误 |
| APP_GUARD DI 导出 | PermissionGuard 注册为 APP_GUARDAppModule 级),其依赖 PermissionCacheService/IamRepository 须在 IamModule `exports` 中导出,否则 AppModule 上下文无法注入 |
| Proto 路径解析 | `resolveProtoPath()` 兼顾 cwd=monorepo 根CI与 cwd=services/iam`pnpm --filter` dev先 `packages/shared-proto/proto/`,回退 `../../packages/shared-proto/proto/` 与本地 `proto/`Docker |
| Drizzle ORM API | `inArray(col, vals)` 替代不存在的 `.in()``delete().where()` 不支持链式多次 where用 `and(eq(...), eq(...))` 组合 |
| Drizzle MySQL insert | `MySqlInsertBase` 无 `.ignore()` 方法PostgreSQL 才有),改用 SELECT-then-INSERT 手动存在性检查避免唯一键冲突 |
| Docker 多阶段构建 | builder 阶段先 `WORKDIR /app/packages/shared-ts && pnpm build` 再 `WORKDIR /app/services/iam && pnpm build`runtime 阶段需 `COPY --from=builder /app/packages/shared-ts/dist` 复制预编译产物 |
| Husky prepare 脚本移除 | `ENV HUSKY=0` 无效pnpm install 仍会执行 prepare用 `node -e "delete p.scripts.prepare"` 在 install 前删除根 package.json 的 prepare 脚本 |
| TOTP 表 schema | `iam_user_totp`user_id UNIQUE+ `iam_totp_backup_codes`user_id 索引 + code_hash必须在 `02-all-services-schema.sql` 初始化,否则 TOTP 启用端点报 Table not exists |
| JWT RS256 密钥生成 | `docker run --rm -v <host>:/keys alpine/openssl genrsa -out /keys/iam-private.pem 2048` + `openssl rsa -in ... -pubout -out ...`,加载用 `IAM_PRIVATE_KEY_PATH` / `IAM_PUBLIC_KEY_PATH` |
| PowerShell curl JSON | PowerShell 内联 JSON 转义不可靠(双引号嵌套),改用 `curl.exe -d "@file.json"` 从文件读取请求体;务必用 `curl.exe` 而非 PowerShell 别名 `curl` |
| ESLint no-useless-escape | 正则字符类 `[...]` 内的 `[` `]` `/` 无需转义,`/[!@#$%^&*()_+\-=[\]{};':"\\ | ,.<>/?`~]/` 是正确写法(`\[` `\/` 触发 no-useless-escape |
| Drizzle DataScope 类型 | `mysqlEnum` 推断类型为字面量联合Repository 方法参数须用 `DataScope` 类型而非 `string`,否则 TS 报错 |
| ioredis 类型 | `import { Redis } from "ioredis"`named import`type RedisClient = InstanceType<typeof Redis>`,默认导入在 TS 中不可构造 |
| jwt.sign expiresIn 类型 | `@types/jsonwebtoken` v9 `expiresIn` 类型为 `number \| StringValue`,普通 string 不兼容,用 `ttlToSeconds(ttl)` 返回 number |
| 健康检查依赖 | `/readyz` 5 依赖DB(SELECT 1) / Redis(ping) / Kafka(producer 实例) / JWKS(密钥文件可读) / gRPC(进程内) |
| 优雅停机 | 顺序Kafka producer disconnect → Redis quit → DB pool endLifecycleService `onApplicationShutdown` |
### 2.4 core-eduTS/NestJSP3
| 场景 | 技术/规则 |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 |
| 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_versionISSUE-006 #6 裁决 exam.submitted 事件只带 submission_id+studentId |
| IAM 事件消费 | `iam-consumer` 订阅 `edu.iam.teacher.assigned`/`edu.iam.class.created`,幂等 upsert 到 `teacher_associations`/`classes` 表 |
### 2.5 contentTS/NestJSP4
| 场景 | 技术/规则 |
| -------------- | ------------------------------------------------------------------------------- |
| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) |
| 题库 CRUD | P4 仅 CRUDP5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 |
| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 |
| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j |
| API 字段名 | 请求体用 TS schema 字段名(如 `order`),非 DB 列名(如 `order_num` |
| Neo4j 不可用 | 未设置 NEO4J_URL 时 driver=nullgetNeo4jSession 返回 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-anaPython/FastAPIP4
| 场景 | 技术/规则 |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 学情诊断 | 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 AIOKafkaConsumerlifespan 启动 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_logai-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 端口 50055004 §4.2 矩阵HTTP 3006 保留作 Gateway 直连降级 |
| DataScope 6 级过滤 | Repository 层注入 WHERESELF/CLASS/GRADE/SCHOOL/DISTRICT/ALLiam `GetEffectiveDataScope` gRPC P4 补全004 §15.3 #5 |
| Outbox 豁免 | 派生数据事件MasteryUpdated / WarningTriggered / AIUsageRecorded豁免 Outbox允许直接 Kafka producer004 §12.2 + §15.3 #6 |
| 掌握度算法 | WEIGHTED_MOVING_AVGw_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` 手动 commitat-least-oncecommit 前确保 ClickHouse 写入成功 |
| ClickHouse 容量规划 | 5 宽表年增量 ~2.3GB5000 学生×20 次/月单节点可承载P6 按 class_id hash 分片 + ReplicatedMergeTree |
| ClickHouse FINAL | ReplacingMergeTree 查询必须加 `FINAL` 或用 `argMax` 聚合否则读到重复版本clickhouse_repository.py 已实现 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_*_DASHBOARDv2 新增 4 端权限) |
| SubscribeMasteryUpdate | gRPC server-streaming RPC 为 P5+ AI 个性化推荐预留实时推送通道analytics.proto v2 提案) |
| GDPR 数据删除 | ClickHouse 无 UPDATE/DELETE用 `ALTER TABLE ... DELETE WHERE student_id=?` 异步 mutationP6 实现) |
| 字段脱敏 | student_id 在日志中 hash 化structlog processorPII 不入日志 |
| 事件版本化 | 事件 payload 加 `schema_version: "v1"` 字段,破坏性变更发 v2consumer 双消费 6 个月过渡 |
| API 版本化 | HTTP `/analytics/v1/*` + gRPC `next_edu_cloud.analytics.v1`,破坏性变更新增 v2旧版保留 6 个月 |
| Projection 演进 | P4 CDC 直连 → P4 后期双消费CDC + 领域事件)→ P6 ClickHouse MaterializedView 自动聚合 |
| 测试策略 | pytest + TestcontainersClickHouse/Kafka 容器化)+ 单元 ≥80% + 集成 ≥60% + 契约 pact-python + E2E |
| Dockerfile 多阶段 | builderuv sync --frozen+ runtimepython: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` 已废弃 |
| ruff per-file-ignores | gRPC servicer 方法必须 PascalCaseN802 忽略)/ FastAPI Depends 在参数默认值B008 忽略pyproject.toml 配 per-file-ignores |
| Mock 数据脚本 | `scripts/data-ana-mock-data.sql` 提供 5 宽表演示数据20 学生×2 考试×3 知识点),用 `now()-INTERVAL` 避免硬编码日期 |
| Python 3.12+ 现代化 | ruff UP042 用 StrEnum 替代 `str, Enum` / UP046 用 PEP 695 类型参数 `class Foo[T]` 替代 `Generic[T]` |
| ClickHouse argMax 别名冲突 | `argMax(col, calculated_at)` 与同 SELECT 的 `max(calculated_at) AS calculated_at` 冲突ILLEGAL_AGGREGATION别名改为 `latest_calculated_at` |
| Python proto 生成 | `grpc_tools.protoc` 生成 `_pb2_grpc.py` 用 `import xxx_pb2`flat放入 package 需改为 `from generated_proto import xxx_pb2` |
| Dockerfile PYTHONPATH | Python 服务 `src/generated_proto/` 作为包导入时Dockerfile 需 `ENV PYTHONPATH="/app/src"`,否则 `ModuleNotFoundError: generated_proto` |
| OTel 日志噪声 | OTel collector 不可达时刷屏 `Transient error ... retrying`,设 `OTEL_TRACES_EXPORTER=none` + `OTEL_METRICS_EXPORTER=none` + `OTEL_LOGS_EXPORTER=none` 静默 |
| Docker Hub 不可达 | Dockerfile base image 用标准名 `python:3.12-slim`,本地 `docker pull docker.m.daocloud.io/library/python:3.12-slim` 后 `docker tag` 为标准名 |
| ruff per-file-ignores | 自动生成的 proto 代码(`src/generated_proto/*.py`)在 `pyproject.toml` 配 `per-file-ignores = ["ALL"]`,避免 lint-staged 传单文件时 `extend-exclude` 无效 |
### 2.7 messagingTS/NestJSP5
| 场景 | 技术/规则 |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
| 通知批量化 | `createBatch(items)` 单次 INSERT沿用旧项目 dispatcher 模式 |
| 多渠道策略 | ChannelStrategy 接口 + InApp/Email/Sms/Push 实现新增渠道不改核心ai10 设计) |
| fan-out 分页 | `listByUserWithPagination(userId, page, pageSize)` 分页查询 |
| ES 降级(单向) | ES_URL 未设置时 esClient=nullsafeIndex/safeSearch 跳过返回空结果 |
| DB→ES 降级读 | DB 故障时 fallback 到 ES 读模型,响应头标 `X-Read-Model: es-degraded`ai10 设计) |
| ES→DB 降级检索 | ES 故障时 search() fallback 到 DB LIKE降级计数打 metricsai10 设计) |
| Push 推送降级 | PUSH_GATEWAY_URL 未设置或连接失败时 try/catch 跳过,不影响 DB 写入PushChannel 失败降级 in_app |
| Kafka 消费幂等 | event_id → Redis SETNX `msg:idem:{event_id}` TTL 7dRedis 不可用降级 msg_idempotency 表唯一键ai10 设计) |
| 三层幂等防线 | L1 Redis SETNX / L2 msg_idempotency 表 / L3 notifications.source_event_id 唯一索引ai10 设计) |
| 已读/未读位图 | Redis Bitmap `msg:unread:{userId}`BITCOUNT 计数Redis 不可用降级 DB COUNTai10 设计) |
| Outbox 强制(待 ai10 确认) | 004 §7.2 列 msg 生产 `edu.notification.events`,按 004 §12.2 强制 OutboxP5 必须实现 msg_outbox_events 表 + Publisher worker |
| 12 类事件消费清单(待 ai10 确认) | 消费 iamuser.created/updated/deleted/role_changed + role.created/updated+ core-eduexam.published/assignment.submitted/assignment.graded/grade.recorded/attendance.recorded+ data-anamastery.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_hours5 维度配置矩阵 |
| 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 → Tracer8 步序列保证在途消息处理完成 |
| events.proto message 缺失阻塞(待 ai10 确认) | events.proto 缺 UserEvent/RoleEvent/NotificationEvent/MasteryEvent 4 类 messagemsg 消费 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_EXCEEDEDai10 设计) |
| 优雅关闭 | 统一到 LifecycleService顺序 HTTP→Kafka→Outbox→ES→Redis→DB→Tracerai10 修订) |
| db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数 |
| db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数 |
### 2.8 push-gatewayGoP5
| 场景 | 技术/规则 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| WebSocket 长连接 | 单实例支撑 10w+ 连接ISSUE-003业务服务调 /internal/push 或消费 Kafka |
| 心跳协议 | RFC 6455 控制帧Ping/Pong30s 间隔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-005push-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 TTLat-least-once + 消费失败 3 次入 DLQ |
| /readyz 软失败ISSUE-058/006 | Redis/Kafka 不可达返 200 + `degraded: true`,仅 Hub.CloseAll 触发 closing 时返 503避免 K8s 在依赖抖动时驱逐 Pod |
| 无 gRPCISSUE-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/slogJSON prod / text dev替代标准 log 包;字段含 request_id/trace_id/user_id/conn_id/event |
| DEV_MODE 鉴权 | DEV_MODE=true 时接受 dev-tokenHS256 fallback生产环境必须 JWT RS256JWKS 5min 缓存) |
| 广播端点 | POST /internal/broadcastbody {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 aiPython/FastAPIP5
| 场景 | 技术/规则 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| LLM Provider 适配 | 4 适配器OpenAI/Anthropic/Baichuan/Ollama+ ProviderFailoverChain + CircuitBreaker3 次失败 60s 熔断) |
| 降级模式方案 B | success=true + error=null + data 内 degraded=true + degraded_reason总裁 §2.6,非顶层 degraded 字段) |
| ActionState 信封 | 所有 HTTP 响应统一 `{success, data, error:{code,message,details?,traceId?}}`004 §11.5 强制) |
| 路由前缀 | /v1/ai 前缀APIRouter prefix="/v1/ai"Gateway 代理 /api/v1/ai/* → /v1/ai/* |
| gRPC server | grpc.aio 端口 500588 RPC + 3 interceptorLogging/Auth/Error |
| 评估三道防线 | RuleValidatorJSON/字段/难度校验)→ LLMJudge语义评分→ QualityGate综合决策门控 |
| 备课工作流 | P5 用 asyncio.create_task + Redis 状态存储24h TTLP6+ 评估 Temporal |
| 工作流状态机 | pending→analyzing→generating→pending_review→persisted/failed生成失败重试最多 3 次 |
| 限流三维度 | user 10/min + IP 30/min + school 100/minRedis Lua 原子令牌桶Redis 不可用降级放行 |
| 用量记录 | Redis INCRBY 按 user/school/month 维度35 天过期Kafka 发布 AIUsageEvent 到 edu.ai.usage topic |
| Kafka 派生数据豁免 Outbox | AIUsageEvent 为派生数据004 §12.2 豁免 Outbox直接 aiokafka producer事务性 + 幂等) |
| 安全层全本地 | PIIRedactor5 类 PII 正则脱敏)+ InputSanitizerprompt injection 检测)+ OutputModerator5 类敏感内容审核) |
| 六边形架构端口模式 | ContentClient/DataAnaClient/IamClient 均为抽象接口 + Mock + gRPC 实现,全并行模式用 Mock |
| 权限校验 | PermissionGuard 5 权限点ai:chat / ai:question:generate / ai:expression:optimize / ai:lesson:generate / confirm |
| dev_mode tracer | dev_mode=true 时跳过 OTel exporter 初始化 + 跳过权限校验 |
| proto 包名 | 保持 `next_edu_cloud.<domain>.v1`coord G17 裁决覆盖 project_rules §5 |
| proto_gen 排除 ruff | pyproject.toml `[tool.ruff] exclude = ["src/ai/proto_gen"]`,自动生成文件不参与 lint |
| ruff UP017 | `datetime.now(timezone.utc)` 改用 `datetime.now(datetime.UTC)` 别名Python 3.12+ |
| ruff SIM103/SIM110 | `if x: return False; return True` 改 `return not x`for 循环改 `all()` 表达式 |
| PowerShell 不支持 && | RunCommand 用 cwd 参数指定工作目录,不用 `cd ... && uv run` |
### 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 tokenP2 起由 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** 为 Shellstudent:**4001**/parent:**4002**/admin:**4003** 为 Remote004 §1.2 + coord final §F1MF 暴露 **AppShell 整体**coord final §F10各 Remote 自行决定内部布局 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error:{code,message,details?,traceId}}` 解析 + 错误码前缀路由 i18n + trace_id 透传;**P2 起对接 GraphQL**urql/apollocoord 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_errorcoord final §F4翻译文件按域分 namespace |
| P2 权限点命名风格 | **统一 `<RESOURCE>_<ACTION>`**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ数据范围用后缀 `_OWN`/`_CHILD`(如 GRADE_READ_CHILDcoord final §F7 |
| P2 Token 存储 | **P2 用 localStorage**accessToken + refreshTokencoord final §F12httpOnly Cookie + CSRF Token **P6 单独评估**非分阶段迁移P2 直接采用最终方案的当前阶段形态) |
| P2 packages 归属 | **ai13 维护** packages/ui-tokens、ui-components、hookscoord final §F8coord 仅维护 shared-ts、contractsai13 需建立这 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-permissionscoord final §F6/I8+ GET /iam/viewports + GET /iam/rolesparent 子女切换 POST /parent/children/:childId/select |
| P2 MSW mock 层 | `src/mocks/` 含 handlers.ts拦截 /api/auth/login + /api/v1/teacher/graphql+ fixtures/*.ts + browser.tssetupWorker+ server.tssetupServer+ index.tsinitMocks 按 NEXT_PUBLIC_API_MOCKING=enabled 启用) |
| P2 MSW worker 脚本生成 | `npx msw init public/` 生成 mockServiceWorker.js 静态文件package.json 自动添加 `msw.workerDirectory` 配置;必须在 teacher-portal 目录下执行msw 在 devDependencies |
| P2 MSW providers 集成 | GraphQLProvider 内 `initMocks()` ready 前返回 null 避免 GraphQL 请求未拦截server.ts `onUnhandledRequest: "bypass"` 放行非 mock 请求 |
| P2 urql useQuery 类型 | urql useQuery 返回 data 类型推断不足map 回调参数隐式 any需 `import type { XxxItem } from "@/lib/graphql"` + `(data.xxx as XxxItem[]).map(...)` 显式断言 |
| P2 Suspense + useSearchParams | Next.js App Router 中 useSearchParams 必须包裹 `<Suspense>`否则构建报错students/exams/homework/grades 页面均用 `Suspense fallback={<Loading/>}` 包裹 StudentsContent |
| P2 DataScope 类型对齐 | graphql.ts 的 DataScope 必须与 hooks/types.ts 的 DataScope 完全一致(`"SELF" \| "CLASS" \| "GRADE" \| "SCHOOL" \| "DISTRICT" \| "ALL"`ARB-001 §1.2 schema否则 buildPermissionContext 返回类型不匹配 |
| P2 TS4114 override | tsconfig.base.json `noImplicitOverride: true` 下React.Component 子类的 state/componentDidCatch/render 必须加 `override` 修饰符 |
| P2 cn.ts null vs undefined | `match ? match[1] : null` 在 noUncheckedIndexedAccess 下返回 `string \| undefined`,与 `string \| null` 不兼容;改用 `match?.[1] ?? null` |
| P2 AppShell GraphQL 化 | AppShell 从 REST `/teacher/viewports` 改为 GraphQL `ViewportsQuery` + `MeQuery`;权限上下文从 localStorage 读取 + usePermission 注入;视口按 requiredPermission 过滤 |
| P2 健康检查路由 | `app/api/health/route.ts`liveness+ `app/api/ready/route.ts`readiness均 `export const dynamic = "force-dynamic"`,返回 NextResponse.jsonDockerfile HEALTHCHECK 指向 :4000/api/health |
| P2 Dockerfile 端口对齐 | teacher-portal 端口 3000→4000004 §1.2Dockerfile `ENV PORT=4000` + `EXPOSE 4000` + `CMD next start -p 4000` + HEALTHCHECK `http://localhost:4000/api/health` |
| P2 MSW GraphQL 路由 | handlers.ts `http.post("*/api/v1/teacher/graphql")` 按 operationName switch 路由;未传 operationName 时从 query 文本正则提取 `/(?:query\|mutation)\s+(\w+)/` 兜底 |
### 2.13 parent-portal家长端微前端 RemoteP4
> parent-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 parent-portal **特有**的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P4 端口分配 | parent-portal = **4002**004 §1.2 强制 4 端 4000-4003ai07 初稿误用 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-3Tab Bar→ 4-10下拉→ >10搜索 + 列表);超出 10 触发性能预警 |
| P4 通知偏好三维矩阵 | `Record<childId, Record<eventType, Record<channel, boolean>>>`react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 |
| P4 跨标签同步 | BroadcastChannel API`parent-portal-sync`+ localStorage 持久化 + LWWLast-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-SAar-SA 用 `dir="rtl"` + Tailwind `rtl:` 变体;翻译文件按域分 namespaceparent/grades/notifications/preferences |
| P4 PWA + Service Worker | manifest.jsonname/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 ActionsMF 升级路径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_ 对齐 |
| P4 认证 token 存储 | localStorageF12 裁决P4 用 localStorage非 cookierefreshAccessToken 竞态防护用 inflight Promise 复用,并发请求只触发一次 refresh fetch |
| P4 switchChild 双层实现 | ARB-020 §22.5 裁决:切换子女走 GraphQL Mutation后端审计Redis `parent:selected:{parentId}` TTL 30s+ Zustand slice 前端缓存乐观更新mutation fire-and-forget失败不回滚 |
| P4 通知偏好 localStorage 降级 | ISSUE-033 裁决P4 localStorage 为准,更新先写本地再尝试 GraphQL mutation失败不阻塞P5+ 切换到后端为主存储 |
| P4 上游 mock 策略 | MSW 拦截 parent-bff GraphQL + iam RESTiam GetChildrenByParent P0 阻塞用 mock fixturesstudent-001 + student-002 双子女MSW server 在 providers 启动 |
| P4 双 /v1 前缀路由 | ARB-022 §24.4 ISSUE-003 方案 A所有 API 路径双 /v1gateway /api/v1 + 服务 /v1parent-portal GraphQL=`/api/v1/parent/v1/graphql`、iam login=`/api/v1/iam/v1/login`、notifications=`/api/v1/parent/v1/notifications`、web-vitals=`/api/v1/parent/v1/web-vitals` |
| P4 测试覆盖率阈值 | vitest.config.ts thresholds: statements/lines/functions ≥85%branches ≥75%pages/observability/middleware/ErrorBoundary 排除(由集成/E2E 覆盖413 测试通过,覆盖率 99%+ |
| TS noUncheckedIndexedAccess | Record 索引访问返回 `T \| undefined`;内层 Record 改 `Partial<Record<...>>` 或空对象回退(`?? {}`)避免 undefined 传播 |
| urql fetchOptions headers 类型 | 返回类型须用 `Record<string, string>` 累积再展开,不可用条件对象(`{...(token?{Authorization}:{})}`)否则 TS 推断不兼容 |
| Vitest Promise 身份比较 | `expect(p1).toBe(p2)` 对 Promise 不可靠async 包装产生新 Promise改为验证行为副作用如 fetch 调用次数 === 1 证明竞态防护生效) |
| jsdom window.location.href | 设置触发 "Not implemented: navigation" stderr 警告非错误测试仍通过mock 用 `Object.defineProperty(window, "location", {...})` 替代 `vi.spyOn` |
| MSW 响应过快观察加载态 | MSW 默认同步响应,加载态测试看不到 "加载中";用 `server.use()` + `delay(ms)` 延迟响应再断言按钮文本 |
| Testing Library getByRole 误匹配 | `getByRole("button", { name: /未读/ })` 会匹配通知项 aria-label="未读";改用 `getAllByRole("button")` + find 文本内容过滤精确选中筛选按钮 |
| ESLint flat config 测试文件 | 放宽规则需含 `*.test.tsx`/`*.spec.tsx`/`__tests__/**` 模式,否则 tsx 测试文件按严格规则报错 |
| Dockerfile 多阶段构建 | G1 裁决首次即多阶段buildernode:20-alpine 跑 build+ runnernode:20-alpine 跑 server端口 4002HEALTHCHECK 指向 `/api/health`,非 root 用户 nextjs |
### 2.14 parent-bffTS/NestJSP4
> parent-bff 是 BFF 聚合层,无 DB无聚合根无 @RequirePermissionU4 仲裁豁免)。本节记录 parent-bff 特有的"场景→技术"映射。
| 场景 | 技术/规则 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BFF 聚合层定位 | 只聚合不持状态,无 DB无聚合根无 @RequirePermissionU4 仲裁豁免,权限由 Gateway JWT + 下游服务负责) |
| GraphQL Yoga 端点 | POST /graphqlGraphQL over HTTP对前端 GraphQL对下游 gRPCU3 仲裁 GraphQL P2 立即引入parent-bff P4 直接用 GraphQL |
| DataScope=CHILDREN 越权校验 | ChildGuard 在 Resolver 执行前校验 requestedChildId ∈ iam.GetChildrenByParent(parentId),不绑定抛 BFF_PARENT_CHILD_NOT_BOUND403 |
| 多子女切换方案 A | 前端管理 childIdZustand sliceBFF 无状态ParentSession 仅请求级值对象不持久化selectChild mutation 仅记录审计日志 |
| 并行 gRPC 编排 | Promise.allSettled + 降级策略iam 失败返回 user=nullcore-edu 失败返回 grades=null部分失败标记 partial=true |
| DataLoader 批量去重 | per-request 实例,按 parentId 批量取孩子列表、按 studentId 批量取成绩/作业/考试,防 N+1 |
| Redis 聚合缓存 | 30s TTLcoord 推断 60sai05 调整为 30s + 主动失效key=bff:parent:{userId}:{queryHash}singleflight 防击穿 |
| 错误码前缀 | BFF_PARENT__coord 仲裁 C1对齐 004 §11.4 BFF 在前风格,不是 PARENT_BFF__ |
| 端口分配 | HTTP 3010不暴露 gRPCcoord 仲裁 C2BFF 对下游走 gRPC对上游仅 HTTP/GraphQL |
| 下游依赖 | iam + core-edu + data-ana + msgcoord 仲裁 C6 扩展,与 student-bff 对齐) |
| P0 阻塞 | iam 缺 GetChildrenByParent RPC + iam_student_guardians 表ai02 补全前 parent-bff 无法启动 |
| push-gateway 调用 | 走 HTTP /internal/pushU2 仲裁豁免 gRPCmsg 调用方改用 HTTP |
| Kafka topic 命名 | edu.notification.sent/read/recalled/failedcoord 仲裁 C5废弃 edu.notification.events 抽象名) |
| /readyz 下游探针 | P4 添加并行 ping iam + core-edu + data-ana + msg 的 /healthzcoord 推断 BFF 直接 okai05 调整为 P4 加下游探针) |
| GraphQL complexity 防御 | depth limit≤7+ query cost analysis≤1000+ persisted queriesP6 预留) |
| 优雅关闭 | HTTP server → Redis → gRPC clients → TracerBFF 无 DB/Kafka关闭顺序简化 |
| ParentSession 值对象 | parentId/roles/dataScope:CHILDREN/requestedChildId/traceId每次请求重建不跨请求004 §12.1 无状态约束) |
| 通知偏好配置 | NotificationPreference渠道开关 channels[] + 频率限制 frequency_limit + 静默时段 quiet_hoursP4 通过 msg 服务管理 |
| P5 msg gRPC 客户端 | MsgClient interface + MockMsgClient + GrpcMsgClientNotificationPreferences RPC 未定义时回退默认偏好(避免下游未实现阻塞 BFF |
| P5 push-gateway HTTP 客户端 | PushHttpClient interface + MockPushHttpClient + HttpPushHttpClientfetch + 3s timeout + AbortController |
| P5 通知偏好过滤 | EVENT_TYPE_MAP 映射事件→通知类型,拉取偏好后检查偏好开关 + channels 交集,跳过关闭偏好的家长推送 |
| P5 Kafka consumer 多 handler | 同一 topic 可注册多个 handlerpush + cache-invalidationhandlers 改为 `Map<string, EventHandler[]>` 支持多 handler |
| P5 Kafka 幂等 + DLQ | event_id Redis SETNX 去重24h TTL+ MAX_RETRIES=3 指数退避 + DLQ `edu.parent-bff.dlq` |
| P5 缓存失效 handler | edu.teaching.grade.recorded→grades+dashboard 失效edu.notification.read/recalled→notifications 失效 |
| P6 opossum 熔断器 | per-service 单例 + 事件监听open/close/halfOpen→circuitStateGauge+ withCircuitBreaker 包装 callWithMetrics开启抛 503 |
| P6 熔断器配置 | timeout 5s / errorThresholdPercentage 50% / resetTimeout 30s / volumeThreshold 10 |
| P6 Helm chart 结构 | Chart.yaml + values.yaml + values-{dev,staging,prod}.yaml + templates/_helpers.tpl + deployment + service + hpa + configmap |
| P6 podAntiAffinity | preferredDuringSchedulingIgnoredDuringExecution + weight 100 + topologyKey kubernetes.io/hostname |
| P6 HPA 多环境 | dev: 1 replica HPA disabledstaging: 2 replicas HPA 2-5prod: 3 replicas HPA 3-10 + CPU 70%/memory 80% |
| P6 SLO 告警规则 | parent-bff-slo 组 9 条告警P95>200ms / 错误率>0.1% / 可用性<99.9% / 熔断器open / 下游错误率>5% / 缓存命中率<50% / Kafka停滞 / push失败率>5% / ChildGuard突增 |
| P6 Grafana 面板 | 11 面板P95延迟/错误率/可用性/熔断器/缓存命中率/ChildGuard/下游QPS/下游P95/Kafka消费/push推送/GraphQL QPS |
| ESM 模式 import 后缀 | 相对 import 用 `.js` 后缀NodeNext 构建产物test 文件 import 源码也用 `.js` 后缀 |
| vitest coverage 配置 | coverage 配置在 `test` 内部vitest v2 API不在根层级exclude 排除基础设施文件gRPC clients/DataLoader/GraphQL 框架/Redis/Kafka |
| 测试覆盖率排除策略 | 基础设施/框架代码gRPC client 实现/DataLoader 工厂/GraphQL 框架设置/Redis client/Kafka consumer需 E2E 测试覆盖,单元/集成测试排除 |
| Kafka consumer Buffer 类型 | kafkajs `message.value` 类型为 `Buffer | null`,传参需 `?? undefined`转换为`Buffer | undefined` |
| Mock 客户端 markAsRead 类型 | `MOCK_NOTIFICATIONS[parentId]` 可能 undefined需 `if (!list) continue``list[idx]` 可能 undefined需 `const target = list[idx]; if (target)` 守卫 |
| msg.client callWithMetrics | callWithMetrics 签名是 `(client, method, request, serviceName)`,不是 `("msg", "X", () => fn)` |
### 2.14.1 student-bffTS/NestJS/GraphQLP3
> student-bff 是学生场景域 BFF 聚合层GraphQL Yoga + gRPC 下游通信。本节仅记录 student-bff **特有**的"场景→技术"映射,通用 NestJS 映射见 §1.4。
| 场景 | 技术/规则 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P3 API 风格 | **GraphQL Yoga**coord §B1 裁决P2 起直接 GraphQL禁止 REST 渐进schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`president §2.2.1 |
| P3 下游通信 | **gRPC 首次即用**coord §B2 裁决,@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetchDownstreamClient 抽象复用 shared-tscoord §B83 BFF 统一) |
| P3 错误码前缀 | `BFF_STUDENT_`coord §B5 裁决BFF 在前,非 `STUDENT_BFF_`i18n key `error.bffStudent.<code_snake>`coord §F4 |
| P3 越权防御 | AuthorizationGuard 强制 `studentId = userId`coord §B4场景 A 抛 `ForbiddenResourceError`,场景 B 抛 `IdentityMismatchError`president §2.7DEV_MODE 放行president §2.9 方案 D |
| P3 降级模式 | 方案 Bpresident §2.6`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 |
| P3 Redis 缓存 | 5-30s 短缓存coord §B6TTL ±20% 随机抖动防雪崩CacheService 封装 get/set/invalidate/invalidateByPrefix |
| P3 ESM 相对 import 路径深度 | 嵌套目录(如 `src/student/events/`、`src/student/guards/`)引用 shared 层需 `../../shared/` 前缀(非 `../shared/`NodeNext + .js 后缀 |
| P5 GraphQL Subscription SSE | GraphQL Yoga 原生 SSE 传输AsyncGenerator 桥接 gRPC server-streamingchunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable |
| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream需手动转换为 AsyncIterablefinally 块调用 `stream.destroy?.()` 清理 |
| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayServiceDI 注入fetch POST + AbortSignal.timeout(3000),失败软处理(仅 warn 日志,不抛异常) |
| P5 Kafka 事件订阅 | coord §B7 裁决P2-P4 不订阅P5 起订阅 7 个 topic消费者组 `student-bff-event-subscriber`Redis SETNX `event_id` 幂等去重 |
| P6 熔断器设计 | opossum 库,每下游服务一个独立 CircuitBreaker 实例Map 缓存action 函数闭包捕获动态参数;状态变更监听同步 Prometheus 指标student_bff_circuit_state Gauge |
| P6 熔断器配置 | timeout 5000mserrorThresholdPercentage 50resetTimeout 30000volumeThreshold 10rollingCountTimeout 60000coord §G11 |
| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据config/mock-data.tscallStream 产出单个 mock chunk 后结束 |
| /readyz 探针 | 检查 6 个下游服务可达性iam/classes/core-edu/content/msg/ai/data-ana必需失败返回 503可选软失败返回 200 + degraded=true |
| ActionState 信封 | coord §G8 裁决:成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`ok()/fail()/degraded() 工具函数 |
| GraphQL Resolver 测试 Mock | vi.mock 模拟 env/metrics/loggervi.stubGlobal 模拟 fetchmockContext 工厂函数注入 downstream/cache mock 对象Vitest 覆盖率 ≥ 80% |
| prom-client getSingleMetric 类型 | `getSingleMetric()` 返回 `Metric<string>` 联合类型,需 `as Counter<string>`/`Histogram<string>`/`Gauge<string>` 断言后调用 `.inc()`/`.observe()`/`.set()` |
| opossum v8 Status 类型 | `CircuitBreaker.Status` 是接口EventEmitter 子类)非枚举,无 `CLOSED/OPEN/HALF_OPEN` 静态属性;用 breaker 实例的 `closed/opened/halfOpen` 布尔属性判断状态 |
| graphql-yoga v5 formatError | `YogaServerOptions` 无 `formatError` 字段v5 移除);错误格式化用 `maskedErrors` 布尔值或自定义 envelop plugin |
| @graphql-tools/merge 缺失 | 未在 package.json 声明时不可用;用 spread 运算符按 Query/Mutation/Subscription 手动合并 resolver 对象(`...(x.Query ?? {})` |
| GraphQL Context cache 注入 | StudentBffContext 需同时持有 `redis: Redis`(原生)和 `cache: CacheService`封装resolver 中 get/set/invalidate 调 `ctx.cache`,不直接调 `ctx.redis` |
### 2.15 student-portal学生端微前端 RemoteP3
> student-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 student-portal **特有**的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P3 端口分配 | student-portal = **4001**004 §1.2 强制 4 端 4000-4003ai07 初稿误用 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` HookRTT 计算 `offset = (t1 + rtt/2) - serverTime` + 5min 重同步周期;`CountdownTimer` 用 `role="timer"` + `aria-live="polite"` 朗读剩余时间 |
| P3 考试作答状态机 | NotStarted→InProgress→Paused切屏警告→AutoSubmitting倒计时归零→Submitting→SubmittedZustand L3 + localStorage 持久化草稿,自动保存每 30s + blur 时保存 |
| P3 考试页面 CSP iframe 防嵌套 | 考试路由 `frame-ancestors 'none'` + `X-Frame-Options: DENY`;其他路由允许 Shell 同源嵌入 |
| P3 考试 chunk 性能预算 | 考试作答 chunk < 200KB动态 import 隔离);总 Bundle < 300KBLCP < 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` 等) |
### 2.16 admin-portal管理端微前端 RemoteP2-P6
> admin-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / 设计令牌,本节仅记录 admin-portal **特有**的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P2 端口分配 | admin-portal = **4003**004 §1.2 强制 4 端 4000-4003初稿误用 3003 与 data-ana 冲突,修订对齐仲裁 ARB-003 |
| P2 GraphQL admin 命名空间 | 复用 teacher-bff GraphQL endpoint`POST /api/admin/graphql`),不新建 admin-bffARB-001urql client standalone 自建MF 模式复用 Shell 单例 |
| P2 权限点 ADMIN_ 前缀 | admin 自身资源用 `ADMIN_` 前缀(如 `ADMIN_DASHBOARD_VIEW`/`ADMIN_AUDIT_READ`),跨服务资源保留原前缀(如 `IAM_USER_READ`);路由权限映射在 `lib/permissions.ts` ROUTE_PERMISSIONS |
| P2 MSW mock 全量拦截 | `NEXT_PUBLIC_API_MOCKING=enabled` 时 MSW 拦截所有 GraphQL/HTTP`mocks/handlers.ts` 实现 18 个 GraphQL operation + `/api/auth/login` mockfixtures 内存可变副本支持增删改 |
| P2 WebSocket 实时通知 | 接入 push-gateway `GET /ws`ARB-006开发期 `use-websocket.ts` 用定时器模拟推送30s/条3 类通知:`audit_alert`/`abnormal_login`/`system_error` |
| P2 MF Remote 入口 | `next.config.js` 暴露 `./AdminApp`exposes: `./src/app/admin-app.tsx``NEXT_PUBLIC_MF_ENABLED` 控制启用standalone 默认 falseShell 就绪后设 true |
| P3 用户管理 CRUD | `useUsers`/`useUser`/`useCreateUser`/`useUpdateUser`/`useToggleUserStatus` hooks`UserManagementTable` + `UserFormModal` 组件mock 登录 admin@edu.test/admin123 |
| P3 角色权限矩阵 | `RolePermissionMatrix` 组件按 resource 分组、支持全选/半选/单选;`useUpdateRolePermissions` 提交权限码列表;系统预置角色不可删除 |
| P3 视口拖拽排序 | `@dnd-kit/sortable` 实现拖拽排序 + `useSortable` hook`SortableViewportItem` 每行可编辑 label/permission/visible保存时批量调用 `updateViewport` |
| P4 组织树递归加载 | `OrganizationTree` 递归组件 + `useOrganization(parentId)` 按需加载子节点;展开/折叠状态本地管理A11y `role="tree"`/`role="treeitem"`/`aria-expanded`/`aria-selected` |
| P5 审计日志 CSV 导出 | `exportAuditLogsCsv` 生成 BOM + UTF-8 CSV`useAuditLogs` 支持 search/action 过滤;审计日志经 teacher-bff GraphQL 消费ARB-005 不直连 Kafka |
| P5 仪表盘服务健康 | `AdminDashboardViewModel.serviceHealth` 含 serviceName/status/latencyMs`Badge` 组件映射 healthy/degraded/down 状态色 |
| P6 A11y WCAG 2.2 AA | `eslint-plugin-jsx-a11y` flat configESLint 9skip-link + focus-visible + aria-* 完整;`@next/eslint-plugin-next` 14.x 与 ESLint 9 不兼容,暂用 jsx-a11y + tseslint 替代 |
| P6 Web Vitals 采集 | `web-vitals` 包LCP/CLS/FCP/INP/TTFB`WebVitalsInitializer` 组件仅 production 启用;`navigator.sendBeacon` 上报到 `/api/admin/web-vitals` |
| P6 健康检查端点 | `/api/health`liveness 200+ `/api/ready`readiness 检查 gateway 可达性2s 超时mock 模式直接 ready |
| P6 Dockerfile no-push | 多阶段构建 `node:22-alpine`builder 阶段 `pnpm install --frozen-lockfile` + `next build`runner 阶段 `standalone` 输出 + EXPOSE 4003 |
| P6 urql 类型断言 | urql `useQuery`/`useMutation` 泛型与纯字符串 query 兼容性有限;`use-graphql.ts` 用 `as unknown as` 从 unknown 转换(项目规则允许从 unknown 断言) |
| P6 学校组织管理子路由 | `app/admin/school/layout.tsx` 子导航 tab + `usePathname` 活动态判定;`page.tsx` 用 `next/navigation` 的 `redirect()` 跳转 `/schools`5 子页复用 `useSchools`/`useGrades`/`useGradeInsights`/`useDepartments`/`useAcademicYears` hooks |
| P6 `\|\|` 与 `??` 混用 | TS5076`a \|\| b ?? c` 非法,必须加括号 `a \|\| (b ?? c)``noUncheckedIndexedAccess` 下 `arr[0]?.id ?? null` 常见) |
| P6 mock 字面量类型拓宽 | `mocks/handlers.ts` 中对象字面量含 `any` 兄弟属性时,`status: "draft"` 被推断为 `string``unshift` 到强类型数组报错;用 `as const` 断言字面量 |