第一版提交,答题功能OK,题库管理待完善
This commit is contained in:
324
api/controllers/questionController.ts
Normal file
324
api/controllers/questionController.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user