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;
|
||
}
|
||
}
|