325 lines
9.6 KiB
TypeScript
325 lines
9.6 KiB
TypeScript
|
|
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, score } = req.body;
|
|||
|
|
|
|||
|
|
const questionData: CreateQuestionData = {
|
|||
|
|
content,
|
|||
|
|
type,
|
|||
|
|
category,
|
|||
|
|
options,
|
|||
|
|
answer,
|
|||
|
|
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, score } = req.body;
|
|||
|
|
|
|||
|
|
const updateData: Partial<CreateQuestionData> = {};
|
|||
|
|
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 (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'],
|
|||
|
|
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导入失败'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 映射题型
|
|||
|
|
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.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;
|
|||
|
|
}
|
|||
|
|
}
|