feat(core-edu): admin/dashboard/leave-requests 模块 + gRPC + 状态机测试 + nextstep 文档
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { db } from "../config/database.js";
|
||||
import { exams } from "./exams.schema.js";
|
||||
import { examDrafts, examViolations } from "./exam-extensions.schema.js";
|
||||
import { examsRepository } from "./exams.repository.js";
|
||||
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
|
||||
import { buildEvent, serializeEvent } from "../shared/outbox/event-builder.js";
|
||||
@@ -391,6 +392,92 @@ export class ExamsService {
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// P3.13 新增:考试草稿自动保存(upsert by exam_id + student_id)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
async saveExamDraft(
|
||||
examId: string,
|
||||
studentId: string,
|
||||
answers: AnswerInput[],
|
||||
): Promise<{ draftId: string }> {
|
||||
if (!examId || !studentId) {
|
||||
throw new ValidationError("examId and studentId are required");
|
||||
}
|
||||
// 校验考试存在
|
||||
const exam = await examsRepository.findById(examId);
|
||||
if (!exam) {
|
||||
throw new NotFoundError(`Exam ${examId} not found`);
|
||||
}
|
||||
|
||||
const answersPayload = answers.map((a) => ({
|
||||
questionId: a.questionId,
|
||||
answer: a.answer,
|
||||
}));
|
||||
|
||||
// 查找已有草稿(unique key: exam_id + student_id)
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(examDrafts)
|
||||
.where(
|
||||
and(eq(examDrafts.examId, examId), eq(examDrafts.studentId, studentId)),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const draftRow = existing[0];
|
||||
if (draftRow) {
|
||||
const draftId = draftRow.id;
|
||||
await db
|
||||
.update(examDrafts)
|
||||
.set({ answers: answersPayload })
|
||||
.where(eq(examDrafts.id, draftId));
|
||||
return { draftId };
|
||||
}
|
||||
|
||||
const draftId = randomUUID();
|
||||
await db.insert(examDrafts).values({
|
||||
id: draftId,
|
||||
examId,
|
||||
studentId,
|
||||
answers: answersPayload,
|
||||
});
|
||||
return { draftId };
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// P3.13 新增:考试违规事件记录(防作弊)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
async recordExamViolation(
|
||||
examId: string,
|
||||
studentId: string,
|
||||
violationType: string,
|
||||
detail: string,
|
||||
severity: number,
|
||||
): Promise<{ violationId: string }> {
|
||||
if (!examId || !studentId || !violationType) {
|
||||
throw new ValidationError(
|
||||
"examId, studentId, violationType are required",
|
||||
);
|
||||
}
|
||||
// 校验考试存在
|
||||
const exam = await examsRepository.findById(examId);
|
||||
if (!exam) {
|
||||
throw new NotFoundError(`Exam ${examId} not found`);
|
||||
}
|
||||
|
||||
const violationId = randomUUID();
|
||||
await db.insert(examViolations).values({
|
||||
id: violationId,
|
||||
examId,
|
||||
studentId,
|
||||
violationType,
|
||||
detail: detail || null,
|
||||
severity: severity || 1,
|
||||
});
|
||||
return { violationId };
|
||||
}
|
||||
|
||||
private assertTransition(from: ExamStatus, action: ExamAction): void {
|
||||
if (!canTransition(from, action)) {
|
||||
throw new ApplicationError(
|
||||
|
||||
Reference in New Issue
Block a user