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,48 +1,50 @@
import { Body, Controller, Get, Param, Post, Req } from "@nestjs/common";
import type { Request } from "express";
import {
Body,
Controller,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { HomeworkService, type AssignHomeworkInput } from './homework.service.js';
import { PermissionGuard, Permissions } from '../../middleware/permission.guard.js';
HomeworkService,
type AssignHomeworkInput,
} from "./homework.service.js";
interface SuccessResponse<T> {
data: T;
timestamp: string;
}
@Controller('api/v1/homework')
@Controller("homework")
export class HomeworkController {
constructor(private readonly homeworkService: HomeworkService) {}
@Post()
@UseGuards(new PermissionGuard(Permissions.HOMEWORK_CREATE))
async assign(@Body() body: AssignHomeworkInput): Promise<SuccessResponse<{ id: string }>> {
const result = await this.homeworkService.assignHomework(body);
return { data: result, timestamp: new Date().toISOString() };
async assign(
@Body() body: AssignHomeworkInput,
@Req() req: Request,
): Promise<{ success: true; data: { id: string } }> {
const userId = req.headers["x-user-id"] as string;
const result = await this.homeworkService.assignHomework({
...body,
createdBy: userId,
});
return { success: true, data: result };
}
@Get(':id')
@UseGuards(new PermissionGuard(Permissions.HOMEWORK_READ))
async findOne(@Param('id') id: string): Promise<SuccessResponse<Awaited<ReturnType<HomeworkService['getHomework']>>>> {
@Get(":id")
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<HomeworkService["getHomework"]>>;
}> {
const data = await this.homeworkService.getHomework(id);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
@Get('class/:classId')
@UseGuards(new PermissionGuard(Permissions.HOMEWORK_READ))
async listByClass(@Param('classId') classId: string): Promise<SuccessResponse<Awaited<ReturnType<HomeworkService['listByClass']>>>> {
@Get("class/:classId")
async listByClass(@Param("classId") classId: string): Promise<{
success: true;
data: Awaited<ReturnType<HomeworkService["listByClass"]>>;
}> {
const data = await this.homeworkService.listByClass(classId);
return { data, timestamp: new Date().toISOString() };
return { success: true, data };
}
@Post(':id/submit')
@UseGuards(new PermissionGuard(Permissions.HOMEWORK_SUBMIT))
async submit(@Param('id') id: string): Promise<SuccessResponse<{ success: true }>> {
@Post(":id/submit")
async submit(
@Param("id") id: string,
): Promise<{ success: true; data: { success: true } }> {
await this.homeworkService.submitHomework(id);
return { data: { success: true }, timestamp: new Date().toISOString() };
return { success: true, data: { success: true } };
}
}

View File

@@ -1,17 +1,20 @@
import { randomUUID } from 'node:crypto';
import { eq } from 'drizzle-orm';
import { Injectable } from '@nestjs/common';
import { db } from '../../config/database.js';
import { homework } from './homework.schema.js';
import { outboxRepository } from '../../shared/outbox/outbox.repository.js';
import type { Homework, NewHomework } from './homework.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 { homework } from "./homework.schema.js";
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
import type { Homework, NewHomework } from "./homework.schema.js";
import {
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface AssignHomeworkInput {
classId: string;
title: string;
description?: string;
dueDate: Date;
dueDate: Date | string;
createdBy: string;
}
@@ -19,7 +22,7 @@ export interface AssignHomeworkInput {
export class HomeworkService {
async assignHomework(input: AssignHomeworkInput): Promise<{ id: string }> {
if (!input.classId || !input.title || !input.createdBy) {
throw new ValidationError('classId, title, createdBy are required');
throw new ValidationError("classId, title, createdBy are required");
}
const id = randomUUID();
@@ -28,8 +31,10 @@ export class HomeworkService {
classId: input.classId,
title: input.title,
description: input.description,
dueDate: input.dueDate,
status: 'assigned',
// Drizzle datetime 列需要 Date 对象HTTP 请求体里 dueDate 是 ISO 字符串。
dueDate:
input.dueDate instanceof Date ? input.dueDate : new Date(input.dueDate),
status: "assigned",
createdBy: input.createdBy,
};
@@ -39,14 +44,14 @@ export class HomeworkService {
{
id: randomUUID(),
aggregateId: id,
aggregateType: 'homework',
eventType: 'homework.assigned',
aggregateType: "homework",
eventType: "homework.assigned",
payload: JSON.stringify({
id,
classId: input.classId,
title: input.title,
}),
status: 'pending',
status: "pending",
},
tx,
);
@@ -73,23 +78,23 @@ export class HomeworkService {
async submitHomework(id: string): Promise<void> {
const existing = await this.getHomework(id);
if (existing.status === 'submitted') {
if (existing.status === "submitted") {
throw new ValidationError(`Homework ${id} already submitted`);
}
await db.transaction(async (tx) => {
await tx
.update(homework)
.set({ status: 'submitted' })
.set({ status: "submitted" })
.where(eq(homework.id, id));
await outboxRepository.create(
{
id: randomUUID(),
aggregateId: id,
aggregateType: 'homework',
eventType: 'homework.submitted',
aggregateType: "homework",
eventType: "homework.submitted",
payload: JSON.stringify({ id }),
status: 'pending',
status: "pending",
},
tx,
);