Files
Edu/services/core-edu/src/exams/exams.controller.ts
SpecialX 58c0ba1bd9 feat(core-edu): 完整实现 core-edu 教学核心服务
包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
2026-07-10 19:08:56 +08:00

216 lines
6.0 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Req,
} from "@nestjs/common";
import { z } from "zod";
import {
ExamsService,
type CreateExamInput,
type UpdateExamInput,
} from "./exams.service.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
import {
UnauthorizedError,
ValidationError,
} from "../shared/errors/application-error.js";
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: 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 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 };
}
@Get(":id")
@RequirePermission(Permissions.EXAM_READ)
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<ExamsService["getExam"]>>;
}> {
const data = await this.examsService.getExam(id);
return { success: true, data };
}
@Get("class/:classId")
@RequirePermission(Permissions.EXAM_READ)
async listByClass(@Param("classId") classId: string): Promise<{
success: true;
data: Awaited<ReturnType<ExamsService["listExamsByClass"]>>;
}> {
const data = await this.examsService.listExamsByClass(classId);
return { success: true, data };
}
@Put(":id")
@RequirePermission(Permissions.EXAM_UPDATE)
async update(
@Param("id") id: string,
@Body() body: unknown,
): Promise<{ success: true; data: { success: true } }> {
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 } };
}
@Delete(":id")
@RequirePermission(Permissions.EXAM_DELETE)
async remove(
@Param("id") id: string,
): Promise<{ success: true; data: { success: true } }> {
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 } };
}
}