第一版提交,答题功能OK,题库管理待完善

This commit is contained in:
2025-12-18 19:07:21 +08:00
parent e5600535be
commit ba252b2f56
93 changed files with 20431 additions and 1 deletions

View File

@@ -0,0 +1,324 @@
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;
}
}