模块架构设计文档 — core-edu
AI 标识:ai08(按 ai-allocation.md §3.2 负责)
模块:core-edu(教学核心服务)
阶段:架构设计外包 · 阶段 2(模块架构设计)· 待 coord 交叉审查
日期:2026-07-09
关联文档:
0. 设计原则与长远定位
0.1 设计原则
- DDD 限界上下文清晰:core-edu 承载 D2 教学组织 + D3 教学核心两个上下文,聚合根间通过事件通信,禁止跨聚合直接事务
- CQRS 读写分离:写模型 MySQL 独占,读模型由 data-ana 通过 CDC 投递 ClickHouse 宽表;core-edu 只写不读 ClickHouse
- EDA 事件驱动:所有跨聚合/跨服务副作用通过 Outbox + Kafka 事件触发,禁止同步调用下游(msg/data-ana/content)
- 状态机显式化:考试/作业/成绩生命周期用状态机建模,转换需校验前置状态,状态变更必须发事件
- 幂等性优先:所有外部入口(HTTP / gRPC / Kafka 消费)假设可能重试,业务操作必须基于业务唯一键去重
- 配置化优于硬编码:成绩计算公式、状态转换规则、并发限流阈值均配置化,为 P6 配置中心(Consul)热更新预留
- 可观测性内置:每个状态转换、每个 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 分层架构
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 聚合根与边界
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 考试域
// 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 作业域
// 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 成绩域
// 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 新增)
// 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(已存在,需补字段)
// 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) |
— |
最终一致(< 5s) |
| 校管理员查看全校分析 |
ClickHouse 宽表(data-ana 服务) |
— |
最终一致 |
| 教导主任查看年级成绩分布 |
ClickHouse 宽表(data-ana 服务) |
— |
最终一致 |
双轨读策略(known-issues §2.6):刚提交的成绩查 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 权限点清单
5. 事件设计
5.1 发布的领域事件
所有事件 payload 必须含 schema_version(默认 "v1")+ event_id(UUID)+ occurred_at(业务时间戳)+ metadata: { traceId, userId }
Topic 命名遵循 coord 仲裁 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 轮询逻辑
轮询参数配置化(为 P6 Consul 热更新预留):
远期演进(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 初始化
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
- 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 检查逻辑
仅返回进程状态,不检查依赖(已具备)。
6.7 /readyz 检查逻辑
6.8 优雅关闭顺序
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 状态机定义
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 实现方式
8.4 长远演进
- P3:纯函数状态机 + DB status 字段
- P4+:XState 风格状态机库(如果业务规则变复杂)
- P6+:Temporal 工作流托管长流程状态(考试从发布到归档可能跨数月)
9. 关键设计:作业提交高并发优化
9.1 问题场景
期末考试/作业截止前 5 分钟,全班 50+ 学生同时提交,造成:
- 重复提交(网络重试导致同一学生提交多次)
- 数据库行锁竞争(多个事务同时写 homework_submissions 表)
- Outbox 表写入阻塞
9.2 解决方案:Redis 分布式锁 + 排队机制
9.3 锁参数
| 参数 |
值 |
说明 |
| 锁 key |
lock:hw:submit:{homeworkId}:{studentId} |
细粒度:同一学生同一作业 |
| 锁过期 |
30s |
防止持锁进程崩溃导致死锁 |
| 重试次数 |
5 |
排队等待重试 |
| 重试间隔 |
100ms(固定) |
P3 简化,P6+ 可改为指数退避 |
| 超时响应 |
429 + CORE_EDU_HOMEWORK_SUBMIT_LOCK_TIMEOUT |
客户端可重试 |
9.4 幂等性保障
- 锁层:Redis SETNX 防止并发重复提交
- DB 层:唯一索引
(homework_id, student_id) 兜底
- 业务层:先 SELECT 检查已存在则直接返回(幂等)
- Outbox 层:
event_id UUID 保证事件不重复
10. 关键设计:成绩计算配置化
10.1 公式类型
10.2 计算流程
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 检测算法
11.3 长远演进
- P3:DB 查询检测(简单区间重叠)
- P4+:Redis 排课索引(sorted set by start_time),O(log N) 查询
- P6+:全校资源调度算法(约束满足问题 CSP,如 OR-Tools)
12. 关键设计:Temporal 工作流试点
12.1 P3 试点:考试发布编排工作流
12.2 选择 Temporal 的理由
- 长流程编排:考试从发布到批改完成可能跨数小时到数周,传统消息队列难以维护状态
- 可观测:Temporal UI 可视化工作流进度
- 可回滚:工作流失败可重试或回滚
- 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 交叉审查规则
15.1 接口一致性
15.2 Kafka topic 不重复
15.3 端口不冲突
15.4 错误码前缀不重叠
15.5 黄金模板对齐
AI Agent: ai08 (core-edu)
Coordinator: coord-ai
Branch: 单仓库并行模式(直接 push main)