NestJS (6 services): implement @RequirePermission decorator with SetMetadata+Reflector, register APP_GUARD globally, fix as assertions to type guards, add explicit return types, fix import type for express, fix /metrics implicit any, replace native Error with ApplicationError, remove typeorm remnants, register LifecycleService. teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward real userId to downstream, log downstream failures, migrate health controller to shared/health. Go (2 services): interface to any, doc comments, CORS dev whitelist, JWT secret fail-fast, push-gateway internal API auth, metrics and readyz endpoints, remove dead code. Python (2 services): lifespan return type, dev_mode to bool, data-ana APIRouter, ai POST body model, ClickHouse async wrapping.
217 lines
15 KiB
Markdown
217 lines
15 KiB
Markdown
# ai03 阶段 1 交付物:模块理解确认书
|
||
|
||
> AI 标识:ai03
|
||
> 负责模块:teacher-bff(P2)、core-edu(P3)
|
||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||
> 日期:2026-07-09
|
||
> 关联文档:[ai-allocation.md](./ai-allocation.md)、[004 架构影响地图](./004_architecture_impact_map.md)、[pending-features.md](./roadmap/pending-features.md)
|
||
|
||
---
|
||
|
||
## 模块理解确认书 — teacher-bff
|
||
|
||
### 1. 我在架构中的位置
|
||
|
||
- **层级**:BFF 聚合层(L4)
|
||
- **上游**:api-gateway(Go/Gin)通过 HTTP 转发请求,注入 `x-user-id` / `x-user-roles` 头
|
||
- **下游**:iam(3002)、classes(3001)、core-edu(3004);P3 后扩展 content、data-ana、ai
|
||
- **通信方式**:
|
||
- 当前:REST `fetch`(同步)
|
||
- 目标态(004 §4.1 / pending-features P2):**gRPC** 调下游业务服务 + **GraphQL Yoga** 对前端暴露 + DataLoader 防 N+1
|
||
- **端口**:3003(见 [teacher-bff env.ts](../../services/teacher-bff/src/config/env.ts))
|
||
|
||
### 2. 我的限界上下文
|
||
|
||
- **聚合职责**:教学场景域(教师 / 教导主任 / 教研组长 共用)的数据聚合、裁剪、协议转换
|
||
- **业务领域**:跨 D2 教学组织 + D3 教学核心 + D1 身份认证(只读拉取视口/权限)
|
||
- **我不负责**:
|
||
- 不持有业务状态(无 DB 写入,无 Outbox)
|
||
- 不做权限决策(依赖 Gateway JWT 校验 + 下游服务 `@RequirePermission`)
|
||
- 不直接访问任何业务服务数据库
|
||
- **复用策略**(004 §5.4):教导主任 / 教研组长复用 Teacher BFF,通过视口差异化(L1 导航扩展管理菜单,L4 DataScope 扩大到年级)
|
||
|
||
### 3. 我与外部的契约
|
||
|
||
- **消费的 proto message**:
|
||
- `iam.v1.IamService`:GetUserInfo / getEffectivePermissions(视口 + DataScope)
|
||
- `classes.v1.ClassService`:ListClasses / GetClass
|
||
- `core_edu.v1.ExamService / HomeworkService / GradeService`:全部 RPC
|
||
- **暴露的 API**(当前 REST,目标 GraphQL):
|
||
- `GET /teacher/dashboard` — 聚合 IAM 用户信息 + classes 列表
|
||
- `GET /teacher/viewports` — 拉取 IAM 视口配置(L1 导航)
|
||
- `GET /teacher/classes/:classId/exams` — 聚合 core-edu 考试列表
|
||
- `GET /teacher/classes/:classId/homework` — 聚合 core-edu 作业列表
|
||
- `GET /teacher/exams/:examId/grades` — 聚合 core-edu 成绩列表
|
||
- **错误码前缀**:BFF 自身错误用 `BAD_GATEWAY`(下游不可达);业务错误透传下游 `CORE_EDU_*` / `IAM_*` / `CLASSES_*`
|
||
- **缓存**:聚合结果 Redis 短缓存 5-30s(004 §6.3,当前未实现)
|
||
|
||
### 4. 我的技术栈
|
||
|
||
- 语言:TypeScript 5.5+(ESM 模式,相对 import 带 `.js` 后缀)
|
||
- 框架:NestJS 10
|
||
- 下游通信:当前 `fetch`(REST)→ 目标 `@grpc/grpc-js` + `@bufbuild/protobuf`
|
||
- 对前端:目标 GraphQL Yoga + DataLoader
|
||
- 缓存:Redis(待引入)
|
||
- 可观测:pino logger + prom-client metrics + OpenTelemetry tracer(已具备 [tracer.ts](../../services/teacher-bff/src/shared/observability/tracer.ts))
|
||
|
||
### 5. 我的阶段归属
|
||
|
||
- **P2 身份**:教师登录 → 获取 JWT → 访问 teacher-portal → 侧边栏按 viewports.L1 渲染 → 空白 Dashboard
|
||
- **P3 扩展**:考试/作业/成绩的 GraphQL 查询与 mutation
|
||
- **依赖上游**:P1 黄金模板 classes、P2 iam(getEffectivePermissions + 视口)
|
||
|
||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||
|
||
- [ ] 权限装饰器 `@RequirePermission`(**BFF 不做权限决策**,当前无;目标态:BFF 不加 Guard,仅校验 `x-user-id` 存在)
|
||
- [x] 错误处理:[GlobalErrorFilter](../../services/teacher-bff/src/shared/errors/global-error.filter.ts) + ApplicationError 层次
|
||
- [x] logger / metrics / tracer 三支柱(已具备)
|
||
- [x] `/healthz` 健康检查(HealthModule 已注册)
|
||
- [ ] `/readyz`(当前 HealthController 仅 liveness,无下游就绪探针)
|
||
- [x] 优雅关闭 SIGTERM(main.ts 已处理)
|
||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||
- [ ] Dockerfile 多阶段构建(需核对)
|
||
- [ ] Zod 输入验证(当前 Controller 直接透传 unknown,**未做 Zod 校验**)
|
||
- [x] GlobalErrorFilter 统一兜底
|
||
|
||
---
|
||
|
||
## 模块理解确认书 — core-edu
|
||
|
||
### 1. 我在架构中的位置
|
||
|
||
- **层级**:业务微服务层(L5)
|
||
- **上游**:teacher-bff(聚合层)、api-gateway(直接路由 `/api/v1/exams` 等)
|
||
- **下游**:MySQL(独占库)、Kafka(事件发布)、Redis(待引入,高并发提交锁)
|
||
- **通信方式**:
|
||
- 入口:当前 REST(`/exams`、`/homework`、`/grades`)
|
||
- 目标态(proto 注释):P3 起转 gRPC(`core_edu.v1.ExamService/HomeworkService/GradeService` 已定义)
|
||
- 出口:Kafka 事件(Outbox 模式发布)
|
||
- **端口**:3004(见 [core-edu env.ts](../../services/core-edu/src/config/env.ts))
|
||
|
||
### 2. 我的限界上下文
|
||
|
||
- **聚合职责**:跨 **D2 教学组织**(classes 模块,待合并)+ **D3 教学核心**(exams / homework / grades)
|
||
- **聚合根**:Exam、Homework、Grade、Class(待合并)
|
||
- **我不负责**:
|
||
- 不负责题库内容(→ content 服务)
|
||
- 不负责学情分析(→ data-ana 服务,消费 core-edu 事件)
|
||
- 不负责通知投递(→ msg 服务,消费 core-edu 事件)
|
||
- **数据自治**:独占 `core_edu` 数据库,表前缀 `core_edu_*`
|
||
|
||
### 3. 我与外部的契约
|
||
|
||
- **暴露的 gRPC 契约**([core_edu.proto](../../packages/shared-proto/proto/core_edu.proto),已定义待实现):
|
||
- `ExamService`:CreateExam / GetExam / ListExamsByClass / UpdateExam / DeleteExam
|
||
- `HomeworkService`:AssignHomework / GetHomework / ListHomeworkByClass / SubmitHomework
|
||
- `GradeService`:RecordGrade / GetGrade / ListGradesByStudent/Exam/Homework
|
||
- **发布的领域事件**([events.proto](../../packages/shared-proto/proto/events.proto) + [outbox.publisher.ts TOPIC_MAP](../../services/core-edu/src/shared/outbox/outbox.publisher.ts)):
|
||
|
||
| 事件 | Topic | 触发时机 | 消费者 |
|
||
| ------------------ | ------------------- | --------------------- | ------------- |
|
||
| exam.created | edu.exam.events | CreateExam 事务内 | msg、data-ana |
|
||
| exam.updated | edu.exam.events | UpdateExam | msg |
|
||
| exam.deleted | edu.exam.events | DeleteExam | data-ana |
|
||
| homework.assigned | edu.homework.events | AssignHomework | msg、data-ana |
|
||
| homework.submitted | edu.homework.events | SubmitHomework | data-ana、msg |
|
||
| homework.graded | edu.homework.events | **未实现**(P3 待补) | msg、data-ana |
|
||
| grade.recorded | edu.grade.events | RecordGrade | data-ana、msg |
|
||
| grade.updated | edu.grade.events | **未实现**(P3 待补) | data-ana |
|
||
| class.transferred | edu.class.events | classes 合并后 | data-ana |
|
||
|
||
- **消费的事件**:`edu.identity.user.created`(IAM,初始化教师默认班级关联,**当前未消费**,P3 待补)
|
||
- **错误码前缀**:`CORE_EDU_*`(见 [application-error.ts CoreEduErrorCode](../../services/core-edu/src/shared/errors/application-error.ts))
|
||
|
||
### 4. 我的技术栈
|
||
|
||
- 语言:TypeScript 5.5+(ESM 模式)
|
||
- 框架:NestJS 10
|
||
- ORM:Drizzle ORM(mysql2 driver,直接 `db` 导出,**与 classes 的 `getDb()` 不一致**)
|
||
- 存储:MySQL 8(独占库)、Redis(待引入)、Kafka(kafkajs,idempotent + transactionalId)
|
||
- 可观测:pino + prom-client + OTel(已具备)
|
||
|
||
### 5. 我的阶段归属
|
||
|
||
- **P3 核心教学**:考试全生命周期 + Outbox + Kafka 事件落地
|
||
- **退出标准**:教师创建考试 → 发布 → 学生作答 → 教师批改 → 事件到 Kafka → 成绩统计更新 → 全链路可观测
|
||
- **依赖上游**:P1 classes 黄金模板、P2 iam(用户身份 + 权限)
|
||
|
||
### 6. 我需要对齐的黄金模板项(对照 classes 服务)
|
||
|
||
- [x] 权限装饰器 `@RequirePermission`(exams.controller 全覆盖;需核对 homework/grades controller)
|
||
- [x] 错误码前缀 `CORE_EDU_*`(已用 [CoreEduErrorCode 枚举](../../services/core-edu/src/shared/errors/application-error.ts))
|
||
- [x] logger / metrics / tracer 三支柱
|
||
- [x] `/healthz` 健康检查
|
||
- [ ] `/readyz`(需补 DB SELECT 1 / Kafka 连接探针)
|
||
- [x] 优雅关闭 SIGTERM(main.ts 已处理 outboxPublisher.stop + disconnectKafka)
|
||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件)
|
||
- [ ] Dockerfile 多阶段构建(需核对)
|
||
- [ ] Zod 输入验证(**当前 Controller 直接接收 body,未 Zod 校验**;classes 用 zod schema)
|
||
- [x] GlobalErrorFilter 统一兜底
|
||
- [x] Outbox 模式(事务内写业务表 + outbox 表,独立 publisher 投递)
|
||
|
||
---
|
||
|
||
## §10 服务审计表 — ai03
|
||
|
||
> 对照 [黄金模板 classes 服务](../../services/classes/src/),审计已实现的两服务。状态:✅ 达标 / ⚠️ 部分 / ❌ 缺失
|
||
|
||
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
|
||
| ----------- | --------------------------------------- | --------------------------------- | ------- | -------------- | ------- | -------- | ------- | -------- | ---------- | ---------- |
|
||
| teacher-bff | ⚠️ 无(BFF 不做权限决策,依赖 Gateway) | ⚠️ 用 `BAD_GATEWAY`(无自有前缀) | ✅ pino | ✅ prom-client | ✅ OTel | ✅ | ❌ | ✅ | 0% ❌ | 待核对 |
|
||
| core-edu | ✅ `@RequirePermission(EXAM_*)` 全覆盖 | ✅ `CORE_EDU_*` | ✅ pino | ✅ prom-client | ✅ OTel | ✅ | ❌ | ✅ | 0% ❌ | 待核对 |
|
||
|
||
### 审计发现的关键差距(P3 阶段 2 设计需解决)
|
||
|
||
**teacher-bff**:
|
||
|
||
1. ❌ 通信方式:当前 REST `fetch`,目标 gRPC + GraphQL(P2 退出标准要求 GraphQL Yoga + DataLoader)
|
||
2. ❌ 无 Redis 聚合缓存(004 §6.3 要求 5-30s 短缓存)
|
||
3. ❌ 无 DataLoader(防 N+1,pending-features P2 明确要求)
|
||
4. ❌ 无 `/readyz` 下游就绪探针
|
||
5. ⚠️ env 配置用 `IamServiceUrl`/`ClassesServiceUrl`(REST URL),转 gRPC 后需改为 gRPC target
|
||
6. ⚠️ 无 Zod 输入验证
|
||
7. ⚠️ 无测试
|
||
|
||
**core-edu**:
|
||
|
||
1. ❌ 考试生命周期状态机缺失(当前仅 `draft` 初值,无 `published → in_progress → grading → graded → archived` 转换与校验)
|
||
2. ❌ 作业状态机不完整(仅 `assigned → submitted`,缺 `graded`;pending-features 要求 `HomeworkGraded` 事件)
|
||
3. ❌ 成绩录入无业务校验(不校验 exam/homework 是否存在、score 是否在 totalScore 范围内、是否重复录入)
|
||
4. ❌ 作业提交高并发优化缺失(004 §9.2 要求 Redis 分布式锁 + 排队)
|
||
5. ❌ 无 `grade.updated` / `homework.graded` 事件触发点(proto 已定义,service 未实现)
|
||
6. ❌ 未消费 IAM `user.created` 事件(初始化教师默认关联)
|
||
7. ⚠️ Drizzle `db` 直接导出 vs classes 的 `getDb()` 函数式 — **不一致**,建议统一为 `getDb()`
|
||
8. ⚠️ [kafka.ts](../../services/core-edu/src/config/kafka.ts) 用 `console.log`/`console.warn`,应改用结构化 logger
|
||
9. ⚠️ classes 模块在 core-edu 仅有 `classes.module.ts` 占位,**P3 待合并**(classes 服务代码迁入 + 删除独立 services/classes)
|
||
10. ⚠️ 入口仍为 REST,proto gRPC 契约已定义但未接入 `@grpc/grpc-js` + buf generate 代码
|
||
11. ❌ 无 Zod 输入验证(Controller 直接接收 `body: CreateExamInput`,未走 zod schema)
|
||
12. ❌ 无测试
|
||
|
||
### 跨模块契约对齐待确认项(提请 coord 交叉审查)
|
||
|
||
| 待确认项 | 我方期望 | 对方模块 | 状态 |
|
||
| ---------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||
| iam `getEffectivePermissions(userId)` 返回结构 | `{permissions, viewports, dataScope}` | iam(ai02) | ⚠️ 当前 teacher-bff 调 `/iam/viewports` 与 `/iam/me`,未定义此聚合 API 的 proto |
|
||
| iam `user.created` 事件 topic | `edu.identity.user.created` | iam(ai02) | ⚠️ 004 §7.2 定义,但 core-edu 未消费,需确认 iam 是否发布 |
|
||
| core-edu 端口 3004 | 不冲突 | 全局端口矩阵 | 待 coord 核对 |
|
||
| Kafka topic 命名 | `edu.exam.events` / `edu.homework.events` / `edu.grade.events` / `edu.class.events` | 004 §7.2 用 `edu.teaching.*` 前缀 | ⚠️ **不一致**:004 文档用 `edu.teaching.exam.published`,代码用 `edu.exam.events`,需 coord 仲裁统一 |
|
||
| data-ana 消费 core-edu 事件 | 消费 `exam.created` / `homework.submitted` / `grade.recorded` | data-ana(ai06) | 待 ai06 确认消费契约 |
|
||
| msg 消费 core-edu 事件 | 消费 `exam.created` / `homework.assigned` / `grade.recorded` 触发通知 | msg(ai05) | 待 ai05 确认消费契约 |
|
||
|
||
---
|
||
|
||
## 下一步(阶段 2 入口)
|
||
|
||
待 coord 审核本确认书通过后,ai03 进入阶段 2,按 [ai-allocation.md §5 ai03 设计重点](./ai-allocation.md#ai03--教学场景域) 产出两份模块架构设计文档:
|
||
|
||
1. **teacher-bff 模块架构设计**:GraphQL schema(Query/Mutation 按场景域组织)、DataLoader 批量去重、并行 gRPC 编排、聚合缓存 TTL、教师角色差异化视口推导
|
||
2. **core-edu 模块架构设计**:classes 黄金模板对齐、考试生命周期状态机、Outbox 事件定义、成绩计算配置化、作业提交高并发(Redis 锁 + 排队)、排课/考勤数据模型
|
||
|
||
阶段 2 设计需先解决上述 12 项差距与 6 项跨模块契约对齐。
|
||
|
||
---
|
||
|
||
**AI Agent**: ai03 (teacher-bff + core-edu)
|
||
**Coordinator**: coord-ai
|
||
**Branch**: 单仓库并行模式(直接 push main)
|