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

710 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — classes
> AIai07TS / 教学组织 · 黄金模板)
> 阶段:阶段 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 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_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 模式。
| 事件 | 触发时机 | 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.proto``ClassEvent`(见 [events.proto](../../../packages/shared-proto/proto/events.proto) 第 15-24 行):
```protobuf
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 落地)
```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 幂等性设计
- **ProducerOutbox Relay**Kafka `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](../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_PERMISSIONSproject_memory 已记录该约束)。
### 6.2 错误码清单
见 [§4.3](#43-错误响应actionstate-失败信封)。前缀 `CLASSES_`004 §11.4 确认保留P3 合并入 core-edu 后保留历史遗留前缀)。
### 6.3 Logger
| 项 | 配置 |
| --------- | ------------------------------------------------------------- |
| 库 | pino 9 + pino-prettydev |
| 初始化 | [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 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](../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 | HTTPP2/ gRPCP3 | `GET /classes``ClassService.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-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 层保持薄,业务逻辑在 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_total`Outbox 积压告警)
- 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](../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 里程碑
| 里程碑 | 交付物 | 验收标准 |
| ------------- | ----------------------- | ------------------------------------------------- |
| 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 服务的强制对齐基准。