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,50 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { env } from './config/env.js';
import { connectKafka, disconnectKafka } from './config/kafka.js';
import { outboxPublisher } from './shared/outbox/outbox.publisher.js';
import { GlobalErrorFilter } from './shared/errors/global-error.filter.js';
import { initTracer, shutdownTracer } from './shared/observability/tracer.js';
import { logger } from './shared/observability/logger.js';
async function bootstrap(): Promise<void> {
initTracer();
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.setGlobalPrefix('api');
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
// Connect Kafka producer/consumer before starting the outbox publisher
await connectKafka();
// Start the transactional outbox publisher - polls pending messages
// and publishes them to Kafka topics defined in TOPIC_MAP.
await outboxPublisher.start();
await app.listen(env.PORT);
logger.info(
{ port: env.PORT, service: 'core-edu' },
'CoreEdu service is listening',
);
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully...');
await outboxPublisher.stop();
await disconnectKafka();
await shutdownTracer();
await app.close();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received, shutting down gracefully...');
await outboxPublisher.stop();
await disconnectKafka();
await shutdownTracer();
await app.close();
process.exit(0);
});
}
void bootstrap();