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

128 KiB
Raw Permalink Blame History

已知问题速查

索引式速查手册:场景 → 技术/规则映射。不写代码示例,不写流水日志,不标注 AI 身份。 架构规则见 ../architecture/004_architecture_impact_map.md../../.trae/rules/project_rules.md AI 查阅时只读自己负责模块的分区(§二 对应小节),禁止读其他 AI 模块分区,避免被历史记录或他人方案误导。 经验沉淀不标注 AI 身份:所有记录只写"场景→技术/规则"映射,不得出现 AI 标识(如 ai01/ai02/coord 等),避免新同名 AI 将前任记录误认为当前事实。模块内经验沉淀在各自服务 README。


一、全局经验

1.1 多语言 monorepo 配置

场景 技术/规则
多语言 workspace pnpm 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.jsflat configlint 脚本去掉 --ext .tslint-staged 恢复 eslint --fix
next lint 交互式初始化 teacher-portal 无 .eslintrc.jsonnext lint 触发 Strict/Base 选择提示CI 中需预置配置或改 eslint src
uv.lock 解析失败 uv.lockgrpcio-health-checkingsource 字段且匹配多包,导致 uv run pytest 全 workspace Python 服务不可用,需重建 lockuv lock
pnpm 11 allowBuilds pnpm 11 默认拒绝 build scriptspostinstall需在 pnpm-workspace.yamlallowBuilds: { pkg: true } 对象格式声明白名单(非 package.jsonpnpm.onlyBuiltDependencies,已废弃)
pnpm 11 独立安装 Dockerfile 仅 COPY package.json(无 pnpm-workspace.yamlpnpm 11 无法读取 allowBuilds,需加 --ignore-scripts 跳过 build scripts@nestjs/core/protobufjs 的 postinstall 非功能必需)
pnpm 11 无 lockfile 安装 服务目录无独立 pnpm-lock.yamlDockerfile 用 --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 pingRedis 用 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.v1xxx.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 暴露 /metricsQPS/延迟/错误率)
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_secondsHistogram+ http_requests_totalCounter
采样策略 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_PURGEAUDIT_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/failedcoord 自身前后不一致时以正式审查报告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-Idreq-<uuid-v4> 格式),c.Set("request_id", ...) + c.Header(...)
限流 per-IP 令牌桶(sync.Map),超限 429 GW_RATE_LIMITEDP6 迁 Redis 支持多副本
熔断 gobreaker v2 共享 downstream 实例5xx > 50% 触发 OPEN返回 503 GW_CIRCUIT_OPEN + Retry-After
错误码 统一 GW_ 前缀W1响应体 ActionState 信封 {success,error:{code,message}}W2
结构化日志 标准库 log/slogW3slog.Info/slog.Error 带 key-value未接入 shared-go/loggerzap待评估
健康检查 GET /healthzliveness无鉴权+ GET /readyzreadiness并行 ping 9 下游 /healthz软失败规则W4
Metrics 7 个业务指标promautoGET /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.goapi.Group 内注册,internal/routing/routing.go 若未被 main 引用即为死代码
多服务路由扩展 新增服务代理时用 registerProxy(api, prefix, targetURL) helper 注册两组路由,统一管理
DEV_MODE 环境变量 Go 不自动加载 .envDEV_MODE 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒
go.work workspace 解析 go.workuse ./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/cuid2createId()
响应转换 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 5mindata_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_guardiansunique(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_outboxtopic 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/iampnpm --filter devpackages/shared-proto/proto/,回退 ../../packages/shared-proto/proto/ 与本地 proto/Docker
Drizzle ORM API inArray(col, vals) 替代不存在的 .in()delete().where() 不支持链式多次 whereand(eq(...), eq(...)) 组合
Drizzle MySQL insert MySqlInsertBase.ignore() 方法PostgreSQL 才有),改用 SELECT-then-INSERT 手动存在性检查避免唯一键冲突
Docker 多阶段构建 builder 阶段先 WORKDIR /app/packages/shared-ts && pnpm buildWORKDIR /app/services/iam && pnpm buildruntime 阶段需 COPY --from=builder /app/packages/shared-ts/dist 复制预编译产物
Husky prepare 脚本移除 ENV HUSKY=0 无效pnpm install 仍会执行 preparenode -e "delete p.scripts.prepare" 在 install 前删除根 package.json 的 prepare 脚本
TOTP 表 schema iam_user_totpuser_id UNIQUE+ iam_totp_backup_codesuser_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 正则字符类 [...] 内的 [ ] / 无需转义,`/[!@#$%^&*()_+-=[]{};':"\
Drizzle DataScope 类型 mysqlEnum 推断类型为字面量联合Repository 方法参数须用 DataScope 类型而非 string,否则 TS 报错
ioredis 类型 import { Redis } from "ioredis"named importtype 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.recordedISSUE-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() reqreq.headers['x-user-id'] 读 createdBy/gradedBy不依赖未注册的 AuthMiddleware
健康检查 health.controller 用 Drizzle db.execute(sql\SELECT 1`)`,不用 typeorm DataSource
Outbox 验证 业务事务同写 core_edu_outboxKafka 未启动时事件 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 EXRedis 不可用时降级到 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 → Kafkaedu-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/DELETEALTER 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=true4 错误码 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.pyimport xxx_pb2flat放入 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-slimdocker tag 为标准名
ruff per-file-ignores 自动生成的 proto 代码(src/generated_proto/*.py)在 pyproject.tomlper-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-degradedai10 设计)
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>.v1coord 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 Truereturn not xfor 循环改 all() 表达式
PowerShell 不支持 && RunCommand 用 cwd 参数指定工作目录,不用 cd ... && uv run

2.10 shared-proto契约包

场景 技术/规则
目录结构 proto/*.proto + buf.yaml + buf.gen.yaml
P1 契约 classes.protoiam.proto/core_edu.proto 占位
代码生成输出 TS → shared-ts/generatedGo → shared-go/Python → services/*/generated/

2.11 arch-scan多语言扫描器

场景 技术/规则
TS 扫描 regex 提取function/class/interface/UPPER_CASE const避免 ts-morph 对未安装依赖文件解析失败
Go 扫描 正则提取(行首锚定 ^func/^typeP2 起替换为 tree-sitter-go AST
Python 扫描 正则提取(行首锚定 ^def/^classP4 起替换为 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 存 localStorageauthHeaders()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 起对接 GraphQLurql/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-bffcoord 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 用 localStorageaccessToken + 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 或 apollocoord 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 回调参数隐式 anyimport 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: trueReact.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.tsliveness+ app/api/ready/route.tsreadinessexport 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 = 4002004 §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 sliceselectedChildId+ 切换后 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 APIparent-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_guardiansai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试
P4 BFF 错误码前缀 BFF_PARENT_=parent-bffcoord 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
Mock 客户端 markAsRead 类型 MOCK_NOTIFICATIONS[parentId] 可能 undefinedif (!list) continuelist[idx] 可能 undefinedconst 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 Yogacoord §B1 裁决P2 起直接 GraphQL禁止 REST 渐进schema 存放 packages/shared-ts/contracts/graphql/student-bff.schema.graphqlpresident §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 = userIdcoord §B4场景 A 抛 ForbiddenResourceError,场景 B 抛 IdentityMismatchErrorpresident §2.7DEV_MODE 放行president §2.9 方案 D
P3 降级模式 方案 Bpresident §2.6success=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-subscriberRedis 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 YogaServerOptionsformatError 字段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 = 4001004 §1.2 强制 4 端 4000-4003ai07 初稿误用 3001 与 iam 冲突ai14 修订)
P3 MF Remote 角色 next.config.js 配 @module-federation/nextjs-mfname: student_appport 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 重同步周期;CountdownTimerrole="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 = 4003004 §1.2 强制 4 端 4000-4003初稿误用 3003 与 data-ana 冲突,修订对齐仲裁 ARB-003
P2 GraphQL admin 命名空间 复用 teacher-bff GraphQL endpointPOST /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/HTTPmocks/handlers.ts 实现 18 个 GraphQL operation + /api/auth/login mockfixtures 内存可变副本支持增删改
P2 WebSocket 实时通知 接入 push-gateway GET /wsARB-006开发期 use-websocket.ts 用定时器模拟推送30s/条3 类通知:audit_alert/abnormal_login/system_error
P2 MF Remote 入口 next.config.js 暴露 ./AdminAppexposes: ./src/app/admin-app.tsxNEXT_PUBLIC_MF_ENABLED 控制启用standalone 默认 falseShell 就绪后设 true
P3 用户管理 CRUD useUsers/useUser/useCreateUser/useUpdateUser/useToggleUserStatus hooksUserManagementTable + UserFormModal 组件mock 登录 admin@edu.test/admin123
P3 角色权限矩阵 RolePermissionMatrix 组件按 resource 分组、支持全选/半选/单选;useUpdateRolePermissions 提交权限码列表;系统预置角色不可删除
P3 视口拖拽排序 @dnd-kit/sortable 实现拖拽排序 + useSortable hookSortableViewportItem 每行可编辑 label/permission/visible保存时批量调用 updateViewport
P4 组织树递归加载 OrganizationTree 递归组件 + useOrganization(parentId) 按需加载子节点;展开/折叠状态本地管理A11y role="tree"/role="treeitem"/aria-expanded/aria-selected
P5 审计日志 CSV 导出 exportAuditLogsCsv 生成 BOM + UTF-8 CSVuseAuditLogs 支持 search/action 过滤;审计日志经 teacher-bff GraphQL 消费ARB-005 不直连 Kafka
P5 仪表盘服务健康 AdminDashboardViewModel.serviceHealth 含 serviceName/status/latencyMsBadge 组件映射 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-vitalsLCP/CLS/FCP/INP/TTFBWebVitalsInitializer 组件仅 production 启用;navigator.sendBeacon 上报到 /api/admin/web-vitals
P6 健康检查端点 /api/healthliveness 200+ /api/readyreadiness 检查 gateway 可达性2s 超时mock 模式直接 ready
P6 Dockerfile no-push 多阶段构建 node:22-alpinebuilder 阶段 pnpm install --frozen-lockfile + next buildrunner 阶段 standalone 输出 + EXPOSE 4003
P6 urql 类型断言 urql useQuery/useMutation 泛型与纯字符串 query 兼容性有限;use-graphql.tsas unknown as 从 unknown 转换(项目规则允许从 unknown 断言)
P6 学校组织管理子路由 app/admin/school/layout.tsx 子导航 tab + usePathname 活动态判定;page.tsxnext/navigationredirect() 跳转 /schools5 子页复用 useSchools/useGrades/useGradeInsights/useDepartments/useAcademicYears hooks
P6 ||?? 混用 TS5076a || b ?? c 非法,必须加括号 a || (b ?? c)noUncheckedIndexedAccessarr[0]?.id ?? null 常见)
P6 mock 字面量类型拓宽 mocks/handlers.ts 中对象字面量含 any 兄弟属性时,status: "draft" 被推断为 stringunshift 到强类型数组报错;用 as const 断言字面量