Files
Edu/services/classes/docs/02-architecture-design.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
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.设计规格文档
2026-07-10 12:58:22 +08:00

43 KiB
Raw Blame History

模块架构设计文档 — classes

AIai07TS / 教学组织 · 黄金模板) 阶段:阶段 2 交付物 日期2026-07-10 关联:阶段 1 理解确认书004 架构影响地图pending-features项目规则 状态:待 coord 交叉审查

设计原则classes 是黄金模板,本架构设计同时服务于两个目标:

  1. 当前目标D2 教学组织域班级 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
  2. 长远目标:为 P3 合并入 core-edu 做无痕准备 + 为其余 8 个 TS 服务输出对照标准 + 为未来事件驱动 / gRPC / DataScope / 缓存 / 多租户 / 软删除等演进预留接口

架构通用标准对齐:本设计遵循 IEEE/ThoughtWorks/Stitch 架构评估维度——功能正确性、可维护性、可测试性、可扩展性、可观测性、安全性、可部署性、容错性,每个设计决策均标注其服务的标准维度。


目录

  1. 模块内部分层图
  2. 领域模型
  3. 数据模型
  4. API 设计
  5. 事件设计
  6. 横切关注点对齐清单
  7. 与其他模块的交互点(契约清单)
  8. 风险与假设
  9. 长远架构与未来铺垫
  10. 黄金模板对照 checklist供其他 TS 服务自检)
  11. 整改清单与里程碑
  12. 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 NULLDrizzle 封装软删除 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 msDrizzle 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_tokenREST 落地 ?pageSize=20&pageToken=<cursor>,游标分页
P3 软删除 DELETE 改为软删除(写 deleted_at新增 ?includeDeleted=true 管理端查询
P3 批量查询 新增 POST /classes/batchbody: {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 模式。

事件 触发时机 Topic004 §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.protoClassEvent(见 events.proto 第 15-24 行):

message ClassEvent {
  string event_id = 1;        // 幂等去重 IDcuid2
  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 幂等性设计

  • ProducerOutbox RelayKafka idempotent=true + transactionalId=classes-outbox-relay
  • Consumer:基于 event_id 去重DB 唯一索引 idx_outbox_event_id 或 Redis SETNXTTL 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_PERMISSIONSproject_memory 已记录该约束)。

6.2 错误码清单

见 [§4.3](#43-错误响应actionstate-失败信封)。前缀 CLASSES_004 §11.4 确认保留P3 合并入 core-edu 后保留历史遗留前缀)。

6.3 Logger

配置
pino 9 + pino-prettydev
初始化 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 collectDefaultMetricsCPU/内存/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.loglogger.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

  1. NestJS app.enableShutdownHooks() 捕获 SIGTERM/SIGINT
  2. OnApplicationShutdown 钩子触发
  3. closeDb() 关闭 MySQL 连接池
  4. shutdownTracer() 刷新 OTel span
  5. K8s terminationGracePeriodSeconds=60 兜底

演进P3 起增加 Kafka consumer graceful drain消费完 inflight 消息再退出、Redis 连接关闭。


7. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途 阶段
被调用 api-gateway HTTP /classes/* 路由转发 Gateway 反向代理 P1
被调用 teacher-bff HTTPP2/ gRPCP3 GET /classesClassService.ListClasses BFF 聚合班级列表 P2
调用 MySQL TCP mysql2 连接池 数据读写 P1
调用 Redis TCPP3 ioredis 班级列表缓存 P3
发布 KafkaP3 edu.org.class.created/updated/deleted/transferred 领域事件 P3
消费 iam KafkaP3 edu.identity.user.deleted 班主任置空 P3
被调用 core-edu gRPCP3 ClassService.GetClassesByTeacher 教师所属班级查询 P3

P3 合并边界classes 合并入 core-edu 后,"被调用"方从 classes 改为 core-edu端口从 3001 改为 3004gRPC 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-rolesproject_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 已定义 ClassService 5 RPC与 REST 端点一一对应
  • Controller 层保持薄,业务逻辑在 ServicegRPC 实现可直接复用 Service
  • 响应信封 ActionState 在 gRPC 侧用 metadata 传递 errorgRPC 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 WHEREdataScope 从 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/batch body {ids: string[]},供 BFF DataLoader 批量去重(防 N+1

9.8 测试策略(可测试性铺垫)

当前:单元测试覆盖率 60%,仅 Service 层。 目标80% 覆盖率 + 集成测试 + 契约测试。

测试类型 范围 工具 目标覆盖率
单元测试 Servicemock repo Vitest 80%
集成测试 Controller + Service + Repositorytest 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_totalOutbox 积压告警)
  • Grafana 面板模板化,供其他服务复用
  • 告警规则5xx 错误率 > 1% / p99 延迟 > 500ms / outbox 积压 > 100

9.10 配置管理演进(可部署性铺垫)

当前env 环境变量Zod 校验)。 P6 目标:配置中心热更新。

设计预留

  • 三层配置env环境变量< yaml业务参数< 配置中心P6 Consul
  • 业务参数(如分页默认值、缓存 TTL抽离为 yamlP6 接入 Consul 热更新

10. 黄金模板对照 checklist供其他 TS 服务自检)

本 checklist 是 classes 作为黄金模板的核心产出,供 ai01-ai10 的 TS 服务在实现前自检对齐。

10.1 目录结构

services/<service>/src/
├─ <domain>/                  # 限界上下文
│  ├─ <domain>.controller.ts  # ControllerHTTP/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 分钟可复制)

  1. 复制 services/classes/ 目录为 services/<new-service>/
  2. 全局替换 classes<new-service>目录名、文件名、类名、错误码前缀、serviceName、metrics 指标名)
  3. 修改 <domain>/ 为新限界上下文,替换 schema / dto / controller / service / repository
  4. 修改 env.ts 端口(按 port-allocation.md
  5. 修改 Dockerfile EXPOSE 端口
  6. pnpm install + pnpm lint + pnpm typecheck + pnpm test
  7. 运行 pnpm run arch:scan 更新 arch.db

11. 整改清单与里程碑

11.1 P1 立即整改(黄金模板对齐)

# 整改项 文件 优先级
1 移除 typeorm 冗余依赖 package.json 第 33 行
2 console.loglogger.infotracer.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 里程碑

里程碑 交付物 验收标准
M1P1 黄金模板整改完成 6 项 P1 整改全部完成 + lint/typecheck/test 零错误
M2P1 黄金模板 checklist 产出 本文档 §10 + 其他 AI 自检通过
M3P3 交接) 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 服务的强制对齐基准。