feat(p1): complete P1 foundation stage
- monorepo: pnpm workspace + go.work + pyproject.toml + commitlint/husky - infra: docker-compose (minimal + full profiles) + init-sql + prometheus - arch-scan: multi-language scanner skeleton (TS/Go/Python/Proto) - shared-proto: buf v2 + classes.proto (ClassService CRUD contract) - api-gateway: Go/Gin + JWT HS256 auth + reverse proxy + request ID - classes: NestJS golden template (error system + observability + middleware + CRUD + tests) - teacher-portal: Next.js + paper-feel UI design system - CI/CD: 4 workflows (go/ts/py/proto) - docs: migration guide + project_rules + coding-standards + git-workflow + ui-design-system + 004 + 9 module READMEs + known-issues + spec/plan migration + roadmap
This commit is contained in:
352
docs/modules/classes/README.md
Normal file
352
docs/modules/classes/README.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# classes 班级服务(P1 黄金模板)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-07
|
||||
> 状态:P1 已交付(P3 合并入 core-edu)
|
||||
> 关联文档:[架构影响地图](../../architecture/004_architecture_impact_map.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块职责
|
||||
|
||||
classes 服务是 P1 阶段的**黄金模板服务**,承担以下双重职责:
|
||||
|
||||
### 1.1 业务职责
|
||||
- **班级 CRUD**:创建、查询、更新、删除班级(Class 聚合根)
|
||||
- **班级成员管理**:学生加入/退出班级、教师分配
|
||||
- **班级查询**:按年级/学校/教师维度查询班级列表
|
||||
|
||||
### 1.2 模板职责(供后续服务复制)
|
||||
- **横切关注点验证**:JWT 鉴权、权限校验、DataScope 过滤、OpenTelemetry 追踪、结构化日志、错误处理、参数校验、限流降级
|
||||
- **CQRS 模式验证**:Command 写 MySQL + Outbox,Query 读 Redis 缓存
|
||||
- **契约验证**:protobuf 定义 + buf 生成 + gRPC + Connect-RPC
|
||||
- **测试范式验证**:单元测试(vitest)+ 集成测试 + 契约测试 + E2E 测试
|
||||
|
||||
**P3 阶段**:classes 域将合并入 core-edu 服务,作为 core-edu 的一个聚合模块。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 类别 | 技术 | 版本 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 语言 | TypeScript | 5.5+ | 类型安全 |
|
||||
| 框架 | NestJS | 10.x | 依赖注入、模块化、装饰器 |
|
||||
| ORM | Drizzle ORM | 0.31+ | 类型安全 SQL 构建器 |
|
||||
| 数据库 | MySQL | 8.x | 写模型主库 |
|
||||
| 缓存 | Redis | 7.x | 读模型缓存、会话 |
|
||||
| 校验 | Zod | 3.23+ | 运行时类型校验 |
|
||||
| 链路追踪 | OpenTelemetry | 1.24+ | 分布式追踪 |
|
||||
| 日志 | pino | 9.x | 结构化高性能日志 |
|
||||
| 测试 | vitest | 2.x | 单元/集成测试 |
|
||||
| 契约测试 | buf | 1.40+ | protobuf 契约与生成 |
|
||||
| E2E | Playwright | 1.45+ | 端到端测试 |
|
||||
| 容器化 | Docker | - | 多阶段构建 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Client["调用方"]
|
||||
BFF[BFF 聚合层]
|
||||
end
|
||||
|
||||
subgraph Classes["classes 服务"]
|
||||
subgraph API["API 层"]
|
||||
CTRL[ClassController<br/>gRPC + Connect-RPC]
|
||||
GUARD[AuthGuard<br/>JWT metadata 校验]
|
||||
PERM[PermissionGuard<br/>requirePermission]
|
||||
SCOPE[DataScopeInterceptor<br/>6 级数据范围]
|
||||
end
|
||||
|
||||
subgraph App["Application 层"]
|
||||
CMD[CommandHandler<br/>CreateClass/UpdateClass/DeleteClass]
|
||||
QRY[QueryHandler<br/>ListClasses/GetClass]
|
||||
OUTBOX[OutboxWriter<br/>同事务写 outbox]
|
||||
end
|
||||
|
||||
subgraph Domain["Domain 层"]
|
||||
AGG[ClassAggregate<br/>聚合根 + 不变量]
|
||||
EVT[DomainEvent<br/>ClassCreated/Updated/Deleted]
|
||||
end
|
||||
|
||||
subgraph Infra["Infrastructure 层"]
|
||||
REPO[ClassRepository<br/>Drizzle ORM]
|
||||
CACHE[ClassCache<br/>Redis 读模型]
|
||||
PROJ[Projection<br/>outbox → Redis]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Store["存储"]
|
||||
MYSQL[(MySQL<br/>classes + outbox)]
|
||||
REDIS[(Redis<br/>班级缓存)]
|
||||
KAFKA[(Kafka<br/>领域事件)]
|
||||
end
|
||||
|
||||
BFF -->|gRPC| GUARD
|
||||
GUARD --> PERM
|
||||
PERM --> SCOPE
|
||||
SCOPE --> CTRL
|
||||
CTRL --> CMD
|
||||
CTRL --> QRY
|
||||
CMD --> AGG
|
||||
AGG --> EVT
|
||||
CMD --> OUTBOX
|
||||
CMD --> REPO
|
||||
QRY --> CACHE
|
||||
REPO --> MYSQL
|
||||
OUTBOX --> MYSQL
|
||||
CACHE --> REDIS
|
||||
PROJ --> MYSQL
|
||||
PROJ --> REDIS
|
||||
PROJ -->|Relay Worker| KAFKA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心流程图
|
||||
|
||||
### 4.1 CreateClass 命令流程(写路径)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant BFF as BFF
|
||||
participant CTRL as ClassController
|
||||
participant CMD as CreateClassHandler
|
||||
participant AGG as ClassAggregate
|
||||
participant REPO as ClassRepository
|
||||
participant OUTBOX as OutboxWriter
|
||||
participant DB as MySQL
|
||||
participant PROJ as Projection
|
||||
participant K as Kafka
|
||||
|
||||
BFF->>CTRL: gRPC CreateClass(req)
|
||||
CTRL->>CTRL: AuthGuard 校验 JWT metadata
|
||||
CTRL->>CTRL: PermissionGuard 校验 class:create
|
||||
CTRL->>CTRL: DataScopeInterceptor 注入 schoolId
|
||||
CTRL->>CMD: execute(CreateClassCommand)
|
||||
CMD->>AGG: ClassAggregate.create(name, gradeId, schoolId)
|
||||
AGG->>AGG: 校验不变量(名称非空、年级存在)
|
||||
AGG-->>CMD: ClassCreatedEvent
|
||||
CMD->>REPO: save(classAggregate)
|
||||
REPO->>DB: INSERT INTO classes ...
|
||||
CMD->>OUTBOX: append(ClassCreatedEvent)
|
||||
OUTBOX->>DB: INSERT INTO outbox (同事务)
|
||||
DB-->>CMD: 事务提交成功
|
||||
CMD-->>CTRL: ClassDTO
|
||||
CTRL-->>BFF: gRPC 响应
|
||||
|
||||
Note over PROJ,K: 异步投影
|
||||
PROJ->>DB: 轮询 outbox (100ms)
|
||||
PROJ->>K: 发布 edu.org.class.created
|
||||
PROJ->>DB: 标记 outbox processed
|
||||
```
|
||||
|
||||
### 4.2 ListClasses 查询流程(读路径)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant BFF as BFF
|
||||
participant CTRL as ClassController
|
||||
participant QRY as ListClassesHandler
|
||||
participant CACHE as ClassCache
|
||||
participant REPO as ClassRepository
|
||||
participant DB as MySQL
|
||||
|
||||
BFF->>CTRL: gRPC ListClasses(filter)
|
||||
CTRL->>CTRL: AuthGuard + PermissionGuard(class:read)
|
||||
CTRL->>CTRL: DataScopeInterceptor 注入 dataScope=L2/gradeId
|
||||
CTRL->>QRY: execute(ListClassesQuery)
|
||||
QRY->>CACHE: getClassesByGrade(gradeId)
|
||||
alt 缓存命中
|
||||
CACHE-->>QRY: ClassDTO[]
|
||||
else 缓存未命中
|
||||
CACHE-->>QRY: null
|
||||
QRY->>REPO: findByGrade(gradeId, dataScope)
|
||||
REPO->>DB: SELECT * FROM classes WHERE grade_id=? AND ...
|
||||
DB-->>REPO: 行数据
|
||||
REPO-->>QRY: ClassAggregate[]
|
||||
QRY->>CACHE: setClassesByGrade(gradeId, dtos, TTL=5min)
|
||||
end
|
||||
QRY-->>CTRL: ClassDTO[]
|
||||
CTRL-->>BFF: gRPC 响应
|
||||
```
|
||||
|
||||
### 4.3 事件发布与投影流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CMD[Command 处理完成] --> TX[MySQL 事务提交<br/>classes 表 + outbox 表]
|
||||
TX --> SUCC{提交成功?}
|
||||
SUCC -- 是 --> RELAY[Relay Worker 轮询<br/>每 100ms]
|
||||
SUCC -- 否 --> ROLL[回滚<br/>无事件产生]
|
||||
RELAY --> READ[读取 outbox 未处理记录]
|
||||
READ --> PUB[发布到 Kafka<br/>edu.org.class.created]
|
||||
PUB --> MARK[标记 outbox processed=true]
|
||||
MARK --> CONSUME[消费者处理]
|
||||
CONSUME --> PROJ1[Projection 更新<br/>Redis 读模型]
|
||||
CONSUME --> PROJ2[DataAna 投影<br/>ClickHouse 宽表]
|
||||
CONSUME --> PROJ3[Msg 服务<br/>通知触发]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 对外契约
|
||||
|
||||
### 5.1 gRPC 服务契约(protobuf)
|
||||
|
||||
```protobuf
|
||||
service ClassService {
|
||||
rpc CreateClass(CreateClassRequest) returns (Class);
|
||||
rpc GetClass(GetClassRequest) returns (Class);
|
||||
rpc ListClasses(ListClassesRequest) returns (ListClassesResponse);
|
||||
rpc UpdateClass(UpdateClassRequest) returns (Class);
|
||||
rpc DeleteClass(DeleteClassRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message Class {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string grade_id = 3;
|
||||
string school_id = 4;
|
||||
int32 student_count = 5;
|
||||
string homeroom_teacher_id = 6;
|
||||
int64 created_at = 7;
|
||||
int64 updated_at = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 领域事件契约
|
||||
|
||||
| 事件 | Topic | 生产者 | 消费者 | 字段 |
|
||||
|------|-------|--------|--------|------|
|
||||
| ClassCreated | `edu.org.class.created` | classes | DataAna、Msg | classId, name, gradeId, schoolId |
|
||||
| ClassUpdated | `edu.org.class.updated` | classes | DataAna、Msg | classId, changes, updatedAt |
|
||||
| ClassDeleted | `edu.org.class.deleted` | classes | DataAna、Msg | classId, deletedAt |
|
||||
|
||||
### 5.3 数据库表
|
||||
|
||||
| 表 | 用途 | 关键字段 |
|
||||
|----|------|----------|
|
||||
| classes | 班级写模型主表 | id, name, grade_id, school_id, homeroom_teacher_id, student_count, created_at, updated_at |
|
||||
| outbox | 事件 Outbox 表 | id, aggregate_id, event_type, payload, processed, created_at |
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖关系
|
||||
|
||||
### 6.1 上游依赖(调用方)
|
||||
|
||||
| 上游 | 协议 | 说明 |
|
||||
|------|------|------|
|
||||
| teacher-bff | gRPC | 教师端班级管理 |
|
||||
| student-bff | gRPC | 学生端班级查询 |
|
||||
| parent-bff | gRPC | 家长端子女班级查询 |
|
||||
|
||||
### 6.2 下游依赖(被调用方)
|
||||
|
||||
| 下游 | 协议 | 说明 |
|
||||
|------|------|------|
|
||||
| MySQL | TCP | 写模型主库(classes + outbox) |
|
||||
| Redis | TCP | 读模型缓存(5 分钟 TTL) |
|
||||
| Kafka | TCP | 领域事件发布 |
|
||||
|
||||
### 6.3 共享包依赖
|
||||
|
||||
| 包 | 用途 |
|
||||
|----|------|
|
||||
| shared-proto | ClassService protobuf 定义与生成代码 |
|
||||
| shared-ts | 通用类型、错误码、日志字段约定 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 架构约束
|
||||
|
||||
1. **黄金模板强制**:本服务的横切关注点实现必须可复制,后续服务(iam/core-edu/content 等)以本服务为模板
|
||||
2. **CQRS 强制**:写路径必须经 Command → Aggregate → Repository + Outbox;读路径必须先查缓存再查库
|
||||
3. **Outbox 强制**:所有写操作必须同事务写入 outbox 表,禁止直接发布 Kafka 消息
|
||||
4. **权限校验强制**:每个 RPC 方法必须经 PermissionGuard 校验 `class:create`/`class:read`/`class:update`/`class:delete`
|
||||
5. **DataScope 强制**:查询必须经 DataScopeInterceptor 注入数据范围,禁止全表查询
|
||||
6. **不变量保护**:ClassAggregate 必须在创建/更新时校验业务不变量(名称非空、学生数 ≤ 上限)
|
||||
7. **事务边界**:单聚合事务,禁止跨聚合事务
|
||||
8. **P3 合并约束**:合并入 core-edu 时,protobuf 契约保持不变,仅内部模块迁移
|
||||
|
||||
---
|
||||
|
||||
## 8. 架构决策
|
||||
|
||||
### ADR-001:选用 NestJS 而非 Express/Fastify
|
||||
|
||||
**决策**:业务服务统一使用 NestJS 10 框架。
|
||||
|
||||
**理由**:
|
||||
- 依赖注入容器便于横切关注点组合(Guard/Interceptor/Pipe)
|
||||
- 模块化系统天然匹配 DDD 限界上下文
|
||||
- 装饰器声明式编程,代码可读性高
|
||||
- 原生支持 gRPC、Microservices、OpenTelemetry
|
||||
|
||||
**替代方案**:Express + 手工组合中间件 - 缺乏依赖注入,横切关注点难以复用。
|
||||
|
||||
### ADR-002:选用 Drizzle ORM 而非 Prisma/TypeORM
|
||||
|
||||
**决策**:数据访问层使用 Drizzle ORM。
|
||||
|
||||
**理由**:
|
||||
- SQL-first 设计,生成的 SQL 可预测、可优化
|
||||
- 类型安全且零运行时开销(不像 Prisma 有查询引擎)
|
||||
- 支持 MySQL 事务、Outbox 模式所需的同事务多表写入
|
||||
- Schema 即代码,迁移文件可读性强
|
||||
|
||||
**替代方案**:Prisma - 查询引擎二进制大、事务控制弱;TypeORM - 装饰器模型过重。
|
||||
|
||||
### ADR-003:CQRS 读写分离
|
||||
|
||||
**决策**:写路径操作 MySQL 主库,读路径优先读 Redis 缓存。
|
||||
|
||||
**理由**:
|
||||
- 班级列表读多写少,缓存可承接 90%+ 读流量
|
||||
- 写路径与读路径分离,可独立扩展
|
||||
- Outbox 保证缓存最终一致(事件触发缓存失效)
|
||||
|
||||
**替代方案**:直接读 MySQL - 高并发下主库压力大;双写 - 一致性难保证。
|
||||
|
||||
### ADR-004:Outbox 模式保证事件可靠发布
|
||||
|
||||
**决策**:所有领域事件通过 Outbox 表同事务写入,Relay Worker 异步发布。
|
||||
|
||||
**理由**:
|
||||
- 业务表与 outbox 表同事务,保证"要么都成功要么都失败"
|
||||
- Relay Worker 轮询 outbox 发布到 Kafka,至少一次语义
|
||||
- 避免业务提交成功但事件丢失的问题
|
||||
|
||||
**替代方案**:直接发 Kafka - 事务提交失败但消息已发,导致幻读;CDC - 增加运维复杂度,P4 引入。
|
||||
|
||||
### ADR-005:Zod 运行时校验
|
||||
|
||||
**决策**:所有 RPC 请求参数使用 Zod schema 校验。
|
||||
|
||||
**理由**:
|
||||
- protobuf 提供编译期类型检查,但运行时仍需业务规则校验
|
||||
- Zod schema 可复用为 TypeScript 类型,单一数据源
|
||||
- NestJS Pipe 集成,校验失败返回结构化错误
|
||||
|
||||
### ADR-006:pino 结构化日志
|
||||
|
||||
**决策**:使用 pino 而非 winston 或 NestJS 默认 Logger。
|
||||
|
||||
**理由**:
|
||||
- pino 性能是 winston 的 3-5 倍,异步写入不阻塞事件循环
|
||||
- JSON 结构化输出,便于 Loki 采集与查询
|
||||
- 支持 traceId/userId 上下文注入,与 OpenTelemetry 对齐
|
||||
|
||||
### ADR-007:P3 阶段合并入 core-edu
|
||||
|
||||
**决策**:classes 服务在 P3 阶段合并入 core-edu 服务,作为 core-edu 的一个模块。
|
||||
|
||||
**理由**:
|
||||
- classes 域与课程、作业、考试域强耦合,跨服务调用频繁
|
||||
- P1 独立服务用于验证黄金模板,P3 合并以减少网络开销
|
||||
- 合并后 protobuf 契约保持不变,仅内部代码迁移
|
||||
- 合并后 outbox 共享 core-edu 事务,简化一致性保证
|
||||
Reference in New Issue
Block a user