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; categoryRatios: Record; createdAt: string; updatedAt: string; } type RawSubjectRow = { id: string; name: string; typeRatios: string; categoryRatios: string; totalScore: number; timeLimitMinutes: number; createdAt: string; updatedAt: string; }; const parseJson = (value: string, fallback: T): T => { try { return JSON.parse(value) as T; } catch { return fallback; } }; const validateRatiosSum100 = (ratios: Record, 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 { 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>(row.typeRatios, { single: 40, multiple: 30, judgment: 20, text: 10 }), categoryRatios: parseJson>(row.categoryRatios, { 通用: 100 }), createdAt: row.createdAt, updatedAt: row.updatedAt })); } static async findById(id: string): Promise { 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>(row.typeRatios, { single: 40, multiple: 30, judgment: 20, text: 10 }), categoryRatios: parseJson>(row.categoryRatios, { 通用: 100 }), createdAt: row.createdAt, updatedAt: row.updatedAt }; } static async create(data: { name: string; totalScore: number; timeLimitMinutes?: number; typeRatios: Record; categoryRatios?: Record; }): Promise { 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; categoryRatios?: Record; }): Promise { 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 { 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]); } }