# 已知问题速查 > 索引式速查手册:场景 → 技术/规则映射。不写代码示例,不写流水日志,不标注 AI 身份。 > 架构规则见 [../architecture/004_architecture_impact_map.md](../architecture/004_architecture_impact_map.md) 与 [../../.trae/rules/project_rules.md](../../.trae/rules/project_rules.md) > **AI 查阅时只读自己负责模块的分区(§二 对应小节),禁止读其他 AI 模块分区**,避免被历史记录或他人方案误导。 > **经验沉淀不标注 AI 身份**:所有记录只写"场景→技术/规则"映射,不得出现 AI 标识(如 ai01/ai02/coord 等),避免新同名 AI 将前任记录误认为当前事实。模块内经验沉淀在各自服务 README。 --- ## 一、全局经验 ### 1.1 多语言 monorepo 配置 | 场景 | 技术/规则 | | ------------------------ | ------------------------------------------------------------------------------------------------------------- | | 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 | | 根 package.json scripts | 封装多语言命令入口:`pnpm dev` / `pnpm lint` / `pnpm test` / `pnpm build` | | pnpm-workspace.yaml | 仅声明 TS 包路径(packages/_、services/classes、bff/_、apps/_、scripts/_),Go/Python 不入 | | go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) | | pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) | | 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 | | tsx 执行 TS 脚本 | arch-scan 等工具脚本用 `tsx` 直接运行,无需编译 | | husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits | | .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 | | ESLint 9 flat config | P6 硬化:创建 `eslint.config.js`(flat config),lint 脚本去掉 `--ext .ts`,lint-staged 恢复 `eslint --fix` | | next lint 交互式初始化 | teacher-portal 无 `.eslintrc.json` 时 `next lint` 触发 Strict/Base 选择提示,CI 中需预置配置或改 `eslint src` | ### 1.2 Docker Compose 基础设施 | 场景 | 技术/规则 | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | 日常开发启动 | 用 `docker-compose.minimal.yml` 仅起 MySQL+Redis | | 全量启动内存不足 | 按 `profiles` 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) | | 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g | | 按阶段启用容器 | P1 仅 MySQL+Redis;P3 加 Kafka+Zookeeper;P4 加 Debezium+CH+Neo4j;P5 加 ES;P6 加 Consul+Istio | | MySQL 初始化 | `init-sql/01-init.sql` 挂载到 `/docker-entrypoint-initdb.d:ro` | | healthcheck | MySQL 用 `mysqladmin ping`,Redis 用 `redis-cli ping` | | Windows 下卷挂载 | init-sql 用绝对路径或确保相对路径正确 | | 容器名固定 | `container_name: edu-mysql` 便于服务连接配置 | | Kafka 双 listener | INSIDE (kafka:29092) 容器间互访 + OUTSIDE (localhost:9092) 主机访问,避免 Debezium 拿到 localhost metadata 后切回连不上 | | ClickHouse 远程访问 | 默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 `clickhouse/users.d/custom-users.xml` 覆盖密码+任意 IP | | Debezium Connect 镜像源 | daocloud 禁用 debezium/*,用 `quay.io/debezium/connect:2.7` 替代 | | Debezium 跨网络访问 MySQL | MySQL 容器在 edu-minimal_default 时,`docker network connect edu-full_default edu-mysql` 让 Debezium 同时可达 | | CDC 注册 connector | POST `:8083/connectors`,配置 `topic.prefix`/`database.include.list`/`schema.history.internal.kafka.topic` | ### 1.3 protobuf + buf 契约 | 场景 | 技术/规则 | | -------------------- | --------------------------------------------------------------------------- | | 契约唯一源 | `packages/shared-proto/proto/*.proto`,禁止 REST/gRPC 混用 | | breaking change 检测 | `buf breaking --against '.git#branch=main'` CI 强制,必须升版本号 | | proto 版本化 | 包名带版本 `xxx.v1`、`xxx.v2`,新旧版本共存 | | buf lint | `buf lint` 使用 STANDARD 规则集,可按需 except(如 PACKAGE_VERSION_SUFFIX) | | 三端代码生成 | `buf.gen.yaml` 配置 TS/Go/Python 插件,`pnpm proto:gen` 统一生成 | | 契约先行硬约束 | 任何服务间通信必须先改 proto,CI 检测 breaking change 阻止违规合并 | | 事件 schema 版本化 | Kafka 事件带 `schema_version` 字段,消费端按版本处理 | | 错误码定义在 proto | 错误码三端共享,格式 `{服务前缀}_{错误类型}_{资源}`(如 `IAM_PERM_USER`) | ### 1.4 NestJS 服务开发(TS) | 场景 | 技术/规则 | | --------------------- | ------------------------------------------------------------------------------------------------------ | | 服务标准结构 | `src/{main.ts,app.module.ts,config,middleware,,shared,generated}` 黄金模板复制 | | 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 | | 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 | | ApplicationError 基类 | 统一错误体系:ValidationError(400)/NotFoundError(404)/PermissionDeniedError(403)/ConflictError(409) 等 | | 响应信封 | `{ success: boolean, data?: T, error?: { code, message, details, traceId } }` 统一跨服务 | | 权限校验 | `requirePermission(perm)` 装饰器/中间件,admin 角色拥有全部权限 | | DataScope 过滤 | 6 级(all/grade_managed/class_taught/class_members/children/owned),repository 下推 WHERE 条件 | | 结构化日志 | pino + `traceId`/`spanId`/`userId`/`service` 字段,禁止 `console.*` | | Metrics endpoint | prom-client 暴露 `/metrics`(QPS/延迟/错误率) | | OpenTelemetry trace | OTel SDK 初始化,W3C TraceContext header 跨服务传播 | | 健康检查 | `/health` endpoint 返回 `{ status, service, version }` | | 优雅关闭 | SIGTERM → app.close() → shutdownTracing() → process.exit(0) | | i18n 错误码映射 | 错误码 → i18n key 映射表,后端返回 key + 参数,前端翻译 | | Drizzle ORM | 参数化查询,禁止字符串拼接 SQL | | Zod 输入校验 | controller 层 `schema.safeParse(body)`,失败抛 ValidationError | ### 1.5 Go Gateway 开发 | 场景 | 技术/规则 | | ----------------- | --------------------------------------------------------------------------------------- | | 框架 | Gin + httputil.ReverseProxy 路由转发 | | P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) | | JWT claims | `{ user_id, roles[], registered_claims }`,校验后注入 `x-user-id`/`x-user-roles` 头转发 | | 请求 ID 注入 | Gateway 生成或透传 `X-Request-ID`,全链路传递 | | 健康检查 | `GET /health` 无需鉴权,返回 `{ status, timestamp }` | | 包结构 | `internal/{config,middleware,proxy}`,P6 扩展 handler/service/repository | | error 处理 | 显式处理,禁止 `_` 忽略;`gin.AbortWithStatusJSON` 统一错误响应 | | 配置加载 | 环境变量 + 默认值(`getEnv(key, fallback)`),P6 引入 viper | | Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 | ### 1.6 可观测性(OTel + Prometheus + Loki) | 场景 | 技术/规则 | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) | | 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) | | traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 | | P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) | | Prometheus 指标 | `http_request_duration_seconds`(Histogram)+ `http_requests_total`(Counter) | | 采样策略 | P1 全量 trace,P6 引入采样率降低开销 | | 日志参数顺序 | `log.error({ err: error, userId, traceId }, "操作描述")`,错误对象字段名用 `err` | | P6 /metrics 端点 | NestJS 用 `app.getHttpAdapter().get('/metrics', handler)`,不能用 `app.get()`(会被解析为 DI 容器 get) | | P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager | | P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) | | P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 `edu-*` 前缀容器日志,避免无关日志 | | P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 | | P6 collectDefaultMetrics | prom-client 的 `collectDefaultMetrics({ register })` 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 | | P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 `.inc()`/`.observe()`,需后续补 HTTP 中间件自动埋点或业务埋点 | | P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 `@opentelemetry/auto-instrumentations`。**已补全**:NestJS 6 服务用 `getNodeAutoInstrumentations()`;Python 2 服务用 `FastAPIInstrumentor.instrument_app(app)`;Go 2 服务用 `otelgin.Middleware()` | | P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 `docker.m.daocloud.io/` 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 | | P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 | | P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 `--no-deps` + 显式指定服务名,避免重建依赖容器 | | ### 1.7 微前端 Module Federation | 场景 | 技术/规则 | | ---------------------- | ---------------------------------------------------------------------------- | | 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 | | P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation | | 视口驱动渲染 | 侧边栏由 `viewports.L1` 驱动渲染,路由由 `viewports.L2` 控制 | | 共享翻译资源包 | next-intl 微前端共享,BFF/服务返回 i18n key 不返回翻译文本 | | Module Federation 配置 | P2 起在 next.config.js 配置,P1 用单一 Next.js 应用 | ### 1.8 从旧项目迁移的通用经验 | 场景 | 技术/规则 | | --------------------- | ------------------------------------------------------------------------------------ | | React 19 乐观更新 | `useOptimistic` 替代手动 isPending,配合 `useTransition` 自动管理回滚 | | Zustand 细粒度选择器 | 单字段 selector 优于 `useShallow` 多字段包装 | | Tiptap SSR | 必须 `immediatelyRender: false` 避免 hydration mismatch | | 请求级去重 | React `cache()` 包装优于跨请求缓存(权限数据易变,不用 `unstable_cache`) | | 批量 SQL | INSERT batch `db.insert().values([...])` + UPDATE `CASE WHEN` 单次执行,禁止循环单条 | | 动态导入重 UI 库 | `xxx-inner.tsx` + `xxx.tsx` lazy wrapper(tiptap / @xyflow/react / AI SDK) | | ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 | | 递归删除 | 批量收集后代 ID + `inArray` 单次删除,禁止递归逐个 | | 统计走 SQL 聚合 | `COUNT(*)`/`SUM(CASE WHEN ...)` 替代拉全表内存循环 | | 高频子组件 React.memo | `AssignmentCard` / `StatusBadge` / `EmptyState` 等 | | 虚拟化大表 | `@tanstack/react-virtual` | | 破坏性操作独立权限 | `AUDIT_LOG_PURGE` ≠ `AUDIT_LOG_READ`,沿用旧项目规则 | | i18n key 命名 | 禁止包含 `.`,动态 key 必须包含完整嵌套路径(`t(\`type.${type}\`)`) | | i18n 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key | | 类型守卫优先 | JSON.parse 结果经 `unknown` 中转 + 守卫;禁止 `as` 断言(除非从 `unknown` 转换) | | 函数返回值显式标注 | 特别是 `Promise` | | 仅类型导入 | `import type` | ### 1.9 架构工具与验证命令 | 场景 | 命令/规则 | | ------------------- | ----------------------------------------------------------- | | arch.db 扫描 | `pnpm run arch:scan`(多语言:TS+Go+Python+Proto) | | arch.db 查询 | `pnpm run arch:query -- ` | | 查服务依赖 | `pnpm run arch:query -- deps ` | | 查 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` | --- ## 二、模块经验 ### 2.1 api-gateway(Go) | 场景 | 技术/规则 | | ----------------- | ------------------------------------------------------------------------------------------------------------- | | P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 | | P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM | | 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` | | 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 | | 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 | | 请求 ID | Gateway 生成或透传 `X-Request-ID`,`c.Set("request_id", ...)` + `c.Header(...)` | | P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) | | 健康检查 | `GET /health` 无需鉴权 | | 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` | | 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` | | 路由注册位置 | 真实路由在 `main.go` 的 `api.Group` 内注册,`internal/routing/routing.go` 若未被 main 引用即为死代码 | | 多服务路由扩展 | 新增服务代理时在 main.go 注册两组路由:`Any("/x")` + `Any("/x/*path")`,与 classes 一致 | | DEV_MODE 环境变量 | Go 不自动加载 .env,`DEV_MODE` 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 | ### 2.2 classes(TS/NestJS,P1 黄金模板) | 场景 | 技术/规则 | | ---------------- | -------------------------------------------------------------------------------------------------- | | 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 | | 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI | | 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile | | 错误处理 | `ApplicationError` 基类 + 子类,错误码 `CLASSES_*` 前缀 | | 可观测 | pino logger + prom-client metrics + OTel tracer | | 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor | | 配置 | 三层配置 + Zod 校验 env | | i18n | `ERROR_CODES` 映射表(错误码 → i18n key) | | 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) | | 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% | | Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` | | ID 生成 | `@paralleldrive/cuid2` 的 `createId()` | | 响应转换 | repository 返回 Date,service 转换为 `createdAt: number`(时间戳) | | 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README | | 健康检查依赖 | `readyz` 用 Drizzle `getDb().execute(sql\`SELECT 1\`)` 校验,不要依赖 typeorm DataSource DI | | AppModule 注册 | HealthModule 必须在 `app.module.ts` imports 数组显式声明,否则 NestFactory 不扫描 HealthController | | 增量编译陷阱 | `tsconfig.json` 显式 `"incremental": false` 覆盖 base,避免 .tsbuildinfo 导致 nest watch 不 emit | ### 2.3 iam(TS/NestJS,P2) | 场景 | 技术/规则 | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 | | RBAC | 角色/权限/角色-权限 CRUD + `getEffectivePermissions(userId)` API | | 视口配置 | 4 层模型(导航/路由/组件/数据),`role_viewports` 表 | | DataScope 解析 | 6 级数据范围,注入 JWT payload | | JWT payload | `{ userId, roles, permissions(bitmap), dataScope, exp }` | | Token TTL | access 15min / refresh 7day,refresh 用 Redis 黑名单失效 | | 权限缓存 | `getEffectivePermissions` 结果 Redis 缓存 TTL 5 分钟,角色变更主动失效 | | schema 表 | users / roles / permissions / role_permissions / role_viewports / parent_student_relations / class_subject_teachers | | ESM 模式 DI | `providers: [IamService, IamRepository]` + 构造器 `@Inject(IamRepository)` 显式注入,避免 `undefined` 运行时错误 | | Drizzle ORM API | `inArray(col, vals)` 替代不存在的 `.in()`;select 返回字段名按 schema 定义(如 `r.iam_roles` 而非 `r.roles`) | | 健康检查依赖 | `readyz` 用 `db.execute(sql\`SELECT 1\`)` 校验连接,不要依赖 typeorm DataSource(IAM 用 Drizzle,无 typeorm) | | Gateway 身份传递 | Controller 直接读 `req.headers['x-user-id']` / `x-user-roles`,不要依赖未注册的 AuthMiddleware 的 `AuthenticatedRequest` | | DEV_MODE 登录 | DEV_MODE=true 时 Gateway 接受 `Bearer dev-token` 注入固定身份,IAM 仍支持真实 JWT(HS256,P2 应改 RS256) | | P2 公开路径白名单 | Gateway `publicPaths` map 含 `/iam/register`/`/iam/login`/`/iam/refresh`,AuthMiddleware 跳过鉴权避免死锁 | | P2 视口过滤 | `getUserViewports` 按 `requiredPermission` 过滤 + `sortOrder` 字典序排序,无权限要求的视口全员可见 | | P2 JWT payload | HS256 签名含 `sub/email/roles/dataScope/type`,register 自动分配 teacher 角色(TEACHER_ROLE_ID 固定 UUID) | ### 2.4 core-edu(TS/NestJS,P3) | 场景 | 技术/规则 | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 | | Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka | | Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 | | Kafka topic | `exam.published` / `homework.graded` / `grade.recorded` | | 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 | | 批改后联动 | 批改完成 → 发 `homework.graded` 事件 → 下游消费(DataAna/Msg) | | Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) | | schema 表 | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events | | datetime 列 | Drizzle `datetime` 列需 Date 对象,HTTP 请求体里是 ISO 字符串,service 层必须 `new Date(input)` 转换否则报 `toISOString is not a function` | | Kafka 非阻塞 | 开发环境 Kafka 未启动时 `connectKafka()` 必须 try/catch 且 main 中用 `void` 调用,否则阻塞服务启动 | | 相对 import | `src//` 下文件回溯一级用 `../`,不要用 `../../`(NestJS ESM 模块) | | 身份头读取 | Controller 用 `@Req() req` 从 `req.headers['x-user-id']` 读 createdBy/gradedBy,不依赖未注册的 AuthMiddleware | | 健康检查 | health.controller 用 Drizzle `db.execute(sql\`SELECT 1\`)`,不用 typeorm DataSource | | Outbox 验证 | 业务事务同写 `core_edu_outbox` 表,Kafka 未启动时事件 status=failed,启动后会重试 | ### 2.5 content(TS/NestJS,P4) | 场景 | 技术/规则 | | -------------- | ------------------------------------------------------------------------------- | | 知识图谱 | Neo4j 查询前置依赖图(秒级返回) | | 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 | | 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 | | Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j | | API 字段名 | 请求体用 TS schema 字段名(如 `order`),非 DB 列名(如 `order_num`) | | Neo4j 不可用 | 未设置 NEO4J_URL 时 driver=null,getNeo4jSession 返回 null,业务正常落库 MySQL | | Neo4j 连接超时 | driver 配置 connectionTimeout:3000,避免 Neo4j 不可用时拖慢 HTTP 响应 | | Drizzle int | drizzle-orm/mysql-core 导出 `int()` 不是 `integer()` | | db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数,与 core-edu/classes 黄金模板对齐 | ### 2.6 data-ana(Python/FastAPI,P4) | 场景 | 技术/规则 | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | 学情诊断 | ClickHouse 宽表查询,5s 内返回 | | CDC 链路 | Debezium 监听 MySQL binlog → Kafka(`edu-cdc.next_edu_cloud.`)→ DataAna 消费写 ClickHouse | | CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 | | 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) | | 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) | | CDC 消费者实现 | `cdc_consumer.py` 用 aiokafka AIOKafkaConsumer,lifespan 启动 asyncio.create_task 后台运行 | | ClickHouse 写入 | `clickhouse_client.upsert_student_dashboard()` 用 client.insert() 写宽表,client 为 None 时降级返回 False | | Debezium 事件解析 | `before/after/source/op/ts_ms` 五字段,op=r(快照)/c(新增)/u(更新)/d(删除) | | 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件),grades 事件触发时查缓存填充宽表 class_id | | Consumer offset 重置 | `kafka-consumer-groups --reset-offsets --to-earliest --execute` 需先停消费者让 group 处于 Empty 状态 | | structlog API | 24.x 用 `make_filtering_bound_logger(level)`,旧版 `make_filtering_logger` 已废弃 | ### 2.7 messaging(TS/NestJS,P5) | 场景 | 技术/规则 | | ------------- | ------------------------------------------------------------------ | | 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 | | 通知批量化 | `createBatch(items)` 单次 INSERT,沿用旧项目 dispatcher 模式 | | 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 | | fan-out 分页 | `listByUserWithPagination(userId, page, pageSize)` 分页查询 | | ES 降级 | ES_URL 未设置时 esClient=null,safeIndex/safeSearch 跳过返回空结果 | | Push 推送降级 | PUSH_GATEWAY_URL 未设置或连接失败时 try/catch 跳过,不影响 DB 写入 | | db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数 | ### 2.8 push-gateway(Go,P5) | 场景 | 技术/规则 | | ---------------- | ---------------------------------------------------------------- | | WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需调 /internal/push | | 跨实例同步 | Redis PubSub(RedisURL 配置,预留 P6 实现) | | 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 | | 并发写修复 | send chan + 单写协程模式,避免 gorilla/websocket 并发写竞争 | | DEV_MODE 鉴权 | DEV_MODE=true 时接受 dev-token,生产环境必须 JWT 校验 | | 广播端点 | POST /internal/broadcast,body {event, data},调用 hub.Broadcast | ### 2.9 ai-gateway(Python/FastAPI,P5) | 场景 | 技术/规则 | | ----------------- | --------------------------------------------------------------- | | LLM Provider 适配 | OpenAI 兼容 REST API(httpx 异步),不引入 openai SDK | | 降级模式 | API key 为空或调用失败时返回骨架响应,标记 degraded: true | | 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 | | 路由前缀 | 业务路由加 /ai 前缀(APIRouter prefix="/ai"),Gateway 代理 /ai | | dev_mode tracer | dev_mode=true 时跳过 OTel exporter 初始化 | ### 2.10 shared-proto(契约包) | 场景 | 技术/规则 | | ------------ | ------------------------------------------------------------------------------- | | 目录结构 | `proto/*.proto` + `buf.yaml` + `buf.gen.yaml` | | P1 契约 | 仅 `classes.proto`,`iam.proto`/`core_edu.proto` 占位 | | 代码生成输出 | TS → `shared-ts/generated`,Go → `shared-go/`,Python → `services/*/generated/` | ### 2.11 arch-scan(多语言扫描器) | 场景 | 技术/规则 | | -------------- | ----------------------------------------------------------------------------------------------- | | TS 扫描 | regex 提取(function/class/interface/UPPER_CASE const),避免 ts-morph 对未安装依赖文件解析失败 | | Go 扫描 | 正则提取(行首锚定 `^func`/`^type`),P2 起替换为 tree-sitter-go AST | | Python 扫描 | 正则提取(行首锚定 `^def`/`^class`),P4 起替换为 tree-sitter-python AST | | arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 | | 全量扫描 | 先清空旧数据再扫描,避免残留 | | 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 | | 扫描后验证 | `npm run arch:query -- violations` 输出 0 为合格 | | Windows 路径 | 自定义 ESLint 规则加载用 `path.join`,不用 `path.posix.join` | ### 2.12 teacher-portal(微前端宿主,P1 测试页) | 场景 | 技术/规则 | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 | | API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` | | P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 | | P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal | | P2 路由组 + AppShell | `app/(app)/layout.tsx` 用 AppShell 包裹受保护页;`/login` 与 `/` 不套壳 | | P2 真实 JWT | 登录后 token 存 localStorage,`authHeaders()` 读 `Bearer ${getToken()}` | | P2 视口驱动侧边栏 | AppShell fetch `/teacher/viewports` 渲染左侧导航,active 路由高亮 | | P2 根路径重定向 | `app/page.tsx` 客户端组件 `router.replace(isAuthenticated() ? '/dashboard' : '/login')` | | fetch headers 类型 | `authHeaders(): Record` 显式标注,避免 `{}` 与 `HeadersInit` 不兼容 | | P2 前端权限校验等价物 | `usePermission().hasPermission()` Hook + `` 组件,镜像后端 `@RequirePermission()` 装饰器,禁止 `role === "xxx"` 硬编码(§3.1) | | P2 MF Shell+Remote 架构 | teacher-portal:3000 为 Shell,student:3001/parent:3002/admin:3003 为 Remote,共享登录态/布局/组件库/权限体系 | | 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 5 层状态管理 | nuqs(URL 同步)/ TanStack Query(服务端缓存)/ Zustand(客户端业务状态)/ Zustand UI(UI 临时态)/ react-hook-form(表单) | | P2 4 端错误码前缀对齐 | TP_=teacher-bff / SP_=student-bff / PP_=parent-bff / AP_=admin-bff,前端按前缀路由 i18n key | | P2 ErrorBoundary | 路由级 `` 包裹避免白屏,搭配 `` 流式加载 |