# 模块架构设计文档 — core-edu

> AI 标识：ai08（按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) 负责）
> 模块：core-edu（教学核心服务）
> 阶段：架构设计外包 · 阶段 2（模块架构设计）· 待 coord 交叉审查
> 日期：2026-07-09
> 关联文档：
>
> - [阶段 1 理解确认书](./01-understanding.md)
> - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)
> - [coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md)
> - [pending-features P3](../../../docs/architecture/roadmap/pending-features.md#p3-核心教学阶段m7-m10)
> - [项目规则](../../../.trae/rules/project_rules.md)
> - [核心教学服务 README](../README.md)

---

## 0. 设计原则与长远定位

### 0.1 设计原则

1. **DDD 限界上下文清晰**：core-edu 承载 D2 教学组织 + D3 教学核心两个上下文，**聚合根间通过事件通信**，禁止跨聚合直接事务
2. **CQRS 读写分离**：写模型 MySQL 独占，读模型由 data-ana 通过 CDC 投递 ClickHouse 宽表；core-edu 只写不读 ClickHouse
3. **EDA 事件驱动**：所有跨聚合/跨服务副作用通过 Outbox + Kafka 事件触发，**禁止同步调用下游**（msg/data-ana/content）
4. **状态机显式化**：考试/作业/成绩生命周期用状态机建模，转换需校验前置状态，状态变更必须发事件
5. **幂等性优先**：所有外部入口（HTTP / gRPC / Kafka 消费）假设可能重试，业务操作必须基于业务唯一键去重
6. **配置化优于硬编码**：成绩计算公式、状态转换规则、并发限流阈值均配置化，为 P6 配置中心（Consul）热更新预留
7. **可观测性内置**：每个状态转换、每个 Outbox 投递、每个 gRPC 调用必须创建 span 与 metric

### 0.2 长远定位（P3 → P6+ 演进路径）

| 维度       | P3 现状目标                       | P4-P5 演进                            | P6+ 远期                         |
| ---------- | --------------------------------- | ------------------------------------- | -------------------------------- |
| 入口协议   | REST + gRPC 双入口                | gRPC 为主，REST 仅 BFF 兼容           | Service Mesh sidecar 接管协议    |
| 状态机     | 显式状态字段 + 转换函数           | XState 风格状态机抽象                 | Temporal 工作流托管长流程        |
| 成绩计算   | JSON 配置公式（加权/平均/自定义） | 规则引擎（durable-rules）             | DSL + 可视化编辑器               |
| 作业批改   | 教师手动批改                      | AI 辅助批改（选择题自动）             | AI 全题型批改 + 教师审核         |
| Outbox     | NestJS 进程内 relay worker        | 独立 Go 服务 `services/outbox-relay/` | Debezium 替代 Outbox（CDC 直投） |
| 读模型     | MySQL 单库                        | MySQL + ClickHouse 双轨读             | CQRS 完全分离，core-edu 仅写     |
| 多租户     | school_id 字段预留                | 行级隔离                              | 数据库级隔离                     |
| 数据归档   | 软删除 archived_at                | 冷数据迁 ClickHouse                   | 冷数据迁 S3 + 元数据 MySQL       |
| 排课冲突   | 教师时间冲突检测                  | 教室资源冲突                          | 全校资源调度算法                 |
| 微前端协作 | teacher-portal + student-portal   | + parent-portal 查看成绩              | + admin-portal 全局分析          |

---

## 1. 模块内部分层图

### 1.1 分层架构

```mermaid
graph TB
    subgraph Entry["入口层"]
        HTTP[HTTP Controller<br/>REST /exams /homework /grades /attendance]
        GRPC[gRPC Controller<br/>ExamService/HomeworkService<br/>GradeService/AttendanceService]
        KAFKA_C[Kafka Consumer<br/>IAM 事件订阅]
    end

    subgraph Guard["Guard / Filter 层"]
        AUTH[AuthMiddleware<br/>x-user-id/x-user-roles 解析]
        PERM[PermissionGuard<br/>@RequirePermission APP_GUARD]
        VAL[Zod ValidationPipe<br/>输入校验]
        ERR[GlobalErrorFilter<br/>ActionState 信封]
    end

    subgraph App["Application Service 层"]
        EXAM_SVC[ExamsService<br/>考试状态机编排]
        HW_SVC[HomeworkService<br/>作业状态机 + 并发锁]
        GRADE_SVC[GradesService<br/>成绩计算 + 幂等]
        ATT_SVC[AttendanceService<br/>考勤录入]
        COURSE_SVC[CourseService<br/>排课 + 冲突检测]
        IAM_CSM[IamUserConsumer<br/>消费 user 事件]
    end

    subgraph Domain["Domain 层（纯函数）"]
        EXAM_SM[ExamStateMachine<br/>draft→published→...]
        HW_SM[HomeworkStateMachine<br/>assigned→submitted→graded]
        GRADE_CALC[GradeCalculator<br/>加权/平均/自定义公式]
        SCHED_CHECK[ScheduleConflictChecker<br/>教师/教室/班级冲突]
        AGG[聚合根<br/>Exam/Homework/Grade/Class/Course]
    end

    subgraph Repo["Repository 层（Drizzle）"]
        EXAM_REPO[ExamsRepository]
        HW_REPO[HomeworkRepository]
        GRADE_REPO[GradesRepository]
        ATT_REPO[AttendanceRepository]
        COURSE_REPO[CourseRepository]
        OUTBOX_REPO[OutboxRepository<br/>事务内写]
        DATASCOPE[DataScopeInjector<br/>WHERE 条件注入]
    end

    subgraph Infra["基础设施层"]
        DB[(MySQL core_edu_*<br/>独占库)]
        REDIS[(Redis<br/>分布式锁 + 短缓存)]
        KAFKA_P[Kafka Producer<br/>idempotent+transactionalId]
        OUTBOX_PUB[OutboxPublisher<br/>setInterval 5s 轮询]
        TEMPORAL[Temporal Client<br/>考试发布工作流]
        OTEL[OTel SDK<br/>tracer/metrics/logger]
    end

    HTTP --> AUTH
    GRPC --> AUTH
    AUTH --> PERM
    PERM --> VAL
    VAL --> ERR
    ERR --> EXAM_SVC
    ERR --> HW_SVC
    ERR --> GRADE_SVC
    ERR --> ATT_SVC
    ERR --> COURSE_SVC
    KAFKA_C --> IAM_CSM

    EXAM_SVC --> EXAM_SM
    HW_SVC --> HW_SM
    GRADE_SVC --> GRADE_CALC
    COURSE_SVC --> SCHED_CHECK
    EXAM_SM --> AGG
    HW_SM --> AGG

    EXAM_SVC --> EXAM_REPO
    HW_SVC --> HW_REPO
    HW_SVC --> REDIS
    GRADE_SVC --> GRADE_REPO
    ATT_SVC --> ATT_REPO
    COURSE_SVC --> COURSE_REPO
    IAM_CSM --> COURSE_REPO

    EXAM_REPO --> DATASCOPE
    GRADE_REPO --> DATASCOPE
    DATASCOPE --> DB

    EXAM_SVC --> OUTBOX_REPO
    HW_SVC --> OUTBOX_REPO
    GRADE_SVC --> OUTBOX_REPO
    ATT_SVC --> OUTBOX_REPO
    OUTBOX_REPO --> DB

    OUTBOX_PUB --> KAFKA_P
    OUTBOX_PUB --> DB
    EXAM_SVC --> TEMPORAL

    EXAM_SVC --> OTEL
    HW_SVC --> OTEL
    GRADE_SVC --> OTEL
```

### 1.2 调用链标注

| 调用链                          | 拦截点                                                                                            | 说明                                                             |
| ------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| HTTP POST /exams                | Auth → PermissionGuard → ZodPipe → GlobalErrorFilter → ExamsService                               | 创建考试，事务内写 exams + outbox                                |
| gRPC ExamService.CreateExam     | 同上（gRPC 上下文适配）                                                                           | P3 启用 50053 端口                                               |
| HTTP POST /homework/:id/submit  | Auth → PermissionGuard → HomeworkService → Redis 分布式锁 → HomeworkRepository → OutboxRepository | 高并发提交，锁 key = `homework:submit:{homeworkId}:{studentId}`  |
| Kafka edu.identity.user.created | IamUserConsumer → CourseService.UpdateTeacherAssociation                                          | 初始化教师默认班级关联，幂等（基于 user_id + class_id 唯一索引） |
| OutboxPublisher.poll()          | 每 5s 轮询 pending → KafkaProducer.send → markProcessed                                           | 失败重试 5 次，超阈值 markFailed                                 |

---

## 2. 领域模型

### 2.1 聚合根与边界

```mermaid
graph LR
    subgraph D2["D2 教学组织（classes 子域）"]
        CLASS[Class 班级聚合]
        SUBJ[Subject 学科聚合]
        ENROLL[Enrollment 选课聚合]
    end

    subgraph D3a["D3 教学核心 - 考试子域"]
        EXAM[Exam 考试聚合]
        EXAM_Q[ExamQuestion 考试题项<br/>实体，归 Exam]
        EXAM_SUB[ExamSubmission 考试提交聚合]
    end

    subgraph D3b["D3 教学核心 - 作业子域"]
        HW[Homework 作业聚合]
        HW_SUB[HomeworkSubmission 作业提交聚合]
        HW_ANS[HomeworkAnswer 作业答案<br/>实体，归 Submission]
    end

    subgraph D3c["D3 教学核心 - 成绩子域"]
        GRADE[Grade 成绩聚合]
    end

    subgraph D3d["D3 教学核心 - 排课考勤子域"]
        COURSE[Course 课程聚合]
        LESSON[Lesson 课时实体<br/>归 Course]
        SCHED[Schedule 排课聚合]
        ATT[Attendance 考勤聚合]
    end

    CLASS -.1..*> EXAM
    CLASS -.1..*> HW
    CLASS -.1..*> COURSE
    EXAM -.1..1.-> EXAM_SUB
    HW -.1..1.-> HW_SUB
    EXAM -.1..*> GRADE
    HW -.1..*> GRADE
    COURSE -.1..*> SCHED
    SCHED -.1..*> ATT
```

### 2.2 聚合根职责

| 聚合根                 | 职责               | 不变式（Invariants）                                                                        | 跨聚合通信                                                    |
| ---------------------- | ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| **Exam**               | 考试生命周期管理   | status 状态机转换合法；totalScore > 0；examDate > now() 当 status=draft                     | 发 `exam.published` 事件触发 msg 通知 + data-ana 创建分析骨架 |
| **ExamSubmission**     | 学生考试作答       | 同一学生同一考试仅 1 条提交；提交时间在 [examDate, examDate+duration] 窗口内                | 发 `exam.submitted` 事件                                      |
| **Homework**           | 作业布置与生命周期 | status ∈ {assigned, submitted, graded}；dueDate > assignedAt                                | 发 `homework.assigned` 事件                                   |
| **HomeworkSubmission** | 学生作业提交与批改 | 同一学生同一作业仅 1 条提交；提交时间 ≤ dueDate + grace_period                              | 发 `homework.submitted` / `homework.graded` 事件              |
| **Grade**              | 成绩记录           | score ∈ [0, totalScore]；examId/homeworkId 二选一；同一 (studentId, examId/homeworkId) 唯一 | 发 `grade.recorded` / `grade.updated` 事件                    |
| **Class**              | 班级组织           | name 唯一；gradeId 存在                                                                     | 发 `class.transferred` 事件（合并 classes 后）                |
| **Course**             | 课程与课时编排     | 教师时间不冲突；教室不冲突；班级不冲突                                                      | 发 `schedule.conflict_detected` 事件（可选）                  |
| **Attendance**         | 考勤记录           | 同一学生同一课时仅 1 条；status ∈ {present, absent, late, leave}                            | 发 `attendance.recorded` 事件                                 |

### 2.3 值对象

| 值对象          | 字段                                                                                         | 用途                                     |
| --------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `Score`         | value: number, total: number, weight?: number                                                | 成绩值对象，封装分数与满分               |
| `TimeWindow`    | start: Date, end: Date, grace?: number                                                       | 时间窗口（考试作答/作业提交）            |
| `GradeFormula`  | type: 'weighted'\|'average'\|'custom', weights?: Record<string, number>, expression?: string | 成绩计算公式                             |
| `ConflictInfo`  | type: 'teacher'\|'room'\|'class', conflictingScheduleId: string                              | 排课冲突信息                             |
| `EventMetadata` | schemaVersion: string, traceId: string, userId: string                                       | 事件元数据，所有 outbox payload 强制携带 |

---

## 3. 数据模型

### 3.1 表 schema 清单

> 当前已实施：`core_edu_exams` / `core_edu_homework` / `core_edu_grades` / `core_edu_outbox`（4 表）
> P3 新增：`core_edu_exam_questions` / `core_edu_exam_submissions` / `core_edu_homework_submissions` / `core_edu_homework_answers` / `core_edu_courses` / `core_edu_lessons` / `core_edu_schedules` / `core_edu_attendance` / `core_edu_grade_formulas` / `core_edu_teacher_associations`（10 表）
> P3 总计 14 表

#### 3.1.1 考试域

```typescript
// core_edu_exams（已存在，需补字段）
export const exams = mysqlTable("core_edu_exams", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  classId: char("class_id", { length: 36 }).notNull(),
  subjectId: char("subject_id", { length: 36 }).notNull(), // P3 新增
  title: varchar("title", { length: 200 }).notNull(),
  description: text("description"),
  examDate: datetime("exam_date").notNull(),
  duration: int("duration").notNull(), // 改为 int（秒），原 varchar
  totalScore: decimal("total_score", { precision: 6, scale: 2 }).notNull(), // 改为 decimal
  status: varchar("status", { length: 20 }).notNull().default("draft"),
  // P3 新增：状态机审计
  statusChangedAt: datetime("status_changed_at").notNull().defaultNow(),
  statusChangedBy: char("status_changed_by", { length: 36 }),
  // P3 新增：多租户预留
  schoolId: char("school_id", { length: 36 }).notNull(),
  createdBy: char("created_by", { length: 36 }).notNull(),
  // P3 新增：软删除
  archivedAt: datetime("archived_at"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});

// core_edu_exam_questions（P3 新增）
export const examQuestions = mysqlTable("core_edu_exam_questions", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  examId: char("exam_id", { length: 36 }).notNull(),
  questionId: char("question_id", { length: 36 }).notNull(), // 引用 content 服务题库
  order: int("order").notNull(),
  score: decimal("score", { precision: 6, scale: 2 }).notNull(),
  questionType: varchar("question_type", { length: 30 }).notNull(), // choice/fill/short_answer/essay
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

// core_edu_exam_submissions（P3 新增）
export const examSubmissions = mysqlTable("core_edu_exam_submissions", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  examId: char("exam_id", { length: 36 }).notNull(),
  studentId: char("student_id", { length: 36 }).notNull(),
  status: varchar("status", { length: 20 }).notNull().default("in_progress"),
  // in_progress / submitted / graded
  submittedAt: datetime("submitted_at"),
  gradedAt: datetime("graded_at"),
  gradedBy: char("graded_by", { length: 36 }),
  totalScore: decimal("total_score", { precision: 6, scale: 2 }),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});
```

#### 3.1.2 作业域

```typescript
// core_edu_homework（已存在，需补字段）
export const homework = mysqlTable("core_edu_homework", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  classId: char("class_id", { length: 36 }).notNull(),
  subjectId: char("subject_id", { length: 36 }).notNull(), // P3 新增
  title: varchar("title", { length: 200 }).notNull(),
  description: text("description"),
  dueDate: datetime("due_date").notNull(),
  gracePeriod: int("grace_period").notNull().default(0), // P3 新增：宽限期（秒）
  status: varchar("status", { length: 20 }).notNull().default("assigned"),
  schoolId: char("school_id", { length: 36 }).notNull(), // P3 新增
  createdBy: char("created_by", { length: 36 }).notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});

// core_edu_homework_submissions（P3 新增）
export const homeworkSubmissions = mysqlTable("core_edu_homework_submissions", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  homeworkId: char("homework_id", { length: 36 }).notNull(),
  studentId: char("student_id", { length: 36 }).notNull(),
  status: varchar("status", { length: 20 }).notNull().default("draft"),
  // draft / submitted / graded
  submittedAt: datetime("submitted_at"),
  gradedAt: datetime("graded_at"),
  gradedBy: char("graded_by", { length: 36 }),
  totalScore: decimal("total_score", { precision: 6, scale: 2 }),
  feedback: text("feedback"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});

// core_edu_homework_answers（P3 新增）
export const homeworkAnswers = mysqlTable("core_edu_homework_answers", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  submissionId: char("submission_id", { length: 36 }).notNull(),
  questionId: char("question_id", { length: 36 }).notNull(),
  answer: text("answer"),
  score: decimal("score", { precision: 6, scale: 2 }),
  teacherComment: text("teacher_comment"),
  isCorrect: boolean("is_correct"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});
```

#### 3.1.3 成绩域

```typescript
// core_edu_grades（已存在，需补字段 + 唯一索引）
export const grades = mysqlTable(
  "core_edu_grades",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    studentId: char("student_id", { length: 36 }).notNull(),
    examId: char("exam_id", { length: 36 }),
    homeworkId: char("homework_id", { length: 36 }),
    score: decimal("score", { precision: 6, scale: 2 }).notNull(), // 改为 decimal
    totalScore: decimal("total_score", { precision: 6, scale: 2 }).notNull(), // P3 新增
    feedback: text("feedback"),
    gradedBy: char("graded_by", { length: 36 }).notNull(),
    schoolId: char("school_id", { length: 36 }).notNull(), // P3 新增
    // P3 新增：幂等性
    idempotencyKey: varchar("idempotency_key", { length: 128 }), // studentId + examId/homeworkId
    createdAt: timestamp("created_at").notNull().defaultNow(),
    updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
  },
  (table) => ({
    // 同一学生同一考试/作业仅 1 条成绩
    uniqueStudentExam: uniqueIndex("uniq_student_exam").on(
      table.studentId,
      table.examId,
    ),
    uniqueStudentHomework: uniqueIndex("uniq_student_hw").on(
      table.studentId,
      table.homeworkId,
    ),
    idempotencyIdx: uniqueIndex("uniq_idempotency").on(table.idempotencyKey),
  }),
);

// core_edu_grade_formulas（P3 新增，成绩计算配置化）
export const gradeFormulas = mysqlTable("core_edu_grade_formulas", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  scope: varchar("scope", { length: 20 }).notNull(), // class / subject / school
  scopeId: char("scope_id", { length: 36 }).notNull(),
  formulaType: varchar("formula_type", { length: 20 }).notNull(),
  // weighted / average / custom
  weights: json("weights"), // { "exam": 0.4, "homework": 0.3, "attendance": 0.3 }
  customExpression: text("custom_expression"), // 自定义公式表达式
  effectiveFrom: datetime("effective_from").notNull(),
  effectiveTo: datetime("effective_to"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
});
```

#### 3.1.4 排课考勤域（P3 新增）

```typescript
// core_edu_courses（P3 新增）
export const courses = mysqlTable("core_edu_courses", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  classId: char("class_id", { length: 36 }).notNull(),
  subjectId: char("subject_id", { length: 36 }).notNull(),
  teacherId: char("teacher_id", { length: 36 }).notNull(),
  schoolId: char("school_id", { length: 36 }).notNull(),
  name: varchar("name", { length: 200 }).notNull(),
  startDate: date("start_date").notNull(),
  endDate: date("end_date").notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});

// core_edu_lessons（P3 新增，课时实体归 Course）
export const lessons = mysqlTable("core_edu_lessons", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  courseId: char("course_id", { length: 36 }).notNull(),
  title: varchar("title", { length: 200 }).notNull(),
  order: int("order").notNull(),
  knowledgePointIds: json("knowledge_point_ids"), // 引用 content 服务知识点
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

// core_edu_schedules（P3 新增，排课聚合）
export const schedules = mysqlTable("core_edu_schedules", {
  id: char("id", { length: 36 }).notNull().primaryKey(),
  courseId: char("course_id", { length: 36 }).notNull(),
  lessonId: char("lesson_id", { length: 36 }),
  teacherId: char("teacher_id", { length: 36 }).notNull(),
  classId: char("class_id", { length: 36 }).notNull(),
  roomId: char("room_id", { length: 36 }), // 教室 ID（可选，pending-features 未要求）
  startTime: datetime("start_time").notNull(),
  endTime: datetime("end_time").notNull(),
  status: varchar("status", { length: 20 }).notNull().default("scheduled"),
  // scheduled / completed / cancelled
  schoolId: char("school_id", { length: 36 }).notNull(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
});

// core_edu_attendance（P3 新增）
export const attendance = mysqlTable(
  "core_edu_attendance",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    scheduleId: char("schedule_id", { length: 36 }).notNull(),
    studentId: char("student_id", { length: 36 }).notNull(),
    status: varchar("status", { length: 20 }).notNull(),
    // present / absent / late / leave
    remark: text("remark"),
    recordedBy: char("recorded_by", { length: 36 }).notNull(),
    schoolId: char("school_id", { length: 36 }).notNull(),
    createdAt: timestamp("created_at").notNull().defaultNow(),
    updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
  },
  (table) => ({
    uniqueScheduleStudent: uniqueIndex("uniq_sched_student").on(
      table.scheduleId,
      table.studentId,
    ),
  }),
);
```

#### 3.1.5 Outbox（已存在，需补字段）

```typescript
// core_edu_outbox（已存在，P3 补 event_id + schema_version + occurred_at）
export const outbox = mysqlTable(
  "core_edu_outbox",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    // P3 新增：业务事件 ID（独立于 outbox id，支持重投递幂等）
    eventId: char("event_id", { length: 36 }).notNull(),
    aggregateId: char("aggregate_id", { length: 36 }).notNull(),
    aggregateType: varchar("aggregate_type", { length: 50 }).notNull(),
    eventType: varchar("event_type", { length: 100 }).notNull(),
    // P3 新增：事件发生时间（业务时间，区别于 createdAt 系统时间）
    occurredAt: datetime("occurred_at").notNull(),
    payload: text("payload").notNull(), // JSON: { schema_version, ...业务字段, metadata: { traceId, userId } }
    status: varchar("status", { length: 20 }).notNull().default("pending"),
    retryCount: bigint("retry_count", { mode: "number" }).notNull().default(0),
    nextRetryAt: datetime("next_retry_at"), // P3 新增：指数退避
    createdAt: timestamp("created_at").notNull().defaultNow(),
    processedAt: timestamp("processed_at"),
  },
  (table) => ({
    eventIdIdx: uniqueIndex("uniq_event_id").on(table.eventId), // 幂等去重
    statusIdx: index("idx_status_next_retry").on(
      table.status,
      table.nextRetryAt,
    ),
  }),
);

// core_edu_teacher_associations（P3 新增，IAM 事件消费幂等）
export const teacherAssociations = mysqlTable(
  "core_edu_teacher_associations",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    teacherId: char("teacher_id", { length: 36 }).notNull(),
    classId: char("class_id", { length: 36 }).notNull(),
    subjectId: char("subject_id", { length: 36 }),
    schoolId: char("school_id", { length: 36 }).notNull(),
    source: varchar("source", { length: 20 }).notNull().default("iam_event"),
    // iam_event / manual / import
    createdAt: timestamp("created_at").notNull().defaultNow(),
  },
  (table) => ({
    uniqueTeacherClassSubject: uniqueIndex("uniq_teacher_class_subject").on(
      table.teacherId,
      table.classId,
      table.subjectId,
    ),
  }),
);
```

### 3.2 索引策略

| 表                            | 索引类型 | 字段                               | 用途             |
| ----------------------------- | -------- | ---------------------------------- | ---------------- |
| core_edu_exams                | 主键     | id                                 | 单条查询         |
| core_edu_exams                | 普通     | (class_id, status)                 | 按班级列出考试   |
| core_edu_exams                | 普通     | (school_id, exam_date)             | 全校考试日历     |
| core_edu_exam_submissions     | 唯一     | (exam_id, student_id)              | 幂等提交         |
| core_edu_homework_submissions | 唯一     | (homework_id, student_id)          | 幂等提交         |
| core_edu_grades               | 唯一     | (student_id, exam_id)              | 幂等录入         |
| core_edu_grades               | 唯一     | (student_id, homework_id)          | 幂等录入         |
| core_edu_grades               | 普通     | (student_id)                       | 学生成绩查询     |
| core_edu_schedules            | 普通     | (teacher_id, start_time, end_time) | 教师时间冲突检测 |
| core_edu_schedules            | 普通     | (class_id, start_time, end_time)   | 班级时间冲突检测 |
| core_edu_attendance           | 唯一     | (schedule_id, student_id)          | 幂等考勤         |
| core_edu_outbox               | 唯一     | event_id                           | 事件幂等         |
| core_edu_outbox               | 普通     | (status, next_retry_at)            | relay 轮询       |
| core_edu_teacher_associations | 唯一     | (teacher_id, class_id, subject_id) | IAM 事件幂等     |

### 3.3 读写分离策略

| 场景                     | 读源                                                                                            | 写目标         | 一致性           |
| ------------------------ | ----------------------------------------------------------------------------------------------- | -------------- | ---------------- |
| 教师查看班级考试列表     | MySQL（实时）                                                                                   | MySQL          | 强一致           |
| 教师批改单条作业         | MySQL（实时）                                                                                   | MySQL + Outbox | 强一致           |
| 学生查看自己成绩         | MySQL（实时）                                                                                   | MySQL + Outbox | 强一致           |
| 家长查看子女成绩趋势     | ClickHouse 宽表（CDC 同步，[known-issues §2.6](../../../docs/troubleshooting/known-issues.md)） | —              | 最终一致（< 5s） |
| 校管理员查看全校分析     | ClickHouse 宽表（data-ana 服务）                                                                | —              | 最终一致         |
| 教导主任查看年级成绩分布 | ClickHouse 宽表（data-ana 服务）                                                                | —              | 最终一致         |

> **双轨读策略**（[known-issues §2.6](../../../docs/troubleshooting/known-issues.md)）：刚提交的成绩查 MySQL 主库（强一致），聚合统计查 ClickHouse 宽表（延迟 1-5s 可接受）。core-edu 仅负责 MySQL 写，ClickHouse 由 data-ana 通过 CDC 自动同步。

---

## 4. API 设计

### 4.1 REST API

| method | path                             | 权限                       | 请求/响应结构                                                                    | 说明                                                                  |
| ------ | -------------------------------- | -------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| POST   | /exams                           | CORE_EDU_EXAM_CREATE       | { classId, subjectId, title, examDate, duration, totalScore } → { id }           | 创建考试（status=draft）                                              |
| GET    | /exams/:id                       | CORE_EDU_EXAM_READ         | → Exam                                                                           | 查询考试详情                                                          |
| GET    | /exams/class/:classId            | CORE_EDU_EXAM_READ         | → Exam[]                                                                         | 按班级列出考试（DataScope 注入）                                      |
| PUT    | /exams/:id                       | CORE_EDU_EXAM_UPDATE       | { title?, examDate?, ... } → { success }                                         | 更新考试（仅 draft 状态）                                             |
| DELETE | /exams/:id                       | CORE_EDU_EXAM_DELETE       | → { success }                                                                    | 删除考试（仅 draft 状态，软删除）                                     |
| POST   | /exams/:id/publish               | CORE_EDU_EXAM_PUBLISH      | → { success }                                                                    | **P3 新增** 发布考试（draft → published），触发 Temporal 工作流       |
| POST   | /exams/:id/start                 | CORE_EDU_EXAM_SUBMIT       | → { submissionId }                                                               | **P3 新增** 学生开始作答（published → in_progress）                   |
| POST   | /exams/:id/submit                | CORE_EDU_EXAM_SUBMIT       | { answers: [{ questionId, answer }] } → { submissionId }                         | **P3 新增** 提交答卷（in_progress → submitted），高并发锁             |
| POST   | /exams/:id/grade                 | CORE_EDU_EXAM_GRADE        | { submissionId, scores: [{ questionId, score }] } → { success }                  | **P3 新增** 教师批改（submitted → graded）                            |
| POST   | /exams/:id/archive               | CORE_EDU_EXAM_UPDATE       | → { success }                                                                    | **P3 新增** 归档考试（graded → archived）                             |
| POST   | /homework                        | CORE_EDU_HOMEWORK_CREATE   | { classId, subjectId, title, dueDate, gracePeriod? } → { id }                    | 布置作业（status=assigned）                                           |
| GET    | /homework/:id                    | CORE_EDU_HOMEWORK_READ     | → Homework                                                                       | 查询作业                                                              |
| GET    | /homework/class/:classId         | CORE_EDU_HOMEWORK_READ     | → Homework[]                                                                     | 按班级列出作业                                                        |
| POST   | /homework/:id/submit             | CORE_EDU_HOMEWORK_SUBMIT   | { answers: [{ questionId, answer }] } → { submissionId }                         | **P3 新增** 提交作业（assigned → submitted），高并发锁 + 完整 answers |
| POST   | /homework/:id/grade              | CORE_EDU_HOMEWORK_GRADE    | { submissionId, scores, feedback? } → { success }                                | **P3 新增** 批改作业（submitted → graded），发 homework.graded 事件   |
| POST   | /grades                          | CORE_EDU_GRADE_CREATE      | { studentId, examId?, homeworkId?, score, totalScore, idempotencyKey? } → { id } | 录入成绩（幂等）                                                      |
| GET    | /grades/:id                      | CORE_EDU_GRADE_READ        | → Grade                                                                          | 查询成绩                                                              |
| GET    | /grades/student/:studentId       | CORE_EDU_GRADE_READ        | → Grade[]                                                                        | 按学生查成绩（DataScope 注入）                                        |
| GET    | /grades/exam/:examId             | CORE_EDU_GRADE_READ        | → Grade[]                                                                        | 按考试查成绩（教师权限）                                              |
| GET    | /grades/homework/:homeworkId     | CORE_EDU_GRADE_READ        | → Grade[]                                                                        | 按作业查成绩（教师权限）                                              |
| PUT    | /grades/:id                      | CORE_EDU_GRADE_UPDATE      | { score, feedback? } → { success }                                               | **P3 新增** 修正成绩（发 grade.updated 事件）                         |
| POST   | /attendance                      | CORE_EDU_ATTENDANCE_CREATE | { scheduleId, studentId, status, remark? } → { id }                              | **P3 新增** 录入考勤                                                  |
| GET    | /attendance/schedule/:scheduleId | CORE_EDU_ATTENDANCE_READ   | → Attendance[]                                                                   | **P3 新增** 按课时查考勤                                              |
| GET    | /attendance/student/:studentId   | CORE_EDU_ATTENDANCE_READ   | → Attendance[]                                                                   | **P3 新增** 按学生查考勤（DataScope 注入）                            |
| POST   | /courses                         | CORE_EDU_COURSE_CREATE     | { classId, subjectId, teacherId, name, startDate, endDate } → { id }             | **P3 新增** 创建课程                                                  |
| POST   | /schedules                       | CORE_EDU_SCHEDULE_CREATE   | { courseId, lessonId?, teacherId, classId, startTime, endTime } → { id }         | **P3 新增** 排课（含冲突检测）                                        |
| GET    | /schedules/teacher/:teacherId    | CORE_EDU_SCHEDULE_READ     | → Schedule[]                                                                     | **P3 新增** 教师课表                                                  |
| GET    | /schedules/class/:classId        | CORE_EDU_SCHEDULE_READ     | → Schedule[]                                                                     | **P3 新增** 班级课表                                                  |

### 4.2 gRPC API（P3 启用，端口 50053）

| Service               | Method                       | 请求                                | 响应                     | 说明                                     |
| --------------------- | ---------------------------- | ----------------------------------- | ------------------------ | ---------------------------------------- |
| ExamService           | CreateExam                   | CreateExamRequest                   | CreateExamResponse       | 同 REST POST /exams                      |
| ExamService           | GetExam                      | GetExamRequest                      | Exam                     | 同 REST GET /exams/:id                   |
| ExamService           | ListExamsByClass             | ListExamsByClassRequest             | ListExamsResponse        | 同 REST GET /exams/class/:classId        |
| ExamService           | UpdateExam                   | UpdateExamRequest                   | UpdateExamResponse       | 同 REST PUT /exams/:id                   |
| ExamService           | DeleteExam                   | DeleteExamRequest                   | DeleteExamResponse       | 同 REST DELETE /exams/:id                |
| ExamService           | **PublishExam**              | PublishExamRequest                  | PublishExamResponse      | **P3 新增**                              |
| ExamService           | **SubmitExam**               | SubmitExamRequest                   | SubmitExamResponse       | **P3 新增** 含 answers                   |
| ExamService           | **GradeExam**                | GradeExamRequest                    | GradeExamResponse        | **P3 新增**                              |
| HomeworkService       | AssignHomework               | AssignHomeworkRequest               | AssignHomeworkResponse   | 同 REST POST /homework                   |
| HomeworkService       | GetHomework                  | GetHomeworkRequest                  | Homework                 | 同 REST GET /homework/:id                |
| HomeworkService       | ListHomeworkByClass          | ListHomeworkByClassRequest          | ListHomeworkResponse     | 同 REST GET /homework/class/:classId     |
| HomeworkService       | **SubmitHomework**           | SubmitHomeworkRequest（含 answers） | SubmitHomeworkResponse   | **P3 增强** 含完整 answers               |
| HomeworkService       | **GradeHomework**            | GradeHomeworkRequest                | GradeHomeworkResponse    | **P3 新增**                              |
| GradeService          | RecordGrade                  | RecordGradeRequest                  | RecordGradeResponse      | 同 REST POST /grades                     |
| GradeService          | GetGrade                     | GetGradeRequest                     | Grade                    | 同 REST GET /grades/:id                  |
| GradeService          | ListGradesByStudent          | ListGradesByStudentRequest          | ListGradesResponse       | 同 REST GET /grades/student/:studentId   |
| GradeService          | ListGradesByExam             | ListGradesByExamRequest             | ListGradesResponse       | 同 REST GET /grades/exam/:examId         |
| GradeService          | ListGradesByHomework         | ListGradesByHomeworkRequest         | ListGradesResponse       | 同 REST GET /grades/homework/:homeworkId |
| GradeService          | **UpdateGrade**              | UpdateGradeRequest                  | UpdateGradeResponse      | **P3 新增**                              |
| **AttendanceService** | **RecordAttendance**         | RecordAttendanceRequest             | RecordAttendanceResponse | **P3 新增**（coord 整改 #14）            |
| **AttendanceService** | **ListAttendanceBySchedule** | ListAttendanceByScheduleRequest     | ListAttendanceResponse   | **P3 新增**                              |
| **AttendanceService** | **ListAttendanceByStudent**  | ListAttendanceByStudentRequest      | ListAttendanceResponse   | **P3 新增**                              |

### 4.3 权限点清单

```typescript
export const Permissions = {
  // 考试域
  EXAM_CREATE: "CORE_EDU_EXAM_CREATE",
  EXAM_READ: "CORE_EDU_EXAM_READ",
  EXAM_UPDATE: "CORE_EDU_EXAM_UPDATE",
  EXAM_DELETE: "CORE_EDU_EXAM_DELETE",
  EXAM_PUBLISH: "CORE_EDU_EXAM_PUBLISH", // P3 新增
  EXAM_SUBMIT: "CORE_EDU_EXAM_SUBMIT", // P3 新增（学生）
  EXAM_GRADE: "CORE_EDU_EXAM_GRADE", // P3 新增（教师）
  // 作业域
  HOMEWORK_CREATE: "CORE_EDU_HOMEWORK_CREATE",
  HOMEWORK_READ: "CORE_EDU_HOMEWORK_READ",
  HOMEWORK_UPDATE: "CORE_EDU_HOMEWORK_UPDATE",
  HOMEWORK_SUBMIT: "CORE_EDU_HOMEWORK_SUBMIT",
  HOMEWORK_GRADE: "CORE_EDU_HOMEWORK_GRADE", // P3 新增
  // 成绩域
  GRADE_CREATE: "CORE_EDU_GRADE_CREATE",
  GRADE_READ: "CORE_EDU_GRADE_READ",
  GRADE_UPDATE: "CORE_EDU_GRADE_UPDATE", // P3 新增
  // 考勤域（P3 新增）
  ATTENDANCE_CREATE: "CORE_EDU_ATTENDANCE_CREATE",
  ATTENDANCE_READ: "CORE_EDU_ATTENDANCE_READ",
  // 排课域（P3 新增）
  COURSE_CREATE: "CORE_EDU_COURSE_CREATE",
  COURSE_READ: "CORE_EDU_COURSE_READ",
  SCHEDULE_CREATE: "CORE_EDU_SCHEDULE_CREATE",
  SCHEDULE_READ: "CORE_EDU_SCHEDULE_READ",
} as const;
```

---

## 5. 事件设计

### 5.1 发布的领域事件

> 所有事件 payload 必须含 `schema_version`（默认 `"v1"`）+ `event_id`（UUID）+ `occurred_at`（业务时间戳）+ `metadata: { traceId, userId }`
> Topic 命名遵循 [coord 仲裁](../../../docs/architecture/coord-cross-review.md#31-core-edu-教学事件-topic-命名双轨p0-最严重) `edu.teaching.<aggregate>.<action>`

| 事件                | Topic                              | 触发时机                  | 消费者        | payload schema（v1）                                                                                                                              |
| ------------------- | ---------------------------------- | ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| exam.created        | `edu.teaching.exam.created`        | CreateExam 事务内         | msg、data-ana | `{ schema_version, event_id, occurred_at, exam_id, class_id, subject_id, title, exam_date, duration, total_score, status, created_by, metadata }` |
| exam.updated        | `edu.teaching.exam.updated`        | UpdateExam                | msg           | `{ schema_version, event_id, occurred_at, exam_id, changes: {...}, metadata }`                                                                    |
| exam.published      | `edu.teaching.exam.published`      | PublishExam（状态机转换） | msg、data-ana | `{ schema_version, event_id, occurred_at, exam_id, class_id, exam_date, metadata }`                                                               |
| exam.deleted        | `edu.teaching.exam.deleted`        | DeleteExam（软删除）      | data-ana      | `{ schema_version, event_id, occurred_at, exam_id, metadata }`                                                                                    |
| exam.submitted      | `edu.teaching.exam.submitted`      | 学生提交答卷              | data-ana、msg | `{ schema_version, event_id, occurred_at, exam_id, submission_id, student_id, submitted_at, metadata }`                                           |
| homework.assigned   | `edu.teaching.homework.assigned`   | AssignHomework            | msg、data-ana | `{ schema_version, event_id, occurred_at, homework_id, class_id, subject_id, title, due_date, metadata }`                                         |
| homework.submitted  | `edu.teaching.homework.submitted`  | 学生提交作业              | data-ana、msg | `{ schema_version, event_id, occurred_at, homework_id, submission_id, student_id, submitted_at, metadata }`                                       |
| homework.graded     | `edu.teaching.homework.graded`     | 教师批改完成              | msg、data-ana | `{ schema_version, event_id, occurred_at, homework_id, submission_id, student_id, total_score, graded_by, metadata }`                             |
| grade.recorded      | `edu.teaching.grade.recorded`      | RecordGrade               | data-ana、msg | `{ schema_version, event_id, occurred_at, grade_id, student_id, exam_id?, homework_id?, score, total_score, metadata }`                           |
| grade.updated       | `edu.teaching.grade.updated`       | UpdateGrade               | data-ana      | `{ schema_version, event_id, occurred_at, grade_id, student_id, old_score, new_score, metadata }`                                                 |
| attendance.recorded | `edu.teaching.attendance.recorded` | RecordAttendance          | data-ana、msg | `{ schema_version, event_id, occurred_at, attendance_id, schedule_id, student_id, status, metadata }`                                             |
| class.transferred   | `edu.org.class.created`            | classes 合并后            | data-ana      | `{ schema_version, event_id, occurred_at, class_id, name, metadata }`                                                                             |

### 5.2 消费的事件

| Topic                                    | 来源     | 消费动作                                                   | 幂等策略                                    |
| ---------------------------------------- | -------- | ---------------------------------------------------------- | ------------------------------------------- |
| `edu.identity.user.created`              | IAM      | 初始化教师默认班级关联（写 core_edu_teacher_associations） | 唯一索引 (teacher_id, class_id, subject_id) |
| `edu.identity.user.updated`              | IAM      | 更新教师关联（角色变更时）                                 | 基于用户事件序列号去重                      |
| `edu.identity.user.deleted`              | IAM      | 软删除教师关联（不物理删除，保留历史成绩归属）             | 基于用户 id 去重                            |
| `edu.insight.mastery.updated`（P4 强制） | data-ana | 接收学生掌握度，用于推荐个性化练习                         | 基于 mastery_score_id 去重                  |

### 5.3 Outbox Relay Worker 轮询逻辑

```mermaid
sequenceDiagram
    participant Service as Application Service
    participant DB as MySQL
    participant Publisher as OutboxPublisher
    participant Kafka as Kafka
    participant Consumer as 下游消费者

    Service->>DB: BEGIN TRANSACTION
    Service->>DB: INSERT 业务表
    Service->>DB: INSERT outbox (status=pending, event_id=uuid, next_retry_at=now)
    Service->>DB: COMMIT

    loop 每 5 秒
        Publisher->>DB: SELECT * FROM outbox WHERE status='pending' AND next_retry_at <= now() LIMIT 100
        DB-->>Publisher: messages[]
        loop 每条 message
            Publisher->>Kafka: send(topic, key=aggregateId, value=payload, headers={event_id, schema_version, traceId})
            alt 发送成功
                Kafka-->>Publisher: ack
                Publisher->>DB: UPDATE outbox SET status='processed', processed_at=now WHERE id=?
            alt 发送失败
                Kafka-->>Publisher: error
                alt retryCount + 1 < MAX_RETRY(5)
                    Publisher->>DB: UPDATE outbox SET retry_count=retry_count+1, next_retry_at=now()+exponential_backoff WHERE id=?
                else 超过重试上限
                    Publisher->>DB: UPDATE outbox SET status='failed' WHERE id=?
                    Publisher->>Publisher: 发告警 metric + log
                end
            end
        end
    end

    Note over Consumer: 消费端基于 event_id 幂等去重（Redis SETNX 或 DB 唯一索引）
```

**轮询参数配置化**（为 P6 Consul 热更新预留）：

```typescript
const OUTBOX_CONFIG = {
  POLL_INTERVAL_MS: 5000, // 轮询间隔
  BATCH_SIZE: 100, // 每批拉取数
  MAX_RETRY: 5, // 最大重试次数
  BACKOFF_BASE_MS: 1000, // 指数退避基数
  BACKOFF_MAX_MS: 60000, // 最大退避时间
  STUCK_TIMEOUT_MS: 300000, // 5 分钟无进展视为卡死，重置 status=pending
};
```

**远期演进**（P6+）：

- P3：NestJS 进程内 `setInterval` 轮询（当前模式）
- P4+：独立 Go 服务 `services/outbox-relay/`（pending-features 远期目标）
- P6+：Debezium 监听 outbox 表 binlog，CDC 直投 Kafka（彻底移除轮询）

---

## 6. 横切关注点对齐清单

### 6.1 权限装饰器覆盖

| Controller 方法                                 | 权限常量                   |
| ----------------------------------------------- | -------------------------- |
| POST /exams                                     | CORE_EDU_EXAM_CREATE       |
| GET /exams/:id, GET /exams/class/:classId       | CORE_EDU_EXAM_READ         |
| PUT /exams/:id, POST /exams/:id/archive         | CORE_EDU_EXAM_UPDATE       |
| DELETE /exams/:id                               | CORE_EDU_EXAM_DELETE       |
| POST /exams/:id/publish                         | CORE_EDU_EXAM_PUBLISH      |
| POST /exams/:id/start, POST /exams/:id/submit   | CORE_EDU_EXAM_SUBMIT       |
| POST /exams/:id/grade                           | CORE_EDU_EXAM_GRADE        |
| POST /homework                                  | CORE_EDU_HOMEWORK_CREATE   |
| GET /homework/:id, GET /homework/class/:classId | CORE_EDU_HOMEWORK_READ     |
| POST /homework/:id/submit                       | CORE_EDU_HOMEWORK_SUBMIT   |
| POST /homework/:id/grade                        | CORE_EDU_HOMEWORK_GRADE    |
| POST /grades                                    | CORE_EDU_GRADE_CREATE      |
| GET /grades/*                                   | CORE_EDU_GRADE_READ        |
| PUT /grades/:id                                 | CORE_EDU_GRADE_UPDATE      |
| POST /attendance                                | CORE_EDU_ATTENDANCE_CREATE |
| GET /attendance/*                               | CORE_EDU_ATTENDANCE_READ   |
| POST /courses                                   | CORE_EDU_COURSE_CREATE     |
| POST /schedules                                 | CORE_EDU_SCHEDULE_CREATE   |
| GET /schedules/*                                | CORE_EDU_SCHEDULE_READ     |

### 6.2 错误码清单

| 错误码                                      | 触发条件                                  | HTTP 状态码 |
| ------------------------------------------- | ----------------------------------------- | ----------- |
| CORE_EDU_VALIDATION_ERROR                   | Zod 校验失败 / 业务校验失败               | 400         |
| CORE_EDU_UNAUTHORIZED                       | 缺少 x-user-id 头                         | 401         |
| CORE_EDU_FORBIDDEN                          | 权限不足                                  | 403         |
| CORE_EDU_NOT_FOUND                          | 资源不存在                                | 404         |
| CORE_EDU_CONFLICT                           | 状态机非法转换 / 重复提交 / 排课冲突      | 409         |
| CORE_EDU_EXAM_NOT_FOUND                     | 考试不存在                                | 404         |
| CORE_EDU_HOMEWORK_NOT_FOUND                 | 作业不存在                                | 404         |
| CORE_EDU_GRADE_NOT_FOUND                    | 成绩不存在                                | 404         |
| CORE_EDU_ATTENDANCE_NOT_FOUND               | 考勤不存在                                | 404         |
| CORE_EDU_SCHEDULE_NOT_FOUND                 | 排课不存在                                | 404         |
| CORE_EDU_EXAM_INVALID_STATUS_TRANSITION     | 考试状态机非法转换（如 draft → graded）   | 409         |
| CORE_EDU_HOMEWORK_INVALID_STATUS_TRANSITION | 作业状态机非法转换                        | 409         |
| CORE_EDU_GRADE_OUT_OF_RANGE                 | score > totalScore                        | 400         |
| CORE_EDU_GRADE_DUPLICATE                    | 同一学生同一考试/作业重复录入             | 409         |
| CORE_EDU_SCHEDULE_CONFLICT                  | 排课冲突（教师/教室/班级）                | 409         |
| CORE_EDU_HOMEWORK_SUBMIT_LOCK_TIMEOUT       | 作业提交分布式锁获取超时                  | 429         |
| CORE_EDU_OUTBOX_PUBLISH_FAILED              | Outbox 投递失败（仅 log，不影响业务响应） | —           |
| CORE_EDU_TEMPORAL_WORKFLOW_FAILED           | Temporal 工作流启动失败                   | 500         |
| CORE_EDU_INTERNAL_ERROR                     | 未捕获异常                                | 500         |

### 6.3 Logger 初始化

- 实例：`pino`，name=`core-edu`，level 由 `LOG_LEVEL` 环境变量控制
- 位置：[shared/observability/logger.ts](../src/shared/observability/logger.ts)
- 必含字段：`service`、`traceId`、`spanId`、`userId`（从 x-user-id 头注入）
- **P3 修复**：[config/kafka.ts](../src/config/kafka.ts) L20/L22 的 `console.log`/`console.warn` 改用 `logger.info`/`logger.warn`

### 6.4 Metrics 指标清单

| 指标名                                       | 类型      | 标签                  | 描述                              |
| -------------------------------------------- | --------- | --------------------- | --------------------------------- |
| core_edu_requests_total                      | Counter   | method, route, status | HTTP 请求总数（已具备）           |
| core_edu_request_duration_seconds            | Histogram | method, route, status | HTTP 请求延迟（已具备）           |
| core_edu_outbox_pending                      | Gauge     | —                     | 待投递 outbox 消息数（已具备）    |
| core_edu_outbox_published_total              | Counter   | eventType, topic      | 已投递 outbox 消息数（已具备）    |
| core_edu_outbox_failed_total                 | Counter   | eventType             | 投递失败 outbox 消息数（P3 新增） |
| core_edu_outbox_retry_total                  | Counter   | eventType             | 重试次数（P3 新增）               |
| core_edu_exam_state_transitions_total        | Counter   | from, to              | 考试状态机转换次数（P3 新增）     |
| core_edu_homework_submit_lock_acquired_total | Counter   | —                     | 作业提交锁获取成功次数（P3 新增） |
| core_edu_homework_submit_lock_timeout_total  | Counter   | —                     | 作业提交锁超时次数（P3 新增）     |
| core_edu_grade_calculation_duration_seconds  | Histogram | formula_type          | 成绩计算耗时（P3 新增）           |
| core_edu_grpc_requests_total                 | Counter   | method, status        | gRPC 请求总数（P3 新增）          |
| core_edu_grpc_request_duration_seconds       | Histogram | method, status        | gRPC 请求延迟（P3 新增）          |
| core_edu_kafka_consumed_total                | Counter   | topic, event_type     | Kafka 消费消息数（P3 新增）       |
| core_edu_temporal_workflow_started_total     | Counter   | workflow_type         | Temporal 工作流启动数（P3 新增）  |

### 6.5 Tracer 初始化

- SDK：`@opentelemetry/sdk-node` + `getNodeAutoInstrumentations()`（已具备）
- 位置：[shared/observability/tracer.ts](../src/shared/observability/tracer.ts)
- serviceName：`core-edu`
- exporter：OTLP HTTP → `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`
- **P3 新增 span**：
  - `exam.state_transition`（attributes: exam_id, from, to）
  - `homework.submit_lock`（attributes: homework_id, student_id, acquired）
  - `grade.calculate`（attributes: student_id, formula_type）
  - `outbox.publish`（attributes: event_id, event_type, topic）
  - `kafka.consume`（attributes: topic, event_type, event_id）

### 6.6 /healthz 检查逻辑

```typescript
@Get('healthz')
liveness() {
  return { status: 'ok', service: 'core-edu', timestamp: new Date().toISOString() };
}
```

仅返回进程状态，不检查依赖（已具备）。

### 6.7 /readyz 检查逻辑

```typescript
@Get('readyz')
async readiness() {
  const checks = await Promise.allSettled([
    db.execute(sql`SELECT 1`),                           // MySQL
    redis.ping(),                                         // Redis（P3 新增）
    kafkaProducer isConnected ? Promise.resolve() : Promise.reject(),  // Kafka producer
  ]);
  const failed = checks.filter(r => r.status === 'rejected');
  if (failed.length > 0) {
    throw new HttpException({ status: 'error', ... }, 503);
  }
  return { status: 'ok', service: 'core-edu', timestamp: ... };
}
```

### 6.8 优雅关闭顺序

```typescript
process.on("SIGTERM", async () => {
  logger.info("SIGTERM received, shutting down gracefully...");
  // 1. 停止接收新请求（NestJS app.close 自动处理）
  // 2. 等待进行中的 HTTP/gRPC 请求完成
  await app.close();
  // 3. 停止 OutboxPublisher 轮询
  await outboxPublisher.stop();
  // 4. 停止 Kafka consumer（停止消费 IAM 事件）
  await kafkaConsumer.disconnect();
  // 5. 刷新 Kafka producer 缓冲区（确保最后一批事件投递）
  await kafkaProducer.disconnect();
  // 6. 关闭 Redis 连接
  await redis.quit();
  // 7. 关闭 MySQL 连接池
  await closeDb();
  // 8. 关闭 Temporal client
  await temporalClient.close();
  // 9. 关闭 OTel SDK（flush trace）
  await shutdownTracer();
  process.exit(0);
});
```

---

## 7. 与其他模块的交互点（契约清单）

| 方向           | 对方服务    | 协议                              | 接口/事件                                                                                               | 用途                                                        |
| -------------- | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| 被调用         | teacher-bff | HTTP（P2 已用）/ gRPC（P3+ 切换） | ExamService.* / HomeworkService.* / GradeService.*                                                      | 教师场景域聚合                                              |
| 被调用         | student-bff | gRPC                              | ExamService.SubmitExam / HomeworkService.SubmitHomework / GradeService.ListGradesByStudent              | 学生作答与查成绩                                            |
| 被调用         | parent-bff  | gRPC                              | GradeService.ListGradesByStudent / AttendanceService.ListAttendanceByStudent                            | 家长查看子女数据                                            |
| 被调用         | api-gateway | HTTP                              | 直连 REST 端点（绕过 BFF 内部场景）                                                                     | —                                                           |
| 发布           | —           | Kafka                             | `edu.teaching.exam.created` / `.updated` / `.published` / `.submitted` / `.deleted`                     | msg 通知 + data-ana 分析                                    |
| 发布           | —           | Kafka                             | `edu.teaching.homework.assigned` / `.submitted` / `.graded`                                             | msg 通知 + data-ana 掌握度                                  |
| 发布           | —           | Kafka                             | `edu.teaching.grade.recorded` / `.updated`                                                              | data-ana 宽表 + msg 通知                                    |
| 发布           | —           | Kafka                             | `edu.teaching.attendance.recorded`                                                                      | data-ana 出勤统计 + msg 通知                                |
| 发布           | —           | Kafka                             | `edu.org.class.created`                                                                                 | data-ana 班级维度（classes 合并后）                         |
| 消费           | iam         | Kafka                             | `edu.identity.user.created` / `.updated` / `.deleted`                                                   | 同步教师默认班级关联                                        |
| 消费           | data-ana    | Kafka                             | `edu.insight.mastery.updated`（P4 强制）                                                                | 接收掌握度用于推荐个性化练习                                |
| 被动同步       | data-ana    | CDC                               | Debezium 监听 `core_edu_grades` / `core_edu_exams` / `core_edu_homework` / `core_edu_attendance` binlog | data-ana 自动同步 ClickHouse 宽表（无需 core-edu 主动配合） |
| 调用（远期）   | content     | gRPC                              | ContentService.GetKnowledgePoints                                                                       | 排课关联知识点（P4 content 就绪后）                         |
| 被调用（远期） | ai          | gRPC                              | ExamService.GetExam / GradeService.ListGradesByExam                                                     | AI 辅助批改（P5 ai 就绪后）                                 |
| 调用           | temporal    | gRPC                              | Workflow.start（examPublishWorkflow）                                                                   | P3 试点考试发布编排                                         |

---

## 8. 关键设计：考试生命周期状态机

### 8.1 状态机定义

```mermaid
stateDiagram-v2
    [*] --> draft: CreateExam
    draft --> published: PublishExam<br/>(触发 Temporal 工作流)
    published --> in_progress: StartExam<br/>(学生开始作答)
    published --> cancelled: CancelExam
    in_progress --> grading: SubmitExam<br/>(所有学生提交或超时)
    in_progress --> cancelled: CancelExam
    grading --> graded: GradeExam<br/>(教师完成批改)
    graded --> archived: ArchiveExam<br/>(归档)
    graded --> grading: ReopenGrading<br/>(重新批改，发 grade.updated)
    cancelled --> [*]
    archived --> [*]
```

### 8.2 状态转换规则

| 当前状态    | 允许的转换  | 触发条件                                           | 副作用事件                                    |
| ----------- | ----------- | -------------------------------------------------- | --------------------------------------------- |
| draft       | published   | 教师调用 PublishExam，校验 examDate > now()        | exam.published                                |
| draft       | cancelled   | 教师取消                                           | exam.deleted（软删除）                        |
| published   | in_progress | 学生调用 StartExam（首个学生触发）                 | —                                             |
| published   | cancelled   | 教师取消                                           | exam.deleted                                  |
| in_progress | grading     | 所有学生提交 OR 超时（examDate + duration 后自动） | —                                             |
| in_progress | cancelled   | 教师取消                                           | exam.deleted                                  |
| grading     | graded      | 教师完成所有学生批改                               | homework.graded（如果是作业）/ grade.recorded |
| graded      | archived    | 教师归档（30 天后提醒）                            | —                                             |
| graded      | grading     | 教师重新开启批改（成绩复核）                       | grade.updated                                 |

### 8.3 实现方式

```typescript
// domain/exam-state-machine.ts（纯函数，可测试）
const TRANSITIONS: Record<string, string[]> = {
  draft: ["published", "cancelled"],
  published: ["in_progress", "cancelled"],
  in_progress: ["grading", "cancelled"],
  grading: ["graded"],
  graded: ["archived", "grading"],
  archived: [],
  cancelled: [],
};

export function canTransition(from: string, to: string): boolean {
  return TRANSITIONS[from]?.includes(to) ?? false;
}

export function transition(
  from: string,
  to: string,
  context: TransitionContext,
): TransitionResult {
  if (!canTransition(from, to)) {
    throw new ConflictError(`Invalid exam status transition: ${from} → ${to}`, {
      code: "CORE_EDU_EXAM_INVALID_STATUS_TRANSITION",
      from,
      to,
    });
  }
  // 状态特定校验
  if (from === "draft" && to === "published") {
    if (context.exam.examDate <= new Date()) {
      throw new ValidationError(
        "Exam date must be in the future when publishing",
      );
    }
  }
  return { newStatus: to, events: getEventsForTransition(from, to, context) };
}
```

### 8.4 长远演进

- P3：纯函数状态机 + DB status 字段
- P4+：XState 风格状态机库（如果业务规则变复杂）
- P6+：Temporal 工作流托管长流程状态（考试从发布到归档可能跨数月）

---

## 9. 关键设计：作业提交高并发优化

### 9.1 问题场景

期末考试/作业截止前 5 分钟，全班 50+ 学生同时提交，造成：

1. 重复提交（网络重试导致同一学生提交多次）
2. 数据库行锁竞争（多个事务同时写 homework_submissions 表）
3. Outbox 表写入阻塞

### 9.2 解决方案：Redis 分布式锁 + 排队机制

```mermaid
sequenceDiagram
    participant Student as 学生
    participant GW as API Gateway
    participant BFF as student-bff
    participant Core as core-edu
    participant Redis as Redis
    participant DB as MySQL

    Student->>GW: POST /homework/:id/submit
    GW->>BFF: 路由
    BFF->>Core: gRPC SubmitHomework

    Core->>Redis: SET lock:hw:submit:{hwId}:{studentId} NX EX 30
    alt 获取锁成功
        Redis-->>Core: OK
        Core->>DB: BEGIN
        Core->>DB: SELECT * FROM homework_submissions WHERE homework_id=? AND student_id=? FOR UPDATE
        alt 已存在提交
            DB-->>Core: existing
            Core-->>BFF: 幂等返回已存在 submissionId
        else 不存在
            Core->>DB: INSERT homework_submissions
            Core->>DB: INSERT homework_answers (batch)
            Core->>DB: INSERT outbox (homework.submitted)
            Core->>DB: COMMIT
            Core-->>BFF: success, submissionId
        end
        Core->>Redis: DEL lock
    else 锁竞争（重复提交）
        Redis-->>Core: nil
        Core->>Core: 等待 100ms 重试（最多 5 次）
        alt 重试中获取到锁
            Redis-->>Core: OK
            Core->>DB: 同上流程（会发现已存在提交，幂等返回）
        else 重试 5 次仍失败
            Core-->>BFF: 429 CORE_EDU_HOMEWORK_SUBMIT_LOCK_TIMEOUT
        end
    end
```

### 9.3 锁参数

| 参数     | 值                                          | 说明                        |
| -------- | ------------------------------------------- | --------------------------- |
| 锁 key   | `lock:hw:submit:{homeworkId}:{studentId}`   | 细粒度：同一学生同一作业    |
| 锁过期   | 30s                                         | 防止持锁进程崩溃导致死锁    |
| 重试次数 | 5                                           | 排队等待重试                |
| 重试间隔 | 100ms（固定）                               | P3 简化，P6+ 可改为指数退避 |
| 超时响应 | 429 + CORE_EDU_HOMEWORK_SUBMIT_LOCK_TIMEOUT | 客户端可重试                |

### 9.4 幂等性保障

1. **锁层**：Redis SETNX 防止并发重复提交
2. **DB 层**：唯一索引 `(homework_id, student_id)` 兜底
3. **业务层**：先 SELECT 检查已存在则直接返回（幂等）
4. **Outbox 层**：`event_id` UUID 保证事件不重复

---

## 10. 关键设计：成绩计算配置化

### 10.1 公式类型

```typescript
type GradeFormulaType = "weighted" | "average" | "custom";

interface GradeFormula {
  type: GradeFormulaType;
  weights?: Record<string, number>; // weighted: { exam: 0.4, homework: 0.3, attendance: 0.3 }
  customExpression?: string; // custom: 安全表达式（如 "exam*0.5 + homework*0.3 + attendance*0.2"）
}
```

### 10.2 计算流程

```mermaid
flowchart TD
    A[请求计算学生总评成绩] --> B{查询 grade_formulas}
    B --> C{scope 优先级}
    C -->|class 级| D[用 class 公式]
    C -->|subject 级| E[用 subject 公式]
    C -->|school 级| F[用 school 公式]
    C -->|无公式| G[默认平均]

    D --> H{formula_type}
    E --> H
    F --> H
    G --> H

    H -->|weighted| I[按 weights 加权求和]
    H -->|average| J[简单平均]
    H -->|custom| K[解析 customExpression 安全求值]

    I --> L[返回总评成绩]
    J --> L
    K --> L
```

### 10.3 安全求值（custom 公式）

P3 采用白名单 + 受限表达式求值：

- 仅允许：变量名（exam/homework/attendance）、数字、四则运算符、括号
- 禁止：函数调用、对象访问、原型链、eval
- 实现：先用正则白名单校验，再用 `Function` 构造器求值（沙箱）

P6+ 演进：引入 `expr-eval` 或 `mathjs` 等成熟表达式库。

### 10.4 长远演进

- P3：JSON 配置（weights / customExpression）
- P4+：规则引擎（durable-rules / json-rules-engine），支持条件分支（如"出勤率 < 80% 则总评降一档"）
- P6+：可视化公式编辑器（前端 DSL + 后端解释器）

---

## 11. 关键设计：排课冲突检测

### 11.1 冲突类型

| 冲突类型         | 检测条件                                              |
| ---------------- | ----------------------------------------------------- |
| 教师时间冲突     | 同一教师同一时间段（[start, end) 区间重叠）已有排课   |
| 班级时间冲突     | 同一班级同一时间段已有排课                            |
| 教室冲突（可选） | 同一教室同一时间段已有排课（P3 预留 room_id，不强制） |

### 11.2 检测算法

```sql
-- 教师冲突检测
SELECT id FROM core_edu_schedules
WHERE teacher_id = ?
  AND status = 'scheduled'
  AND start_time < ?  -- 新排课的 end_time
  AND end_time > ?    -- 新排课的 start_time
LIMIT 1;

-- 班级冲突检测
SELECT id FROM core_edu_schedules
WHERE class_id = ?
  AND status = 'scheduled'
  AND start_time < ?
  AND end_time > ?
LIMIT 1;
```

### 11.3 长远演进

- P3：DB 查询检测（简单区间重叠）
- P4+：Redis 排课索引（sorted set by start_time），O(log N) 查询
- P6+：全校资源调度算法（约束满足问题 CSP，如 OR-Tools）

---

## 12. 关键设计：Temporal 工作流试点

### 12.1 P3 试点：考试发布编排工作流

```mermaid
flowchart TD
    Start[ExamPublished 事件] --> A1[Activity: 创建 ExamSubmission 骨架<br/>为每个学生创建 in_progress 提交]
    A1 --> A2[Activity: 通知 msg 服务<br/>发送考试通知给学生]
    A2 --> A3[Activity: 等待作答窗口<br/>Timer: examDate + duration]
    A3 --> A4[Activity: 自动提交未答学生<br/>status=submitted, score=0]
    A4 --> A5[Activity: 通知教师<br/>考试已结束可批改]
    A5 --> End[工作流完成]
```

### 12.2 选择 Temporal 的理由

1. **长流程编排**：考试从发布到批改完成可能跨数小时到数周，传统消息队列难以维护状态
2. **可观测**：Temporal UI 可视化工作流进度
3. **可回滚**：工作流失败可重试或回滚
4. **P3 试点**：仅 1 个工作流，验证技术栈，P4+ 可推广到作业批改、成绩结算等场景

### 12.3 集成方式

- Temporal server：独立部署（infra/docker-compose.yml 已规划）
- core-edu 作为 Temporal client：`@temporal/sdk` 包
- 工作流定义：`src/workflows/exam-publish.workflow.ts`
- Activity 实现：`src/workflows/exam-publish.activities.ts`
- 触发点：`ExamsService.publishExam()` 调用 `workflowClient.start(examPublishWorkflow, ...)`

---

## 13. 风险与假设

### 13.1 技术风险

| 风险                                 | 影响             | 缓解措施                                      |
| ------------------------------------ | ---------------- | --------------------------------------------- |
| Redis 分布式锁在主从切换时失效       | 重复提交         | DB 唯一索引兜底 + 业务层 SELECT 检查          |
| Outbox relay 进程内模式，单点故障    | 事件丢失         | P3 接受（业务事务已落库），P4+ 迁独立 Go 服务 |
| Temporal server 引入运维复杂度       | P3 试点延期      | 若 P3 时间紧，可降级为纯事件驱动（无工作流）  |
| gRPC server 启用增加 NestJS 启动时间 | 服务启动慢       | lazy init gRPC，HTTP 先就绪                   |
| ClickHouse CDC 延迟 > 5s             | 家长查看成绩延迟 | 双轨读策略（实时查 MySQL）                    |
| 多题型 answer 结构差异大             | schema 复杂      | answer 字段用 JSON，按 questionType 分桶解析  |
| 成绩计算 custom 公式安全风险         | 代码注入         | 白名单正则 + 沙箱求值，P6+ 迁成熟库           |

### 13.2 假设

| 假设                                          | 依赖方           | fallback                                                                       |
| --------------------------------------------- | ---------------- | ------------------------------------------------------------------------------ |
| 假设 iam 在 P3 启用 gRPC server               | iam（ai06）      | 不就绪则 core-edu 仍走 HTTP 调 iam（仅查权限），不影响 core-edu 自身 gRPC 启用 |
| 假设 data-ana CDC 链路稳定                    | data-ana（ai06） | CDC 失败不影响 core-edu 写业务，仅影响读模型延迟                               |
| 假设 msg 服务在 P5 就绪前不消费 core-edu 事件 | msg（ai05）      | 事件留在 Kafka（按 retention 配置），msg 上线后回溯消费                        |
| 假设 content 服务在 P4 就绪                   | content（ai09）  | P3 不调 content，题库 question_id 仅作外键引用，不校验存在性                   |
| 假设 Redis 在 P3 部署                         | infra            | 不就绪则降级：作业提交锁改用 DB SELECT FOR UPDATE（性能下降但功能可用）        |
| 假设 Temporal 在 P3 部署                      | infra            | 不就绪则考试发布降级为同步事件驱动（无工作流）                                 |

### 13.3 未决的设计决策（提请 coord 仲裁）

| #   | 决策点                                      | 选项                                                        | ai08 倾向                                   | 影响                                     |
| --- | ------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------- | ---------------------------------------- |
| 1   | classes 服务合并到 core-edu 的时机          | (a) P3 初期合并 / (b) P3 末期合并                           | (a) 初期合并，避免后期大改                  | 影响 ClassesModule 迁移路径              |
| 2   | 排课 room_id 是否 P3 实现                   | (a) P3 实现 / (b) P3 仅预留字段 / (c) P6+                   | (b) 仅预留字段，pending-features 未明确要求 | 影响 ScheduleConflictChecker 复杂度      |
| 3   | 成绩计算公式 scope 优先级                   | (a) class > subject > school / (b) school > subject > class | (a) 越细粒度优先                            | 影响 GradeCalculator 实现                |
| 4   | 作业 grace_period 默认值                    | (a) 0 秒（严格）/ (b) 300 秒（5 分钟宽限）                  | (b) 5 分钟宽限                              | 影响 HomeworkService.submitHomework 校验 |
| 5   | 是否在 P3 启用 events.proto schema 强制校验 | (a) JSON Schema 校验 / (b) 仅文档约束                       | (b) 仅文档约束，P6+ 引入 schema registry    | 影响 Outbox payload 序列化               |
| 6   | exam.submitted 事件是否包含完整 answers     | (a) 包含（事件大）/ (b) 仅含 submission_id，下游按需查询    | (b) 仅含 submission_id                      | 影响 Kafka 消息大小与下游查询模式        |
| 7   | archived 考试数据是否物理迁移到归档表       | (a) P3 物理迁移 / (b) P3 仅软删除 / (c) P6+ 迁移            | (b) 仅软删除，P6+ 迁 ClickHouse 冷存储      | 影响 schema 设计                         |

---

## 14. 实施优先级（P3 阶段）

| 优先级 | 任务                                                                           | 依赖                   |
| ------ | ------------------------------------------------------------------------------ | ---------------------- |
| P0     | TOPIC_MAP 重命名为 `edu.teaching.*` + payload 补 `schema_version` + `event_id` | 无                     |
| P0     | Drizzle `getDb()` 统一 + kafka.ts console.log → logger                         | 无                     |
| P0     | Zod 输入验证（全部 Controller）                                                | 无                     |
| P0     | /readyz 补 Kafka + Redis 探针                                                  | Redis 引入             |
| P1     | 考试状态机实现（draft → published → ... → archived）                           | Zod                    |
| P1     | 作业状态机补全（+ graded）+ homework.graded 事件                               | 状态机                 |
| P1     | 成绩录入幂等（唯一索引 + idempotencyKey）                                      | schema 改造            |
| P1     | DataScope 下推（Repository 层 WHERE 注入）                                     | —                      |
| P1     | 消费 IAM 事件（user.created/updated/deleted）                                  | Kafka consumer         |
| P2     | 作业提交 Redis 分布式锁                                                        | Redis                  |
| P2     | gRPC server 启用（端口 50053）                                                 | buf.gen.yaml gRPC 插件 |
| P2     | core_edu.proto 补 AttendanceService + AttendanceService 实现                   | proto coord 审查       |
| P2     | 排课/考勤数据模型（4 表 + CRUD）                                               | schema                 |
| P2     | 成绩计算配置化（grade_formulas 表 + GradeCalculator）                          | schema                 |
| P3     | Temporal 工作流试点（考试发布编排）                                            | Temporal server        |
| P3     | classes 服务合并到 core-edu                                                    | 决策 #1                |
| P3     | Dockerfile 多阶段构建核对                                                      | —                      |
| P3     | 测试覆盖率 ≥ 80%                                                               | 全部功能就绪           |

---

## 15. 自检清单（对照 coord 交叉审查规则）

> [ai-allocation §8 交叉审查规则](../../../docs/architecture/ai-allocation.md#8-交叉审查规则)

### 15.1 接口一致性

- [x] proto 包名 `next_edu_cloud.core_edu.v1`（符合 coord 仲裁）
- [x] gRPC 端口 50053 声明（符合 coord 全局端口矩阵）
- [x] HTTP 端口 3004 声明
- [ ] core_edu.proto 补 AttendanceService（待 coord 修改 proto 后实现）
- [ ] buf generate 后 gRPC 代码生成（待 coord 补 buf.gen.yaml gRPC 插件）

### 15.2 Kafka topic 不重复

- [x] topic 命名统一为 `edu.teaching.<aggregate>.<action>`（符合 coord 仲裁）
- [x] 无与其他服务 topic 重复
- [x] CDC topic `edu-cdc.next_edu_cloud.core_edu_*` 已实施

### 15.3 端口不冲突

- [x] HTTP 3004（不冲突）
- [x] gRPC 50053（不冲突）
- [x] /metrics 随 HTTP 3004

### 15.4 错误码前缀不重叠

- [x] `CORE_EDU_*` 唯一（与 iam `IAM_*` / content `CONTENT_*` / msg `MSG_*` 等不重叠）
- [x] 子域统一 `CORE_EDU_*`（不再细分 `EXAMS_`/`HOMEWORK_`/`GRADES_`，符合 coord §5.5 仲裁）

### 15.5 黄金模板对齐

- [x] @RequirePermission 覆盖全部 Controller 方法（P3 新增端点也补）
- [x] 错误码前缀 `CORE_EDU_*`
- [x] /healthz + /readyz 存在（P3 增强 /readyz 探针）
- [ ] Zod 输入验证（P3 实施）
- [x] GlobalErrorFilter 注册到 AppModule
- [ ] Dockerfile 多阶段（P3 核对）
- [x] ActionState 信封对齐

---

**AI Agent**: ai08 (core-edu)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式（直接 push main）
