feat(p3): core teaching service with Outbox + Kafka event bus

P3 阶段交付物:
- services/core-edu: 教学核心服务(DDD 限界上下文:exams/grades/homework/classes)
  - exams: 考试 CRUD + 事务内写 exam + outbox
  - grades: 成绩 CRUD
  - homework: 作业 CRUD
  - classes.module: 复用 P1 classes 模块(聚合到 core-edu 服务)
- Outbox 模式实现:
  - outbox.schema.ts: core_edu_outbox 表(id/aggregate_id/event_type/payload/status/retry_count)
  - outbox.repository.ts: 支持事务参数 tx,确保业务+事件原子性
  - outbox.publisher.ts: Kafka idempotent producer + transactionalId,TOPIC_MAP 路由 9 种事件,MAX_RETRY=5
- config/kafka.ts: idempotent producer + transactionalId 配置
- main.ts: 启动顺序 initTracer → connectKafka → outboxPublisher.start → app.listen
- packages/shared-proto/proto/core_edu.proto: ExamService/HomeworkService/GradeService 契约
- packages/shared-proto/proto/events.proto: ClassEvent/ExamEvent/HomeworkEvent/GradeEvent 领域事件契约
This commit is contained in:
SpecialX
2026-07-08 01:38:07 +08:00
parent 524204d30a
commit 23246ade6d
37 changed files with 1592 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { ExamsService, type CreateExamInput, type UpdateExamInput } from './exams.service.js';
import { PermissionGuard, Permissions } from '../../middleware/permission.guard.js';
interface SuccessResponse<T> {
data: T;
timestamp: string;
}
@Controller('api/v1/exams')
export class ExamsController {
constructor(private readonly examsService: ExamsService) {}
@Post()
@UseGuards(new PermissionGuard(Permissions.EXAM_CREATE))
async create(@Body() body: CreateExamInput): Promise<SuccessResponse<{ id: string }>> {
const result = await this.examsService.createExam(body);
return { data: result, timestamp: new Date().toISOString() };
}
@Get(':id')
@UseGuards(new PermissionGuard(Permissions.EXAM_READ))
async findOne(@Param('id') id: string): Promise<SuccessResponse<Awaited<ReturnType<ExamsService['getExam']>>>> {
const data = await this.examsService.getExam(id);
return { data, timestamp: new Date().toISOString() };
}
@Get('class/:classId')
@UseGuards(new PermissionGuard(Permissions.EXAM_READ))
async listByClass(@Param('classId') classId: string): Promise<SuccessResponse<Awaited<ReturnType<ExamsService['listExamsByClass']>>>> {
const data = await this.examsService.listExamsByClass(classId);
return { data, timestamp: new Date().toISOString() };
}
@Put(':id')
@UseGuards(new PermissionGuard(Permissions.EXAM_UPDATE))
async update(@Param('id') id: string, @Body() body: UpdateExamInput): Promise<SuccessResponse<{ success: true }>> {
await this.examsService.updateExam(id, body);
return { data: { success: true }, timestamp: new Date().toISOString() };
}
@Delete(':id')
@UseGuards(new PermissionGuard(Permissions.EXAM_DELETE))
async remove(@Param('id') id: string): Promise<SuccessResponse<{ success: true }>> {
await this.examsService.deleteExam(id);
return { data: { success: true }, timestamp: new Date().toISOString() };
}
}