feat(core-edu): 完整实现 core-edu 教学核心服务
包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Put,
|
||||
Req,
|
||||
} from "@nestjs/common";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ExamsService,
|
||||
type CreateExamInput,
|
||||
@@ -18,26 +19,75 @@ import {
|
||||
RequirePermission,
|
||||
} from "../middleware/permission.guard.js";
|
||||
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
|
||||
import { UnauthorizedError } from "../shared/errors/application-error.js";
|
||||
import {
|
||||
UnauthorizedError,
|
||||
ValidationError,
|
||||
} from "../shared/errors/application-error.js";
|
||||
|
||||
@Controller("exams")
|
||||
const createExamSchema = z.object({
|
||||
classId: z.string().min(1),
|
||||
subjectId: z.string().min(1),
|
||||
title: z.string().min(1).max(200),
|
||||
description: z.string().optional(),
|
||||
examDate: z.string().min(1),
|
||||
duration: z.number().int().positive(),
|
||||
totalScore: z.string().min(1),
|
||||
schoolId: z.string().min(1),
|
||||
});
|
||||
|
||||
const updateExamSchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
description: z.string().optional(),
|
||||
examDate: z.string().min(1).optional(),
|
||||
duration: z.number().int().positive().optional(),
|
||||
totalScore: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
const answerInputSchema = z.object({
|
||||
questionId: z.string().min(1),
|
||||
answer: z.string(),
|
||||
});
|
||||
|
||||
const scoreInputSchema = z.object({
|
||||
questionId: z.string().min(1),
|
||||
score: z.string().min(1),
|
||||
teacherComment: z.string().optional(),
|
||||
});
|
||||
|
||||
const submitExamSchema = z.object({
|
||||
studentId: z.string().min(1),
|
||||
answers: z.array(answerInputSchema).default([]),
|
||||
});
|
||||
|
||||
const gradeExamSchema = z.object({
|
||||
submissionId: z.string().min(1),
|
||||
scores: z.array(scoreInputSchema).min(1),
|
||||
});
|
||||
|
||||
@Controller("v1/exams")
|
||||
export class ExamsController {
|
||||
constructor(private readonly examsService: ExamsService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(Permissions.EXAM_CREATE)
|
||||
async create(
|
||||
@Body() body: CreateExamInput,
|
||||
@Body() body: unknown,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<{ success: true; data: { id: string } }> {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError("Missing x-user-id header");
|
||||
}
|
||||
const result = await this.examsService.createExam({
|
||||
...body,
|
||||
const parsed = createExamSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError("Invalid exam input", parsed.error.flatten());
|
||||
}
|
||||
const input: CreateExamInput = {
|
||||
...parsed.data,
|
||||
examDate: new Date(parsed.data.examDate),
|
||||
createdBy: userId,
|
||||
});
|
||||
};
|
||||
const result = await this.examsService.createExam(input, userId);
|
||||
return { success: true, data: result };
|
||||
}
|
||||
|
||||
@@ -65,9 +115,22 @@ export class ExamsController {
|
||||
@RequirePermission(Permissions.EXAM_UPDATE)
|
||||
async update(
|
||||
@Param("id") id: string,
|
||||
@Body() body: UpdateExamInput,
|
||||
@Body() body: unknown,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
await this.examsService.updateExam(id, body);
|
||||
const parsed = updateExamSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError(
|
||||
"Invalid exam update input",
|
||||
parsed.error.flatten(),
|
||||
);
|
||||
}
|
||||
const data: UpdateExamInput = {
|
||||
...parsed.data,
|
||||
examDate: parsed.data.examDate
|
||||
? new Date(parsed.data.examDate)
|
||||
: undefined,
|
||||
};
|
||||
await this.examsService.updateExam(id, data);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
|
||||
@@ -79,4 +142,74 @@ export class ExamsController {
|
||||
await this.examsService.deleteExam(id);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
|
||||
@Post(":id/publish")
|
||||
@RequirePermission(Permissions.EXAM_PUBLISH)
|
||||
async publish(
|
||||
@Param("id") id: string,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError("Missing x-user-id header");
|
||||
}
|
||||
await this.examsService.publishExam(id, userId);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
|
||||
@Post(":id/submit")
|
||||
@RequirePermission(Permissions.EXAM_SUBMIT)
|
||||
async submit(
|
||||
@Param("id") id: string,
|
||||
@Body() body: unknown,
|
||||
): Promise<{ success: true; data: { submissionId: string } }> {
|
||||
const parsed = submitExamSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError("Invalid submit input", parsed.error.flatten());
|
||||
}
|
||||
const result = await this.examsService.submitExam(
|
||||
id,
|
||||
parsed.data.studentId,
|
||||
parsed.data.answers,
|
||||
);
|
||||
return { success: true, data: result };
|
||||
}
|
||||
|
||||
@Post(":id/grade")
|
||||
@RequirePermission(Permissions.EXAM_GRADE)
|
||||
async grade(
|
||||
@Param("id") id: string,
|
||||
@Body() body: unknown,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<{ success: true; data: { totalScore: string } }> {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError("Missing x-user-id header");
|
||||
}
|
||||
const parsed = gradeExamSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError("Invalid grade input", parsed.error.flatten());
|
||||
}
|
||||
const result = await this.examsService.gradeExam(
|
||||
id,
|
||||
parsed.data.submissionId,
|
||||
parsed.data.scores,
|
||||
userId,
|
||||
);
|
||||
return { success: true, data: result };
|
||||
}
|
||||
|
||||
@Post(":id/archive")
|
||||
@RequirePermission(Permissions.EXAM_UPDATE)
|
||||
async archive(
|
||||
@Param("id") id: string,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError("Missing x-user-id header");
|
||||
}
|
||||
await this.examsService.archiveExam(id, userId);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user