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:
24
services/core-edu/src/config/database.ts
Normal file
24
services/core-edu/src/config/database.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { env } from './env.js';
|
||||
|
||||
let pool: mysql.Pool | null = null;
|
||||
|
||||
export function getDb() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
uri: env.DATABASE_URL,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
}
|
||||
return drizzle(pool);
|
||||
}
|
||||
|
||||
export async function closeDb(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
26
services/core-edu/src/config/env.ts
Normal file
26
services/core-edu/src/config/env.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.string().default('3004'),
|
||||
DATABASE_URL: z.string().url(),
|
||||
REDIS_URL: z.string().url().optional(),
|
||||
JWT_SECRET: z.string(),
|
||||
JWT_ISSUER: z.string().default('next-edu-cloud'),
|
||||
KAFKA_BROKERS: z.string().default('localhost:9092'),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
|
||||
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
export function loadEnv(): Env {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
|
||||
throw new Error('Invalid environment configuration');
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export const env = loadEnv();
|
||||
25
services/core-edu/src/config/kafka.ts
Normal file
25
services/core-edu/src/config/kafka.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Kafka } from 'kafkajs';
|
||||
import { env } from './env.js';
|
||||
|
||||
export const kafka = new Kafka({
|
||||
brokers: env.KAFKA_BROKERS.split(','),
|
||||
clientId: 'core-edu-service',
|
||||
});
|
||||
|
||||
export const producer = kafka.producer({
|
||||
idempotent: true,
|
||||
transactionalId: 'core-edu-tx',
|
||||
});
|
||||
|
||||
export const consumer = kafka.consumer({ groupId: 'core-edu-group' });
|
||||
|
||||
export async function connectKafka(): Promise<void> {
|
||||
await producer.connect();
|
||||
await consumer.connect();
|
||||
console.log('Kafka connected');
|
||||
}
|
||||
|
||||
export async function disconnectKafka(): Promise<void> {
|
||||
await producer.disconnect();
|
||||
await consumer.disconnect();
|
||||
}
|
||||
Reference in New Issue
Block a user