Files
Web_BLV_OA_Exam_Prod/api/controllers/questionController.ts

325 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}