Files
SpecialX 2ba4250165
Some checks failed
CI Go / test (push) Has been cancelled
CI Proto / lint (push) Has been cancelled
CI Python / test (push) Has been cancelled
CI TypeScript / test (push) Has been cancelled
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
2026-07-07 23:39:37 +08:00
..

classes 班级服务P1 黄金模板)

版本1.0 日期2026-07-07 状态P1 已交付P3 合并入 core-edu 关联文档:架构影响地图


1. 模块职责

classes 服务是 P1 阶段的黄金模板服务,承担以下双重职责:

1.1 业务职责

  • 班级 CRUD创建、查询、更新、删除班级Class 聚合根)
  • 班级成员管理:学生加入/退出班级、教师分配
  • 班级查询:按年级/学校/教师维度查询班级列表

1.2 模板职责(供后续服务复制)

  • 横切关注点验证JWT 鉴权、权限校验、DataScope 过滤、OpenTelemetry 追踪、结构化日志、错误处理、参数校验、限流降级
  • CQRS 模式验证Command 写 MySQL + OutboxQuery 读 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. 架构图

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 命令流程(写路径)

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 查询流程(读路径)

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 事件发布与投影流程

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

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-003CQRS 读写分离

决策:写路径操作 MySQL 主库,读路径优先读 Redis 缓存。

理由

  • 班级列表读多写少,缓存可承接 90%+ 读流量
  • 写路径与读路径分离,可独立扩展
  • Outbox 保证缓存最终一致(事件触发缓存失效)

替代方案:直接读 MySQL - 高并发下主库压力大;双写 - 一致性难保证。

ADR-004Outbox 模式保证事件可靠发布

决策:所有领域事件通过 Outbox 表同事务写入Relay Worker 异步发布。

理由

  • 业务表与 outbox 表同事务,保证"要么都成功要么都失败"
  • Relay Worker 轮询 outbox 发布到 Kafka至少一次语义
  • 避免业务提交成功但事件丢失的问题

替代方案:直接发 Kafka - 事务提交失败但消息已发导致幻读CDC - 增加运维复杂度P4 引入。

ADR-005Zod 运行时校验

决策:所有 RPC 请求参数使用 Zod schema 校验。

理由

  • protobuf 提供编译期类型检查,但运行时仍需业务规则校验
  • Zod schema 可复用为 TypeScript 类型,单一数据源
  • NestJS Pipe 集成,校验失败返回结构化错误

ADR-006pino 结构化日志

决策:使用 pino 而非 winston 或 NestJS 默认 Logger。

理由

  • pino 性能是 winston 的 3-5 倍,异步写入不阻塞事件循环
  • JSON 结构化输出,便于 Loki 采集与查询
  • 支持 traceId/userId 上下文注入,与 OpenTelemetry 对齐

ADR-007P3 阶段合并入 core-edu

决策classes 服务在 P3 阶段合并入 core-edu 服务,作为 core-edu 的一个模块。

理由

  • classes 域与课程、作业、考试域强耦合,跨服务调用频繁
  • P1 独立服务用于验证黄金模板P3 合并以减少网络开销
  • 合并后 protobuf 契约保持不变,仅内部代码迁移
  • 合并后 outbox 共享 core-edu 事务,简化一致性保证