包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
216 lines
6.0 KiB
TypeScript
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 } };
|
|
}
|
|
}
|