Files
Web_BLV_OA_Exam_Prod/api/models/question.ts

320 lines
8.5 KiB
TypeScript
Raw Normal View History

import { v4 as uuidv4 } from 'uuid';
import { query, run, get } from '../database';
export interface Question {
id: string;
content: string;
type: 'single' | 'multiple' | 'judgment' | 'text';
category: string;
options?: string[];
answer: string | string[];
score: number;
createdAt: string;
}
export interface CreateQuestionData {
content: string;
type: 'single' | 'multiple' | 'judgment' | 'text';
category?: string;
options?: string[];
answer: string | string[];
score: number;
}
export interface ExcelQuestionData {
content: string;
type: string;
category?: string;
answer: string;
score: number;
options?: string[];
}
export class QuestionModel {
// 创建题目
static async create(data: CreateQuestionData): Promise<Question> {
const id = uuidv4();
const optionsStr = data.options ? JSON.stringify(data.options) : null;
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
const sql = `
INSERT INTO questions (id, content, type, options, answer, score, category)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
await run(sql, [id, data.content, data.type, optionsStr, answerStr, data.score, category]);
return this.findById(id) as Promise<Question>;
}
// 批量创建题目
static async createMany(questions: CreateQuestionData[]): Promise<{ success: number; errors: string[] }> {
const errors: string[] = [];
let success = 0;
for (let i = 0; i < questions.length; i++) {
try {
await this.create(questions[i]);
success++;
} catch (error: any) {
errors.push(`${i + 1}题: ${error.message}`);
}
}
return { success, errors };
}
// 根据ID查找题目
static async findById(id: string): Promise<Question | null> {
const sql = `SELECT * FROM questions WHERE id = ?`;
const question = await get(sql, [id]);
if (!question) return null;
return this.formatQuestion(question);
}
// 根据题目内容查找题目
static async findByContent(content: string): Promise<Question | null> {
const sql = `SELECT * FROM questions WHERE content = ?`;
const question = await get(sql, [content]);
if (!question) return null;
return this.formatQuestion(question);
}
// 获取题目列表(支持筛选和分页)
static async findAll(filters: {
type?: string;
category?: string;
keyword?: string;
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
} = {}): Promise<{ questions: Question[]; total: number }> {
const { type, category, keyword, startDate, endDate, limit = 10, offset = 0 } = filters;
const whereParts: string[] = [];
const params: any[] = [];
if (type) {
whereParts.push('type = ?');
params.push(type);
}
if (category) {
whereParts.push('category = ?');
params.push(category);
}
if (keyword) {
whereParts.push('content LIKE ?');
params.push(`%${keyword}%`);
}
if (startDate) {
whereParts.push('created_at >= ?');
params.push(`${startDate} 00:00:00`);
}
if (endDate) {
whereParts.push('created_at <= ?');
params.push(`${endDate} 23:59:59`);
}
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
const questionsSql = `
SELECT * FROM questions
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const countSql = `
SELECT COUNT(*) as total FROM questions ${whereClause}
`;
const [questions, countResult] = await Promise.all([
query(questionsSql, [...params, limit, offset]),
get(countSql, params)
]);
return {
questions: questions.map((q) => this.formatQuestion(q)),
total: countResult.total
};
}
// 随机获取题目(按类型和数量)
static async getRandomQuestions(type: string, count: number, categories?: string[]): Promise<Question[]> {
const whereParts: string[] = ['type = ?'];
const params: any[] = [type];
if (categories && categories.length > 0) {
whereParts.push(`category IN (${categories.map(() => '?').join(',')})`);
params.push(...categories);
}
const sql = `
SELECT * FROM questions
WHERE ${whereParts.join(' AND ')}
ORDER BY RANDOM()
LIMIT ?
`;
const questions = await query(sql, [...params, count]);
return questions.map((q) => this.formatQuestion(q));
}
// 更新题目
static async update(id: string, data: Partial<CreateQuestionData>): Promise<Question> {
const fields: string[] = [];
const values: any[] = [];
if (data.content) {
fields.push('content = ?');
values.push(data.content);
}
if (data.type) {
fields.push('type = ?');
values.push(data.type);
}
if (data.options !== undefined) {
fields.push('options = ?');
values.push(data.options ? JSON.stringify(data.options) : null);
}
if (data.answer !== undefined) {
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
fields.push('answer = ?');
values.push(answerStr);
}
if (data.score !== undefined) {
fields.push('score = ?');
values.push(data.score);
}
if (data.category !== undefined) {
fields.push('category = ?');
values.push(data.category && data.category.trim() ? data.category.trim() : '通用');
}
if (fields.length === 0) {
throw new Error('没有要更新的字段');
}
values.push(id);
const sql = `UPDATE questions SET ${fields.join(', ')} WHERE id = ?`;
await run(sql, values);
return this.findById(id) as Promise<Question>;
}
// 删除题目
static async delete(id: string): Promise<boolean> {
const sql = `DELETE FROM questions WHERE id = ?`;
const result = await run(sql, [id]);
return result.id !== undefined;
}
// 格式化题目数据
private static formatQuestion(row: any): Question {
return {
id: row.id,
content: row.content,
type: row.type,
category: row.category || '通用',
options: row.options ? JSON.parse(row.options) : undefined,
answer: this.parseAnswer(row.answer, row.type),
score: row.score,
createdAt: row.created_at
};
}
// 解析答案
private static parseAnswer(answerStr: string, type: string): string | string[] {
if (type === 'multiple') {
try {
return JSON.parse(answerStr);
} catch {
return answerStr;
}
}
return answerStr;
}
// 验证题目数据
static validateQuestionData(data: CreateQuestionData): string[] {
const errors: string[] = [];
// 验证题目内容
if (!data.content || data.content.trim().length === 0) {
errors.push('题目内容不能为空');
}
// 验证题型
const validTypes = ['single', 'multiple', 'judgment', 'text'];
if (!validTypes.includes(data.type)) {
errors.push('题型必须是 single、multiple、judgment 或 text');
}
// 验证选项
if (data.type === 'single' || data.type === 'multiple') {
if (!data.options || data.options.length < 2) {
errors.push('单选题和多选题必须至少包含2个选项');
}
}
// 验证答案
if (!data.answer) {
errors.push('答案不能为空');
}
// 验证分值
if (!data.score || data.score <= 0) {
errors.push('分值必须是正数');
}
if (data.category !== undefined && data.category.trim().length === 0) {
errors.push('题目类别不能为空');
}
return errors;
}
// 验证Excel数据格式
static validateExcelData(data: ExcelQuestionData[]): { valid: boolean; errors: string[] } {
const errors: string[] = [];
data.forEach((row, index) => {
if (!row.content) {
errors.push(`${index + 1}行:题目内容不能为空`);
}
const validTypes = ['single', 'multiple', 'judgment', 'text'];
if (!validTypes.includes(row.type)) {
errors.push(`${index + 1}行:题型必须是 single、multiple、judgment 或 text`);
}
if (!row.answer) {
errors.push(`${index + 1}行:答案不能为空`);
}
if (!row.score || row.score <= 0) {
errors.push(`${index + 1}行:分值必须是正数`);
}
});
return {
valid: errors.length === 0,
errors
};
}
}