Files
Web_BLV_OA_Exam_Prod/api/models/examSubject.ts

219 lines
6.5 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { get, query, run } from '../database';
export type QuestionType = 'single' | 'multiple' | 'judgment' | 'text';
export interface ExamSubject {
id: string;
name: string;
totalScore: number;
timeLimitMinutes: number;
typeRatios: Record<QuestionType, number>;
categoryRatios: Record<string, number>;
createdAt: string;
updatedAt: string;
}
type RawSubjectRow = {
id: string;
name: string;
typeRatios: string;
categoryRatios: string;
totalScore: number;
timeLimitMinutes: number;
createdAt: string;
updatedAt: string;
};
const parseJson = <T>(value: string, fallback: T): T => {
try {
return JSON.parse(value) as T;
} catch {
return fallback;
}
};
const validateRatiosSum100 = (ratios: Record<string, number>, label: string) => {
const values = Object.values(ratios);
if (values.length === 0) throw new Error(`${label}不能为空`);
if (values.some((v) => typeof v !== 'number' || Number.isNaN(v) || v < 0)) {
throw new Error(`${label}必须是非负数字`);
}
const sum = values.reduce((a, b) => a + b, 0);
if (Math.abs(sum - 100) > 0.01) {
throw new Error(`${label}总和必须为100`);
}
};
export class ExamSubjectModel {
static async findAll(): Promise<ExamSubject[]> {
const sql = `
SELECT
id,
name,
type_ratios as typeRatios,
category_ratios as categoryRatios,
total_score as totalScore,
duration_minutes as timeLimitMinutes,
created_at as createdAt,
updated_at as updatedAt
FROM exam_subjects
ORDER BY created_at DESC
`;
const rows: RawSubjectRow[] = await query(sql);
return rows.map((row) => ({
id: row.id,
name: row.name,
totalScore: row.totalScore,
timeLimitMinutes: row.timeLimitMinutes,
typeRatios: parseJson<Record<QuestionType, number>>(row.typeRatios, {
single: 40,
multiple: 30,
judgment: 20,
text: 10
}),
categoryRatios: parseJson<Record<string, number>>(row.categoryRatios, { 通用: 100 }),
createdAt: row.createdAt,
updatedAt: row.updatedAt
}));
}
static async findById(id: string): Promise<ExamSubject | null> {
const sql = `
SELECT
id,
name,
type_ratios as typeRatios,
category_ratios as categoryRatios,
total_score as totalScore,
duration_minutes as timeLimitMinutes,
created_at as createdAt,
updated_at as updatedAt
FROM exam_subjects
WHERE id = ?
`;
const row: RawSubjectRow | undefined = await get(sql, [id]);
if (!row) return null;
return {
id: row.id,
name: row.name,
totalScore: row.totalScore,
timeLimitMinutes: row.timeLimitMinutes,
typeRatios: parseJson<Record<QuestionType, number>>(row.typeRatios, {
single: 40,
multiple: 30,
judgment: 20,
text: 10
}),
categoryRatios: parseJson<Record<string, number>>(row.categoryRatios, { 通用: 100 }),
createdAt: row.createdAt,
updatedAt: row.updatedAt
};
}
static async create(data: {
name: string;
totalScore: number;
timeLimitMinutes?: number;
typeRatios: Record<QuestionType, number>;
categoryRatios?: Record<string, number>;
}): Promise<ExamSubject> {
const name = data.name.trim();
if (!name) throw new Error('科目名称不能为空');
if (!data.totalScore || data.totalScore <= 0) throw new Error('总分必须为正整数');
const timeLimitMinutes = data.timeLimitMinutes ?? 60;
if (!Number.isInteger(timeLimitMinutes) || timeLimitMinutes <= 0) throw new Error('答题时间必须为正整数(分钟)');
validateRatiosSum100(data.typeRatios, '题型比重');
const categoryRatios = data.categoryRatios && Object.keys(data.categoryRatios).length > 0 ? data.categoryRatios : { 通用: 100 };
validateRatiosSum100(categoryRatios, '题目类别比重');
const id = uuidv4();
const sql = `
INSERT INTO exam_subjects (
id, name, type_ratios, category_ratios, total_score, duration_minutes, updated_at
) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
`;
try {
await run(sql, [
id,
name,
JSON.stringify(data.typeRatios),
JSON.stringify(categoryRatios),
data.totalScore,
timeLimitMinutes
]);
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('科目名称已存在');
}
throw error;
}
return (await this.findById(id)) as ExamSubject;
}
static async update(id: string, data: {
name: string;
totalScore: number;
timeLimitMinutes?: number;
typeRatios: Record<QuestionType, number>;
categoryRatios?: Record<string, number>;
}): Promise<ExamSubject> {
const existing = await this.findById(id);
if (!existing) throw new Error('科目不存在');
const name = data.name.trim();
if (!name) throw new Error('科目名称不能为空');
if (!data.totalScore || data.totalScore <= 0) throw new Error('总分必须为正整数');
const timeLimitMinutes = data.timeLimitMinutes ?? 60;
if (!Number.isInteger(timeLimitMinutes) || timeLimitMinutes <= 0) throw new Error('答题时间必须为正整数(分钟)');
validateRatiosSum100(data.typeRatios, '题型比重');
const categoryRatios = data.categoryRatios && Object.keys(data.categoryRatios).length > 0 ? data.categoryRatios : { 通用: 100 };
validateRatiosSum100(categoryRatios, '题目类别比重');
const sql = `
UPDATE exam_subjects
SET name = ?, type_ratios = ?, category_ratios = ?, total_score = ?, duration_minutes = ?, updated_at = datetime('now')
WHERE id = ?
`;
try {
await run(sql, [
name,
JSON.stringify(data.typeRatios),
JSON.stringify(categoryRatios),
data.totalScore,
timeLimitMinutes,
id
]);
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('科目名称已存在');
}
throw error;
}
return (await this.findById(id)) as ExamSubject;
}
static async delete(id: string): Promise<void> {
const existing = await this.findById(id);
if (!existing) throw new Error('科目不存在');
const taskCount = await get(`SELECT COUNT(*) as total FROM exam_tasks WHERE subject_id = ?`, [id]);
if (taskCount && taskCount.total > 0) {
throw new Error('该科目已被考试任务使用,无法删除');
}
await run(`DELETE FROM exam_subjects WHERE id = ?`, [id]);
}
}