import { Request, Response } from 'express'; import { QuestionModel, CreateQuestionData, ExcelQuestionData } from '../models'; import * as XLSX from 'xlsx'; export class QuestionController { // 获取题目列表 static async getQuestions(req: Request, res: Response) { try { const { type, category, keyword, startDate, endDate, page = 1, limit = 10 } = req.query; const result = await QuestionModel.findAll({ type: type as string, category: category as string, keyword: keyword as string, startDate: startDate as string, endDate: endDate as string, limit: parseInt(limit as string), offset: (parseInt(page as string) - 1) * parseInt(limit as string) }); res.json({ success: true, data: result.questions, pagination: { page: parseInt(page as string), limit: parseInt(limit as string), total: result.total, pages: Math.ceil(result.total / parseInt(limit as string)) } }); } catch (error: any) { console.error('获取题目列表失败:', error); res.status(500).json({ success: false, message: error.message || '获取题目列表失败' }); } } // 获取单个题目 static async getQuestion(req: Request, res: Response) { try { const { id } = req.params; const question = await QuestionModel.findById(id); if (!question) { return res.status(404).json({ success: false, message: '题目不存在' }); } res.json({ success: true, data: question }); } catch (error: any) { console.error('获取题目失败:', error); res.status(500).json({ success: false, message: error.message || '获取题目失败' }); } } // 创建题目 static async createQuestion(req: Request, res: Response) { try { const { content, type, category, options, answer, analysis, score } = req.body; const questionData: CreateQuestionData = { content, type, category, options, answer, analysis, score }; // 验证题目数据 const errors = QuestionModel.validateQuestionData(questionData); if (errors.length > 0) { return res.status(400).json({ success: false, message: '数据验证失败', errors }); } const question = await QuestionModel.create(questionData); res.json({ success: true, data: question }); } catch (error: any) { console.error('创建题目失败:', error); res.status(500).json({ success: false, message: error.message || '创建题目失败' }); } } // 更新题目 static async updateQuestion(req: Request, res: Response) { try { const { id } = req.params; const { content, type, category, options, answer, analysis, score } = req.body; const updateData: Partial = {}; if (content !== undefined) updateData.content = content; if (type !== undefined) updateData.type = type; if (category !== undefined) updateData.category = category; if (options !== undefined) updateData.options = options; if (answer !== undefined) updateData.answer = answer; if (analysis !== undefined) updateData.analysis = analysis; if (score !== undefined) updateData.score = score; const question = await QuestionModel.update(id, updateData); res.json({ success: true, data: question }); } catch (error: any) { console.error('更新题目失败:', error); res.status(500).json({ success: false, message: error.message || '更新题目失败' }); } } // 删除题目 static async deleteQuestion(req: Request, res: Response) { try { const { id } = req.params; const success = await QuestionModel.delete(id); if (!success) { return res.status(404).json({ success: false, message: '题目不存在' }); } res.json({ success: true, message: '删除成功' }); } catch (error: any) { console.error('删除题目失败:', error); res.status(500).json({ success: false, message: error.message || '删除题目失败' }); } } // Excel导入题目 static async importQuestions(req: Request, res: Response) { try { if (!req.file) { return res.status(400).json({ success: false, message: '请上传Excel文件' }); } // 读取Excel文件 const workbook = XLSX.read(req.file.buffer, { type: 'buffer' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; // 转换为JSON数据 const rawData = XLSX.utils.sheet_to_json(worksheet); // 转换数据格式 const questionsData: ExcelQuestionData[] = rawData.map((row: any) => ({ content: row['题目内容'] || row['content'], type: QuestionController.mapQuestionType(row['题型'] || row['type']), category: row['题目类别'] || row['category'] || '通用', answer: row['标准答案'] || row['answer'], analysis: row['解析'] || row['analysis'] || '', score: parseInt(row['分值'] || row['score']) || 0, options: QuestionController.parseOptions(row['选项'] || row['options']) })); // 验证数据 const validation = QuestionModel.validateExcelData(questionsData); if (!validation.valid) { return res.status(400).json({ success: false, message: 'Excel数据格式错误', errors: validation.errors }); } // 批量创建题目 const result = await QuestionModel.createMany(questionsData as any[]); res.json({ success: true, data: { imported: result.success, total: questionsData.length, errors: result.errors } }); } catch (error: any) { console.error('Excel导入失败:', error); res.status(500).json({ success: false, message: error.message || 'Excel导入失败' }); } } static async importTextQuestions(req: Request, res: Response) { try { const mode = req.body?.mode as 'overwrite' | 'incremental'; const rawQuestions = req.body?.questions as any[]; if (mode !== 'overwrite' && mode !== 'incremental') { return res.status(400).json({ success: false, message: '导入模式不合法', }); } if (!Array.isArray(rawQuestions) || rawQuestions.length === 0) { return res.status(400).json({ success: false, message: '题目列表不能为空', }); } const errors: string[] = []; const normalized = new Map(); const normalizeAnswer = (type: string, answer: unknown): string | string[] => { if (type === 'multiple') { if (Array.isArray(answer)) { return answer.map((a) => String(a).trim()).filter(Boolean); } return String(answer || '') .split(/[|,,、\s]+/g) .map((s) => s.trim()) .filter(Boolean); } if (Array.isArray(answer)) { return String(answer[0] ?? '').trim(); } return String(answer ?? '').trim(); }; const normalizeJudgment = (answer: string) => { const v = String(answer || '').trim(); const yes = new Set(['正确', '对', '是', 'true', 'True', 'TRUE', '1', 'Y', 'y', 'yes', 'YES']); const no = new Set(['错误', '错', '否', '不是', 'false', 'False', 'FALSE', '0', 'N', 'n', 'no', 'NO']); if (yes.has(v)) return '正确'; if (no.has(v)) return '错误'; return v; }; for (let i = 0; i < rawQuestions.length; i++) { const q = rawQuestions[i] || {}; const content = String(q.content ?? '').trim(); if (!content) { errors.push(`第${i + 1}题:题目内容不能为空`); continue; } const type = QuestionController.mapQuestionType(String(q.type ?? '').trim()); const category = String(q.category ?? '通用').trim() || '通用'; const score = Number(q.score); const options = Array.isArray(q.options) ? q.options.map((o: any) => String(o).trim()).filter(Boolean) : undefined; let answer = normalizeAnswer(type, q.answer); const analysis = String(q.analysis ?? '').trim(); if (type === 'judgment' && typeof answer === 'string') { answer = normalizeJudgment(answer); } const questionData: CreateQuestionData = { content, type: type as any, category, options: type === 'single' || type === 'multiple' ? options : undefined, answer: answer as any, analysis, score, }; const validationErrors = QuestionModel.validateQuestionData(questionData); if (validationErrors.length > 0) { errors.push(`第${i + 1}题:${validationErrors.join(';')}`); continue; } normalized.set(content, questionData); } if (errors.length > 0) { return res.status(400).json({ success: false, message: '数据验证失败', errors, }); } const result = await QuestionModel.importFromText(mode, Array.from(normalized.values())); res.json({ success: true, data: { mode, total: normalized.size, inserted: result.inserted, updated: result.updated, errors: result.errors, cleared: result.cleared ?? undefined, }, }); } catch (error: any) { console.error('文本导入失败:', error); res.status(500).json({ success: false, message: error.message || '文本导入失败', }); } } // 映射题型 private static mapQuestionType(type: string): string { const typeMap: { [key: string]: string } = { '单选': 'single', '多选': 'multiple', '判断': 'judgment', '文字描述': 'text', 'single': 'single', 'multiple': 'multiple', 'judgment': 'judgment', 'text': 'text' }; return typeMap[type] || 'single'; } // 解析选项 private static parseOptions(optionsStr: string): string[] | undefined { if (!optionsStr) return undefined; // 支持多种分隔符:|、;、\n const separators = ['|', ';', '\n']; for (const separator of separators) { if (optionsStr.includes(separator)) { return optionsStr.split(separator).map(opt => opt.trim()).filter(opt => opt); } } // 如果没有找到分隔符,返回单个选项 return [optionsStr.trim()]; } // Excel导出题目 static async exportQuestions(req: Request, res: Response) { try { const { type, category } = req.query; // 获取所有题目数据(使用大的limit值获取所有题目) const result = await QuestionModel.findAll({ type: type as string, category: category as string, limit: 10000, // 使用大的limit值获取所有题目 offset: 0 }); const questions = result.questions; // 转换为Excel数据格式 const excelData = questions.map((question: any) => ({ '题目ID': question.id, '题目内容': question.content, '题型': this.getQuestionTypeLabel(question.type), '题目类别': question.category || '通用', '选项': question.options ? question.options.join('|') : '', '标准答案': question.answer, '解析': question.analysis || '', '分值': question.score, '创建时间': new Date(question.createdAt).toLocaleString() })); // 创建Excel工作簿和工作表 const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.json_to_sheet(excelData); // 设置列宽 const columnWidths = [ { wch: 10 }, // 题目ID { wch: 60 }, // 题目内容 { wch: 10 }, // 题型 { wch: 15 }, // 题目类别 { wch: 80 }, // 选项 { wch: 20 }, // 标准答案 { wch: 8 }, // 分值 { wch: 20 } // 创建时间 ]; worksheet['!cols'] = columnWidths; // 将工作表添加到工作簿 XLSX.utils.book_append_sheet(workbook, worksheet, '题目列表'); // 设置响应头 res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename=questions_${new Date().getTime()}.xlsx`); // 生成Excel文件并发送 const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); res.send(excelBuffer); } catch (error: any) { console.error('Excel导出失败:', error); res.status(500).json({ success: false, message: error.message || 'Excel导出失败' }); } } // 获取题型中文标签 private static getQuestionTypeLabel(type: string): string { const typeMap: { [key: string]: string } = { 'single': '单选题', 'multiple': '多选题', 'judgment': '判断题', 'text': '文字题' }; return typeMap[type] || type; } }