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.设计规格文档
This commit is contained in:
709
services/classes/docs/02-architecture-design.md
Normal file
709
services/classes/docs/02-architecture-design.md
Normal file
@@ -0,0 +1,709 @@
|
||||
# 模块架构设计文档 — classes
|
||||
|
||||
> AI:ai07(TS / 教学组织 · 黄金模板)
|
||||
> 阶段:阶段 2 交付物
|
||||
> 日期:2026-07-10
|
||||
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features](../../../docs/architecture/roadmap/pending-features.md)、[项目规则](../../../.trae/rules/project_rules.md)
|
||||
> 状态:待 coord 交叉审查
|
||||
>
|
||||
> **设计原则**:classes 是黄金模板,本架构设计同时服务于两个目标:
|
||||
>
|
||||
> 1. **当前目标**:D2 教学组织域班级 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
|
||||
> 2. **长远目标**:为 P3 合并入 core-edu 做无痕准备 + 为其余 8 个 TS 服务输出对照标准 + 为未来事件驱动 / gRPC / DataScope / 缓存 / 多租户 / 软删除等演进预留接口
|
||||
>
|
||||
> **架构通用标准对齐**:本设计遵循 IEEE/ThoughtWorks/Stitch 架构评估维度——功能正确性、可维护性、可测试性、可扩展性、可观测性、安全性、可部署性、容错性,每个设计决策均标注其服务的标准维度。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [模块内部分层图](#1-模块内部分层图)
|
||||
2. [领域模型](#2-领域模型)
|
||||
3. [数据模型](#3-数据模型)
|
||||
4. [API 设计](#4-api-设计)
|
||||
5. [事件设计](#5-事件设计)
|
||||
6. [横切关注点对齐清单](#6-横切关注点对齐清单)
|
||||
7. [与其他模块的交互点(契约清单)](#7-与其他模块的交互点契约清单)
|
||||
8. [风险与假设](#8-风险与假设)
|
||||
9. [长远架构与未来铺垫](#9-长远架构与未来铺垫)
|
||||
10. [黄金模板对照 checklist(供其他 TS 服务自检)](#10-黄金模板对照-checklist供其他-ts-服务自检)
|
||||
11. [整改清单与里程碑](#11-整改清单与里程碑)
|
||||
12. [AI 身份标注](#12-ai-身份标注)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块内部分层图
|
||||
|
||||
### 1.1 调用链总览
|
||||
|
||||
```mermaid
|
||||
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](../src/classes/classes.controller.ts) | HTTP 入口、Zod 校验、权限声明、响应信封封装 | → Service |
|
||||
| Service | [classes.service.ts](../src/classes/classes.service.ts) | 领域编排、业务规则校验(空 body、存在性检查)、ID 生成 | → Repository |
|
||||
| Repository | [classes.repository.ts](../src/classes/classes.repository.ts) | Drizzle ORM 数据访问、参数化查询 | → DB |
|
||||
| Middleware | [auth.middleware.ts](../src/middleware/auth.middleware.ts) / [permission.guard.ts](../src/middleware/permission.guard.ts) | 身份解析(读 header)、权限校验(APP_GUARD 全局) | 横切 |
|
||||
| Shared | shared/observability、shared/errors、shared/health、shared/lifecycle | 可观测三支柱、错误体系、健康检查、优雅关闭 | 横切 |
|
||||
| Config | [env.ts](../src/config/env.ts) / [database.ts](../src/config/database.ts) | Zod 环境校验、Drizzle 连接池 | 基础设施 |
|
||||
|
||||
> **依赖方向单向**:Controller → Service → Repository → DB,禁止反向依赖(project_rules §3.2)。
|
||||
|
||||
### 1.3 中间件 / Guard / Filter 拦截顺序
|
||||
|
||||
```mermaid
|
||||
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 合并后承载更多业务预留扩展点。
|
||||
|
||||
```mermaid
|
||||
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](../src/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 结构
|
||||
|
||||
```typescript
|
||||
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](../../../packages/shared-proto/proto/events.proto) 第 15-24 行):
|
||||
|
||||
```protobuf
|
||||
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 落地)
|
||||
|
||||
```mermaid
|
||||
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](../src/middleware/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](../src/shared/observability/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](../src/shared/observability/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](../src/shared/lifecycle/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 | 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 已定义 `ClassService` 5 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/batch` body `{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](../src/classes/classes.controller.ts) |
|
||||
| 2 | 错误码前缀统一 `<SERVICE>_` | grep 错误码常量 | [application-error.ts](../src/shared/errors/application-error.ts) |
|
||||
| 3 | pino logger,禁止 `console.*` | grep `console.log` | [logger.ts](../src/shared/observability/logger.ts) |
|
||||
| 4 | prom-client metrics + `/metrics` 端点 | 访问 `/metrics` | [metrics.ts](../src/shared/observability/metrics.ts) |
|
||||
| 5 | OTel tracer 初始化 + 关闭 | main.ts 调用 initTracer | [tracer.ts](../src/shared/observability/tracer.ts) |
|
||||
| 6 | `/healthz` liveness | GET 200 | [health.controller.ts](../src/shared/health/health.controller.ts) |
|
||||
| 7 | `/readyz` 检查 DB 依赖 | GET 200 / 503 | 同上 |
|
||||
| 8 | 优雅关闭 SIGTERM | LifecycleService | [lifecycle.service.ts](../src/shared/lifecycle/lifecycle.service.ts) |
|
||||
| 9 | Zod 输入校验 | controller `schema.parse(body)` | [classes.dto.ts](../src/classes/classes.dto.ts) |
|
||||
| 10 | GlobalErrorFilter 注册 | app.useGlobalFilters | [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) |
|
||||
| 11 | ActionState 响应信封 | `{success, data/error}` | controller 返回类型 |
|
||||
| 12 | Dockerfile 多阶段(builder + runtime) | Dockerfile 检查 | [Dockerfile](../Dockerfile) |
|
||||
| 13 | env Zod 校验 | envSchema.safeParse | [env.ts](../src/config/env.ts) |
|
||||
| 14 | Drizzle ORM(禁止 TypeORM) | package.json 无 typeorm | [package.json](../package.json) |
|
||||
| 15 | 测试覆盖率 ≥ 80% | vitest --coverage | [vitest.config.ts](../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](../../../infra/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](../package.json) 第 33 行 | 高 |
|
||||
| 2 | `console.log` → `logger.info`(tracer.ts) | [tracer.ts](../src/shared/observability/tracer.ts) 第 20 行 | 高 |
|
||||
| 3 | `@Req()` → `@Query('gradeId')`(controller list) | [classes.controller.ts](../src/classes/classes.controller.ts) 第 49-53 行 | 高 |
|
||||
| 4 | 测试覆盖率阈值 60% → 80% | [vitest.config.ts](../vitest.config.ts) | 中 |
|
||||
| 5 | Dockerfile node:20 → node:22 | [Dockerfile](../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 服务的强制对齐基准。
|
||||
Reference in New Issue
Block a user