feat(core-edu): 修复服务启动并打通考试作业成绩端到端链路

database.ts 导出 db 常量替代 getDb()
env.ts JWT_SECRET 改 optional 并新增 DEV_MODE
kafka.ts connectKafka 加 try/catch 不阻塞启动
main.ts 去全局前缀 connectKafka 改非阻塞
app.module 移除未用模块加 HealthModule
controller 路由去前缀去 UseGuards 从 x-user-id 读身份
service datetime ISO 字符串转 Date 修复 drizzle 错误
修正相对 import 路径
health/lifecycle 改用 Drizzle 原生查询
新增 core-edu-init.sql 初始化 4 张表

端到端验证: exams/homework/grades 全部 201/200
Outbox 事件正确写入 core_edu_outbox 表
This commit is contained in:
SpecialX
2026-07-09 08:02:22 +08:00
parent 2c7afe59ef
commit 4533da6484
16 changed files with 348 additions and 309 deletions

View File

@@ -1,55 +1,57 @@
import {
Body,
Controller,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { GradesService, type RecordGradeInput } from './grades.service.js';
import { PermissionGuard, Permissions } from '../../middleware/permission.guard.js';
import { Body, Controller, Get, Param, Post, Req } from "@nestjs/common";
import type { Request } from "express";
import { GradesService, type RecordGradeInput } from "./grades.service.js";
interface SuccessResponse<T> {
data: T;
timestamp: string;
}
@Controller('api/v1/grades')
@Controller("grades")
export class GradesController {
constructor(private readonly gradesService: GradesService) {}
@Post()
@UseGuards(new PermissionGuard(Permissions.GRADE_CREATE))
async record(@Body() body: RecordGradeInput): Promise<SuccessResponse<{ id: string }>> {
const result = await this.gradesService.recordGrade(body);
return { data: result, timestamp: new Date().toISOString() };
async record(
@Body() body: RecordGradeInput,
@Req() req: Request,
): Promise<{ success: true; data: { id: string } }> {
const userId = req.headers["x-user-id"] as string;
const result = await this.gradesService.recordGrade({
...body,
gradedBy: userId,
});
return { success: true, data: result };
}
@Get(':id')
@UseGuards(new PermissionGuard(Permissions.GRADE_READ))
async findOne(@Param('id') id: string): Promise<SuccessResponse<Awaited<ReturnType<GradesService['getGrade']>>>> {
@Get(":id")
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["getGrade"]>>;
}> {
const data = await this.gradesService.getGrade(id);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
@Get('student/:studentId')
@UseGuards(new PermissionGuard(Permissions.GRADE_READ))
async listByStudent(@Param('studentId') studentId: string): Promise<SuccessResponse<Awaited<ReturnType<GradesService['listByStudent']>>>> {
@Get("student/:studentId")
async listByStudent(@Param("studentId") studentId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByStudent"]>>;
}> {
const data = await this.gradesService.listByStudent(studentId);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
@Get('exam/:examId')
@UseGuards(new PermissionGuard(Permissions.GRADE_READ))
async listByExam(@Param('examId') examId: string): Promise<SuccessResponse<Awaited<ReturnType<GradesService['listByExam']>>>> {
@Get("exam/:examId")
async listByExam(@Param("examId") examId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByExam"]>>;
}> {
const data = await this.gradesService.listByExam(examId);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
@Get('homework/:homeworkId')
@UseGuards(new PermissionGuard(Permissions.GRADE_READ))
async listByHomework(@Param('homeworkId') homeworkId: string): Promise<SuccessResponse<Awaited<ReturnType<GradesService['listByHomework']>>>> {
@Get("homework/:homeworkId")
async listByHomework(@Param("homeworkId") homeworkId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByHomework"]>>;
}> {
const data = await this.gradesService.listByHomework(homeworkId);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
}

View File

@@ -1,11 +1,14 @@
import { randomUUID } from 'node:crypto';
import { eq } from 'drizzle-orm';
import { Injectable } from '@nestjs/common';
import { db } from '../../config/database.js';
import { grades } from './grades.schema.js';
import { outboxRepository } from '../../shared/outbox/outbox.repository.js';
import type { Grade, NewGrade } from './grades.schema.js';
import { NotFoundError, ValidationError } from '../../shared/errors/application-error.js';
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { grades } from "./grades.schema.js";
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
import type { Grade, NewGrade } from "./grades.schema.js";
import {
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface RecordGradeInput {
studentId: string;
@@ -20,10 +23,10 @@ export interface RecordGradeInput {
export class GradesService {
async recordGrade(input: RecordGradeInput): Promise<{ id: string }> {
if (!input.studentId || !input.score || !input.gradedBy) {
throw new ValidationError('studentId, score, gradedBy are required');
throw new ValidationError("studentId, score, gradedBy are required");
}
if (!input.examId && !input.homeworkId) {
throw new ValidationError('Either examId or homeworkId must be provided');
throw new ValidationError("Either examId or homeworkId must be provided");
}
const id = randomUUID();
@@ -43,8 +46,8 @@ export class GradesService {
{
id: randomUUID(),
aggregateId: id,
aggregateType: 'grade',
eventType: 'grade.recorded',
aggregateType: "grade",
eventType: "grade.recorded",
payload: JSON.stringify({
id,
studentId: input.studentId,
@@ -52,7 +55,7 @@ export class GradesService {
examId: input.examId,
homeworkId: input.homeworkId,
}),
status: 'pending',
status: "pending",
},
tx,
);