# 模块架构设计文档 — 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 服务的强制对齐基准。
