1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
43 KiB
模块架构设计文档 — classes
AI:ai07(TS / 教学组织 · 黄金模板) 阶段:阶段 2 交付物 日期:2026-07-10 关联:阶段 1 理解确认书、004 架构影响地图、pending-features、项目规则 状态:待 coord 交叉审查
设计原则:classes 是黄金模板,本架构设计同时服务于两个目标:
- 当前目标:D2 教学组织域班级 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
- 长远目标:为 P3 合并入 core-edu 做无痕准备 + 为其余 8 个 TS 服务输出对照标准 + 为未来事件驱动 / gRPC / DataScope / 缓存 / 多租户 / 软删除等演进预留接口
架构通用标准对齐:本设计遵循 IEEE/ThoughtWorks/Stitch 架构评估维度——功能正确性、可维护性、可测试性、可扩展性、可观测性、安全性、可部署性、容错性,每个设计决策均标注其服务的标准维度。
目录
- 模块内部分层图
- 领域模型
- 数据模型
- API 设计
- 事件设计
- 横切关注点对齐清单
- 与其他模块的交互点(契约清单)
- 风险与假设
- 长远架构与未来铺垫
- 黄金模板对照 checklist(供其他 TS 服务自检)
- 整改清单与里程碑
- AI 身份标注
1. 模块内部分层图
1.1 调用链总览
flowchart TB
subgraph Client["客户端 / Gateway / BFF"]
Req[HTTP 请求<br/>带 x-user-id / x-user-roles 头]
end
subgraph NestJS["classes 服务(NestJS)"]
direction TB
MW[AuthMiddleware<br/>⚠️ 已定义未注册,P3 接入]
Guard[PermissionGuard<br/>APP_GUARD 全局守卫]
Filter[GlobalErrorFilter<br/>全局异常过滤器]
subgraph Controllers["Controller 层"]
ClassCtl[ClassesController<br/>/classes CRUD]
HealthCtl[HealthController<br/>/healthz /readyz]
end
subgraph Services["Application Service 层"]
ClassSvc[ClassesService<br/>班级领域编排 + 业务校验]
end
subgraph Repo["Repository 层"]
ClassRepo[ClassesRepository<br/>Drizzle 查询]
end
subgraph Shared["Shared 横切"]
Logger[pino logger]
Metrics[prom-client registry]
Tracer[OTel SDK]
Errors[ApplicationError 体系]
end
subgraph Outbox["Outbox 模块(P3 补齐)"]
OutboxTbl[(classes_outbox 表)]
Relay[OutboxRelayWorker<br/>轮询 + Kafka 投递]
end
subgraph Infra["基础设施"]
Db[(MySQL<br/>classes_db)]
Redis[(Redis<br/>P3 班级列表缓存)]
Kafka[(Kafka<br/>edu.org.class.* topic)]
end
end
Req --> Guard
Guard --> ClassCtl
ClassCtl --> ClassSvc
ClassSvc --> ClassRepo
ClassRepo --> Db
ClassSvc -.P3.-> OutboxTbl
OutboxTbl --> Relay
Relay --> Kafka
Filter -.捕获异常.-> ClassCtl
1.2 分层职责
| 层 | 文件 | 职责 | 依赖方向 |
|---|---|---|---|
| Controller | classes.controller.ts | HTTP 入口、Zod 校验、权限声明、响应信封封装 | → Service |
| Service | classes.service.ts | 领域编排、业务规则校验(空 body、存在性检查)、ID 生成 | → Repository |
| Repository | classes.repository.ts | Drizzle ORM 数据访问、参数化查询 | → DB |
| Middleware | auth.middleware.ts / permission.guard.ts | 身份解析(读 header)、权限校验(APP_GUARD 全局) | 横切 |
| Shared | shared/observability、shared/errors、shared/health、shared/lifecycle | 可观测三支柱、错误体系、健康检查、优雅关闭 | 横切 |
| Config | env.ts / database.ts | Zod 环境校验、Drizzle 连接池 | 基础设施 |
依赖方向单向:Controller → Service → Repository → DB,禁止反向依赖(project_rules §3.2)。
1.3 中间件 / Guard / Filter 拦截顺序
sequenceDiagram
participant GW as API Gateway
participant MW as AuthMiddleware
participant Guard as PermissionGuard
participant Ctl as Controller
participant Svc as Service
participant Repo as Repository
participant DB as MySQL
participant Filter as GlobalErrorFilter
GW->>MW: HTTP + x-user-id/roles 头
Note over MW: 当前未注册,Guard 直接读 header
MW->>Guard: req.userId / req.userRoles
Guard->>Guard: DEV_MODE=true 跳过<br/>否则按 ROLE_PERMISSIONS 校验
Guard->>Ctl: 通过 / 抛 PermissionDeniedError
Ctl->>Ctl: Zod schema.parse(body)
Ctl->>Svc: 调用 Service
Svc->>Repo: 调用 Repository
Repo->>DB: Drizzle 查询
DB-->>Repo: 结果
Repo-->>Svc: Class / undefined
Svc-->>Ctl: Class / 抛 NotFoundError
Ctl-->>GW: {success:true, data}
Note over Filter: 任意层抛错被 GlobalErrorFilter 捕获
Filter-->>GW: {success:false, error:{code,message,traceId}}
2. 领域模型
2.1 聚合根与实体
classes 当前是轻量领域模型(CRUD 为主,无复杂业务规则),但为 P3 合并后承载更多业务预留扩展点。
graph LR
subgraph ClassAggregate["Class 聚合根"]
Class[Class<br/>id, name, gradeId,<br/>headTeacherId, description]
ClassStatus[ClassStatus<br/>值对象: active/archived/graduated]
end
subgraph 未来扩展["P3+ 合并后扩展(core-edu 承载)"]
Subject[Subject 聚合<br/>学科]
Enrollment[Enrollment 聚合<br/>选课/分班]
StudentRoster[StudentRoster<br/>值对象: 班级花名册]
end
Class -.归属.-> Subject
Class -.包含.-> StudentRoster
StudentRoster -.关联.-> Enrollment
| 聚合根 | 字段 | 业务不变量(当前 + 预留) |
|---|---|---|
| Class | id, name, gradeId, headTeacherId, description, createdAt, updatedAt | 当前:name 非空、gradeId 必填、headTeacherId 可空;预留:status 状态机(active→archived→graduated)、capacity 上限、schoolId 多租户 |
2.2 聚合间通信
- 当前:classes 仅一个聚合,无聚合间通信
- P3 合并后:Class 与 Subject、Enrollment 同属 core-edu,同服务内直接调用(非跨服务事件)
- 跨服务:Class 变更通过 Kafka 事件通知 data-ana / msg(见 §5)
2.3 领域服务(预留)
当前无独立领域服务(ClassesService 兼任应用服务 + 领域编排)。P3 合并后建议拆分:
| 服务 | 职责 | 触发时机 |
|---|---|---|
| ClassDomainService | 班级状态机转换、容量校验、班主任变更合法性 | P3 业务规则复杂化时 |
3. 数据模型
3.1 表结构(当前)
classes 表(见 classes.schema.ts):
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | char(36) | PK, NOT NULL | 当前 uuid v4,待迁移 cuid2 |
| name | varchar(100) | NOT NULL | 班级名称 |
| grade_id | char(36) | NOT NULL | 年级外键(年级表归属待定) |
| head_teacher_id | char(36) | NULL | 班主任外键(iam user) |
| description | text | NULL | 描述 |
| created_at | timestamp | NOT NULL DEFAULT NOW() | 创建时间 |
| updated_at | timestamp | NOT NULL DEFAULT NOW() ON UPDATE NOW() | 更新时间 |
3.2 索引策略
| 索引 | 字段 | 类型 | 用途 |
|---|---|---|---|
| PRIMARY | id | 主键 | 单条查询、更新、删除 |
| idx_classes_grade_id | grade_id | 普通索引 | 按年级过滤(list 方法高频查询) |
| idx_classes_head_teacher_id | head_teacher_id | 普通索引(预留) | P3 教师查询所属班级(高频,BFF 聚合) |
当前缺失:grade_id 与 head_teacher_id 索引未在 schema 中声明(Drizzle schema 未显式定义索引),需在迁移脚本中补齐。
3.3 读写分离策略
| 场景 | 路径 | 说明 |
|---|---|---|
| 写(create/update/delete) | MySQL 主库(Drizzle) | 当前唯一写路径 |
| 读(list/getById) | MySQL 主库(Drizzle) | 当前唯一读路径 |
| 读(P3 起,高频列表) | Redis 缓存(TTL 5 分钟) | 班级列表低频变更,适合缓存;事件驱动失效(ClassCreated/Updated/Deleted) |
| 读(P4 起,分析聚合) | ClickHouse 宽表(data-ana 投影) | 班级维度统计走宽表,不走主库 |
3.4 数据模型演进路线(长远铺垫)
| 阶段 | 演进项 | schema 变更 | 兼容策略 |
|---|---|---|---|
| P3 | 软删除 | 新增 deleted_at timestamp NULL |
查询加 WHERE deleted_at IS NULL,Drizzle 封装软删除 filter |
| P3 | 状态机 | 新增 status enum('active','archived','graduated') |
默认 active,状态转换由 ClassDomainService 校验 |
| P3 | 多租户预留 | 新增 school_id char(36) |
为 SaaS 化预留,当前单租户填默认值 |
| P3 | 容量限制 | 新增 capacity int + current_count int |
分班时校验,current_count 由 enrollment 聚合维护 |
| P3+ | cuid2 迁移 | id 类型 char(36)→varchar(30) | 数据迁移脚本 + 双写过渡期 |
| P6 | 审计字段 | 新增 created_by / updated_by |
从 x-user-id 头注入 |
4. API 设计
4.1 REST API 端点(已实现)
| Method | Path | 权限 | 请求 | 响应 | HTTP 状态 | 说明 |
|---|---|---|---|---|---|---|
| POST | /classes |
CLASSES_CREATE |
{name, gradeId, headTeacherId?, description?} |
{success, data: ClassResponse} |
201 / 400 / 403 | 创建 |
| GET | /classes |
CLASSES_READ |
?gradeId=<uuid> |
{success, data: ClassResponse[]} |
200 / 403 | 列表,可选年级过滤 |
| GET | /classes/:id |
CLASSES_READ |
path: id |
{success, data: ClassResponse} |
200 / 404 / 403 | 单条 |
| PUT | /classes/:id |
CLASSES_UPDATE |
path:id + {name?, headTeacherId?, description?} |
{success, data: ClassResponse} |
200 / 400 / 404 / 403 | 更新(空 body 抛 ValidationError) |
| DELETE | /classes/:id |
CLASSES_DELETE |
path: id |
{success: true} |
200 / 404 / 403 | 删除(先校验存在) |
4.2 ClassResponse 结构
interface ClassResponse {
id: string;
name: string;
gradeId: string;
headTeacherId: string | null;
description: string | null;
createdAt: number; // epoch ms(Drizzle timestamp → getTime())
updatedAt: number;
}
4.3 错误响应(ActionState 失败信封)
| 错误码 | 触发条件 | HTTP 状态 |
|---|---|---|
CLASSES_VALIDATION_ERROR |
Zod 校验失败 / 空 update body | 400 |
CLASSES_NOT_FOUND |
getById/update/delete 找不到资源 | 404 |
CLASSES_PERMISSION_DENIED |
PermissionGuard 校验失败 | 403 |
CLASSES_CONFLICT |
并发冲突(预留,当前未触发) | 409 |
CLASSES_BUSINESS_ERROR |
业务规则违反(预留) | 422 |
CLASSES_DATABASE_ERROR |
DB 操作失败 | 500 |
CLASSES_INTERNAL_ERROR |
未预期异常 | 500 |
4.4 API 演进路线
| 阶段 | 演进项 | 兼容策略 |
|---|---|---|
| P3 | 分页 | proto 已定义 page_size / page_token;REST 落地 ?pageSize=20&pageToken=<cursor>,游标分页 |
| P3 | 软删除 | DELETE 改为软删除(写 deleted_at),新增 ?includeDeleted=true 管理端查询 |
| P3 | 批量查询 | 新增 POST /classes/batch(body: {ids: string[]}),供 BFF DataLoader 批量去重 |
| P3 | DataScope 注入 | list 方法根据 x-user-datascope 头注入 WHERE(见 §9.4) |
| P3 | gRPC 启用 | proto ClassService 5 RPC 落地,REST 与 gRPC 并存过渡期 |
| P4 | 字段投影 | 支持 ?fields=id,name 减少响应体积(GraphQL 替代后废弃) |
5. 事件设计
5.1 我发布的领域事件(P3 由 core-edu 接管实现)
当前状态:未实现。
shared/outbox/目录缺失,P3 合并后由 core-edu 统一补齐 Outbox 模式。
| 事件 | 触发时机 | Topic(004 §7.2) | 消费者 | 消费者动作 |
|---|---|---|---|---|
ClassCreated |
create 成功后 | edu.org.class.created |
data-ana | 建班级维度宽表行 |
ClassUpdated |
update 成功后 | edu.org.class.updated |
data-ana、msg | 更新宽表;班主任变更通知新旧班主任 |
ClassDeleted |
delete 成功后 | edu.org.class.deleted |
data-ana、core-edu | 删除宽表行;关联检查(考试/作业引用) |
ClassTransferred |
headTeacherId 变更 | edu.org.class.transferred |
msg | 通知新/旧班主任 |
5.2 事件 message 契约
使用 events.proto 的 ClassEvent(见 events.proto 第 15-24 行):
message ClassEvent {
string event_id = 1; // 幂等去重 ID(cuid2)
string aggregate_id = 2; // = class_id
string event_type = 3; // ClassCreated / ClassUpdated / ClassDeleted / ClassTransferred
int64 occurred_at = 4; // 事件发生时间(epoch ms)
string class_id = 5;
string name = 6;
string action = 7; // created / updated / deleted / transferred
map<string, string> metadata = 8; // 扩展字段:旧班主任 ID、变更字段列表等
}
5.3 Outbox 模式实现(P3 设计,供 core-edu 落地)
flowchart LR
Cmd[ClassService.create] --> Repo[Repository.write]
Repo --> BizT[(classes 表)]
Repo --> OutT[(classes_outbox 表<br/>同事务)]
OutT --> Relay[OutboxRelayWorker<br/>每 100ms 轮询]
Relay --> Kafka[(Kafka<br/>edu.org.class.* topic)]
Kafka --> Proj[data-ana Projection<br/>更新宽表]
Kafka --> MsgC[msg 消费者<br/>通知投递]
Outbox 表结构(P3 补齐 shared/outbox/):
| 字段 | 类型 | 说明 |
|---|---|---|
| event_id | varchar(30) PK | cuid2,幂等去重 |
| aggregate_id | varchar(30) | class_id |
| event_type | varchar(50) | ClassCreated 等 |
| payload | json | ClassEvent 序列化 |
| topic | varchar(100) | edu.org.class.created |
| status | enum('pending','published') | 投递状态 |
| created_at | timestamp | 入库时间 |
| published_at | timestamp NULL | 投递成功时间 |
5.4 我消费的外部事件(P3+)
| 事件 | 来源 | Topic | 消费动作 |
|---|---|---|---|
UserDeleted |
iam | edu.identity.user.deleted |
若 deleted user 是班主任,置空 headTeacherId(最终一致) |
UserUpdated |
iam | edu.identity.user.updated |
班主任姓名变更无需同步(classes 仅存 id) |
5.5 幂等性设计
- Producer(Outbox Relay):Kafka
idempotent=true+transactionalId=classes-outbox-relay - Consumer:基于
event_id去重(DB 唯一索引idx_outbox_event_id或 Redis SETNX,TTL 7 天)
6. 横切关注点对齐清单
6.1 权限装饰器(端点 × 权限常量)
| 端点 | 权限常量 | 装饰器 |
|---|---|---|
| POST /classes | CLASSES_CREATE |
@RequirePermission(Permissions.CLASSES_CREATE) |
| GET /classes | CLASSES_READ |
@RequirePermission(Permissions.CLASSES_READ) |
| GET /classes/:id | CLASSES_READ |
@RequirePermission(Permissions.CLASSES_READ) |
| PUT /classes/:id | CLASSES_UPDATE |
@RequirePermission(Permissions.CLASSES_UPDATE) |
| DELETE /classes/:id | CLASSES_DELETE |
@RequirePermission(Permissions.CLASSES_DELETE) |
| GET /healthz | 无 | 无(白名单) |
| GET /readyz | 无 | 无(白名单) |
| GET /metrics | 无 | 无(Prometheus 抓取) |
角色-权限映射(permission.guard.ts 第 25-39 行):
| 角色 | 权限 |
|---|---|
| admin | CREATE / READ / UPDATE / DELETE |
| teacher | CREATE / READ / UPDATE |
| student | READ |
| parent | READ |
演进:P3 起由 iam 提供
getEffectivePermissions(userId)动态查询,PermissionGuard 改为调用 iam 而非硬编码 ROLE_PERMISSIONS(project_memory 已记录该约束)。
6.2 错误码清单
见 [§4.3](#43-错误响应actionstate-失败信封)。前缀 CLASSES_(004 §11.4 确认保留,P3 合并入 core-edu 后保留历史遗留前缀)。
6.3 Logger
| 项 | 配置 |
|---|---|
| 库 | pino 9 + pino-pretty(dev) |
| 初始化 | logger.ts 模块级单例 |
| base 字段 | {service: 'classes', version: '0.1.0'} |
| level | env.LOG_LEVEL(默认 info) |
| 规则 | 禁止 console.*;tracer.ts 第 20 行待整改 |
6.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 描述 |
|---|---|---|---|
classes_requests_total |
Counter | method, endpoint, status | 请求总数 |
classes_request_duration_seconds |
Histogram | method, endpoint | 请求延迟分布 |
| 默认进程指标 | Counter/Gauge/Summary | — | prom-client collectDefaultMetrics(CPU/内存/GC/事件循环) |
指标命名规范(project_rules §12):
<service>_<module>_<operation>_<unit>,classes 已遵循。
6.5 Tracer
| 项 | 配置 |
|---|---|
| SDK | @opentelemetry/sdk-node + auto-instrumentations |
| exporter | OTLP HTTP → ${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces |
| serviceName | classes |
| 初始化 | tracer.ts initTracer() 在 bootstrap 调用 |
| 关闭 | shutdownTracer() 在 SIGTERM 钩子 |
| 整改 | 第 20 行 console.log → logger.info |
6.6 健康检查
| 端点 | 检查逻辑 | 失败行为 |
|---|---|---|
/healthz |
进程存活(无依赖检查) | 200 {status:"ok", service, timestamp} |
/readyz |
DB SELECT 1 |
成功 200;失败 503 {status:"error", service, error} |
/readyz 演进:P3 起补充 Redis PING、Kafka 连接检查(project_memory 约束:/readyz 必须检查所有下游依赖)。
6.7 优雅关闭
关闭顺序(见 lifecycle.service.ts):
- NestJS
app.enableShutdownHooks()捕获 SIGTERM/SIGINT OnApplicationShutdown钩子触发closeDb()关闭 MySQL 连接池shutdownTracer()刷新 OTel span- K8s
terminationGracePeriodSeconds=60兜底
演进:P3 起增加 Kafka consumer graceful drain(消费完 inflight 消息再退出)、Redis 连接关闭。
7. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
|---|---|---|---|---|---|
| 被调用 | api-gateway | HTTP | /classes/* 路由转发 |
Gateway 反向代理 | P1 ✅ |
| 被调用 | teacher-bff | HTTP(P2)/ gRPC(P3) | GET /classes 或 ClassService.ListClasses |
BFF 聚合班级列表 | P2 |
| 调用 | MySQL | TCP | mysql2 连接池 | 数据读写 | P1 ✅ |
| 调用 | Redis | TCP(P3) | ioredis | 班级列表缓存 | P3 |
| 发布 | — | Kafka(P3) | edu.org.class.created/updated/deleted/transferred |
领域事件 | P3 |
| 消费 | iam | Kafka(P3) | edu.identity.user.deleted |
班主任置空 | P3 |
| 被调用 | core-edu | gRPC(P3) | ClassService.GetClassesByTeacher |
教师所属班级查询 | P3 |
P3 合并边界:classes 合并入 core-edu 后,"被调用"方从 classes 改为 core-edu,端口从 3001 改为 3004,gRPC 50053。proto 契约
ClassService迁移至core_edu.proto或保留classes.proto由 core-edu 实现。
8. 风险与假设
8.1 技术风险
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| uuid v4 → cuid2 迁移涉及主键变更 | 数据迁移风险 | P3 合并时统一迁移,双写过渡期 + 迁移脚本 + 回滚预案 |
| PermissionGuard 硬编码 ROLE_PERMISSIONS | 角色变更需改代码重启 | P3 改为调用 iam getEffectivePermissions 动态查询 |
| 无 Outbox 能力 | P3 事件驱动基础缺失 | P3 补齐 shared/outbox/,复用 iam shared-ts Outbox 工具包 |
| 无 DataScope 注入 | 数据越权风险 | P3 Repository 层注入 WHERE 条件(见 §9.4) |
| Dockerfile node:20 与项目规则 node:22 不一致 | 镜像基准不统一 | P1 立即升级 |
8.2 假设
- 假设 api-gateway 已注入
x-user-id/x-user-roles头(project_memory 确认) - 假设
gradeId引用的年级实体由管理端/前端维护,classes 不校验其存在性(无外键约束) - 假设 P3 合并时 classes 表结构可无损迁移至 core-edu 库
8.3 未决决策(提请 coord 仲裁)
| # | 议题 | 选项 | 我的倾向 |
|---|---|---|---|
| 1 | P3 合并后 classes 目录保留方式 | A. 删除目录 / B. 保留只读对照 / C. 改为 shared-ts 模板 | B(保留对照基准) |
| 2 | cuid2 迁移时机 | A. P1 黄金模板立即 / B. P3 合并时统一 | A(黄金模板先行) |
| 3 | proto ClassService 合并后归属 |
A. 迁移到 core_edu.proto / B. 保留 classes.proto 由 core-edu 实现 | B(保留契约稳定性) |
9. 长远架构与未来铺垫
本节是黄金模板的核心增值部分,为 classes 当前 CRUD 之外的未来演进预留接口,同时作为其他 TS 服务的"长远设计范例"。
9.1 事件驱动演进(EDA 铺垫)
当前:纯同步 CRUD,无事件。 P3 目标:Outbox + Kafka 全链路。 设计预留:
shared/outbox/目录结构标准化(outbox.repository / outbox.publisher / outbox.relay-worker),供 core-edu 直接复用- TOPIC_MAP 集中管理
事件 → topic映射,遵循 004 §7.2 命名规范 - 事件 schema 版本化:
event_type字段带v1后缀,演进时v2共存 - 幂等去重表设计标准化(event_id 唯一索引)
9.2 gRPC 演进铺垫
当前:仅 REST。 P3 目标:REST + gRPC 并存,BFF 切换 gRPC。 设计预留:
- proto 已定义
ClassService5 RPC,与 REST 端点一一对应 - Controller 层保持薄,业务逻辑在 Service,gRPC 实现可直接复用 Service
- 响应信封 ActionState 在 gRPC 侧用 metadata 传递 error(gRPC status code + details)
9.3 缓存策略铺垫
当前:无缓存。 P3 目标:Redis 班级列表缓存(TTL 5 分钟)。 设计预留:
- 依赖 ioredis 已声明
- env 已留
REDIS_URL - Repository 层封装
cachedList方法,缓存 key 设计:classes:list:gradeId={gradeId|all} - 失效策略:ClassCreated/Updated/Deleted 事件触发
DEL classes:list:*(模式删除)+ 短 TTL 兜底
9.4 DataScope 数据范围注入(安全铺垫)
当前:PermissionGuard 仅角色级校验,无数据行级过滤。 P3 目标:6 级 DataScope WHERE 注入(004 §5.3)。
设计预留:
| DataScope | WHERE 注入 | 适用角色 |
|---|---|---|
| L0 SELF | WHERE head_teacher_id = :userId |
教师(仅看自己班级) |
| L1 CLASS | WHERE id IN (SELECT class_id FROM enrollment WHERE student_id=:userId) |
学生 |
| L2 GRADE | WHERE grade_id IN (:managedGradeIds) |
年级组长 |
| L3 SCHOOL | WHERE school_id = :schoolId |
校管理员(P3 多租户后) |
| L5 ALL | 无过滤 | admin |
实现:Repository list 方法接收 dataScope 参数,动态构建 Drizzle WHERE;dataScope 从 x-user-datascope 头解析。
9.5 软删除与审计(可维护性铺垫)
当前:物理删除。 P3 目标:软删除 + 审计字段。
设计预留:
- schema 新增
deleted_at timestamp NULL - Repository 封装
withSoftDelete查询包装器,自动加WHERE deleted_at IS NULL - 新增
created_by/updated_by,从x-user-id头注入 - DELETE 端点改为写
deleted_at = NOW(),新增管理端?includeDeleted=true
9.6 多租户 SaaS 演进(可扩展性铺垫)
当前:单租户。 未来目标:SaaS 多校隔离。
设计预留:
- schema 新增
school_id(默认填单租户 ID) - Repository 所有查询注入
WHERE school_id = :currentSchoolId school_id从 JWT claim 或x-school-id头解析- 索引
idx_classes_school_id_grade_id复合索引
9.7 分页与批量查询(性能铺垫)
当前:list 全量返回。 P3 目标:游标分页 + 批量查询。
设计预留:
- proto 已定义
page_size/page_token - 游标分页:
page_token = base64(last_id),查询WHERE id > :lastId LIMIT :pageSize - 批量查询
POST /classes/batchbody{ids: string[]},供 BFF DataLoader 批量去重(防 N+1)
9.8 测试策略(可测试性铺垫)
当前:单元测试覆盖率 60%,仅 Service 层。 目标:80% 覆盖率 + 集成测试 + 契约测试。
| 测试类型 | 范围 | 工具 | 目标覆盖率 |
|---|---|---|---|
| 单元测试 | Service(mock repo) | Vitest | 80% |
| 集成测试 | Controller + Service + Repository(test DB) | Vitest + testcontainers | 关键路径 |
| 契约测试 | proto ↔ REST 一致性 | buf breaking | 100% |
| E2E | 端到端 CRUD | docker-compose | 冒烟 |
9.9 可观测性增强(可观测性铺垫)
当前:基础 metrics + tracer。 目标:结构化日志 + trace 关联 + 告警规则。
设计预留:
- 日志注入
traceId/spanId/userId(从 header 提取) - metrics 增加
classes_outbox_pending_total(Outbox 积压告警) - Grafana 面板模板化,供其他服务复用
- 告警规则:5xx 错误率 > 1% / p99 延迟 > 500ms / outbox 积压 > 100
9.10 配置管理演进(可部署性铺垫)
当前:env 环境变量(Zod 校验)。 P6 目标:配置中心热更新。
设计预留:
- 三层配置:env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul)
- 业务参数(如分页默认值、缓存 TTL)抽离为 yaml,P6 接入 Consul 热更新
10. 黄金模板对照 checklist(供其他 TS 服务自检)
本 checklist 是 classes 作为黄金模板的核心产出,供 ai01-ai10 的 TS 服务在实现前自检对齐。
10.1 目录结构
services/<service>/src/
├─ <domain>/ # 限界上下文
│ ├─ <domain>.controller.ts # Controller(HTTP/gRPC 入口)
│ ├─ <domain>.service.ts # Application Service(编排层)
│ ├─ <domain>.repository.ts # Repository(数据访问)
│ ├─ <domain>.schema.ts # Drizzle schema(表定义)
│ └─ <domain>.dto.ts # Zod schema + DTO 类型
├─ config/ # env.ts + database.ts
├─ middleware/ # auth.middleware.ts + permission.guard.ts
├─ shared/
│ ├─ errors/ # application-error.ts + global-error.filter.ts
│ ├─ health/ # health.controller.ts + health.module.ts
│ ├─ lifecycle/ # lifecycle.service.ts(优雅关闭)
│ ├─ observability/ # logger.ts + metrics.ts + tracer.ts
│ └─ outbox/ # (P3 补齐)
├─ app.module.ts
└─ main.ts
10.2 横切关注点 checklist
| # | 检查项 | 验证方式 | 参考文件 |
|---|---|---|---|
| 1 | @RequirePermission 覆盖全部 Controller 方法 |
grep @RequirePermission |
classes.controller.ts |
| 2 | 错误码前缀统一 <SERVICE>_ |
grep 错误码常量 | application-error.ts |
| 3 | pino logger,禁止 console.* |
grep console.log |
logger.ts |
| 4 | prom-client metrics + /metrics 端点 |
访问 /metrics |
metrics.ts |
| 5 | OTel tracer 初始化 + 关闭 | main.ts 调用 initTracer | tracer.ts |
| 6 | /healthz liveness |
GET 200 | health.controller.ts |
| 7 | /readyz 检查 DB 依赖 |
GET 200 / 503 | 同上 |
| 8 | 优雅关闭 SIGTERM | LifecycleService | lifecycle.service.ts |
| 9 | Zod 输入校验 | controller schema.parse(body) |
classes.dto.ts |
| 10 | GlobalErrorFilter 注册 | app.useGlobalFilters | global-error.filter.ts |
| 11 | ActionState 响应信封 | {success, data/error} |
controller 返回类型 |
| 12 | Dockerfile 多阶段(builder + runtime) | Dockerfile 检查 | Dockerfile |
| 13 | env Zod 校验 | envSchema.safeParse | env.ts |
| 14 | Drizzle ORM(禁止 TypeORM) | package.json 无 typeorm | package.json |
| 15 | 测试覆盖率 ≥ 80% | vitest --coverage | vitest.config.ts |
| 16 | ESM .js 后缀 import |
grep from "./xxx.js" |
全部源码 |
| 17 | import type 用于类型导入 |
grep import type |
全部源码 |
| 18 | 无 any / 无 as 断言(除 unknown) |
tsc + eslint | — |
10.3 复制黄金模板步骤(30 分钟可复制)
- 复制
services/classes/目录为services/<new-service>/ - 全局替换
classes→<new-service>(目录名、文件名、类名、错误码前缀、serviceName、metrics 指标名) - 修改
<domain>/为新限界上下文,替换 schema / dto / controller / service / repository - 修改
env.ts端口(按 port-allocation.md) - 修改
DockerfileEXPOSE 端口 pnpm install+pnpm lint+pnpm typecheck+pnpm test- 运行
pnpm run arch:scan更新 arch.db
11. 整改清单与里程碑
11.1 P1 立即整改(黄金模板对齐)
| # | 整改项 | 文件 | 优先级 |
|---|---|---|---|
| 1 | 移除 typeorm 冗余依赖 | package.json 第 33 行 | 高 |
| 2 | console.log → logger.info(tracer.ts) |
tracer.ts 第 20 行 | 高 |
| 3 | @Req() → @Query('gradeId')(controller list) |
classes.controller.ts 第 49-53 行 | 高 |
| 4 | 测试覆盖率阈值 60% → 80% | vitest.config.ts | 中 |
| 5 | Dockerfile node:20 → node:22 | Dockerfile | 中 |
| 6 | 补齐 grade_id / head_teacher_id 索引 | schema 迁移 | 中 |
11.2 P3 演进(合并入 core-edu 时落地)
| # | 演进项 | 说明 |
|---|---|---|
| 1 | 补齐 shared/outbox/ 目录 |
事件发布能力,复用 shared-ts Outbox 工具包 |
| 2 | cuid2 迁移 | 主键 uuid v4 → cuid2 |
| 3 | DataScope 6 级 WHERE 注入 | Repository 层动态过滤 |
| 4 | Redis 班级列表缓存 | TTL 5 分钟 + 事件驱动失效 |
| 5 | 软删除 + 审计字段 | deleted_at / created_by / updated_by |
| 6 | 分页 + 批量查询 | 游标分页 + POST /classes/batch |
| 7 | gRPC server 启用 | proto ClassService 落地,端口 50053 |
| 8 | PermissionGuard 改调 iam | 动态权限查询 |
11.3 里程碑
| 里程碑 | 交付物 | 验收标准 |
|---|---|---|
| M1(P1) | 黄金模板整改完成 | 6 项 P1 整改全部完成 + lint/typecheck/test 零错误 |
| M2(P1) | 黄金模板 checklist 产出 | 本文档 §10 + 其他 AI 自检通过 |
| M3(P3 交接) | classes 合并入 core-edu | ai08 接收,表结构 + proto + 事件能力迁移 |
12. AI 身份标注
AI Agent: ai07 (classes · 黄金模板) Branch: docs/classes-architecture-design-ai07 Coordinator: coord
本架构设计文档遵循架构通用标准(功能正确性 / 可维护性 / 可测试性 / 可扩展性 / 可观测性 / 安全性 / 可部署性 / 容错性),既实现 P1 当前目标(班级 CRUD + 黄金模板),又为 P3+ 演进(事件驱动 / gRPC / DataScope / 缓存 / 软删除 / 多租户 / SaaS)铺设接口。待 coord 交叉审查通过后,§10 黄金模板 checklist 作为其余 8 个 TS 服务的强制对齐基准。