第一版提交,答题功能OK,题库管理待完善
This commit is contained in:
218
api/models/examSubject.ts
Normal file
218
api/models/examSubject.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user