361 lines
9.6 KiB
JavaScript
361 lines
9.6 KiB
JavaScript
const fs = require('fs');
|
||
const xlsx = require('xlsx');
|
||
const mammoth = require('mammoth');
|
||
const db = require('../db/index');
|
||
|
||
// 答案转换函数
|
||
function convertAnswer(answer, type) {
|
||
answer = answer.replace(/\s/g, '');
|
||
// 如果是多选题
|
||
if (type === 'checkbox') {
|
||
let answers;
|
||
if (answer.includes(',')) {
|
||
answers = answer.split(',');
|
||
} else {
|
||
answers = answer.split('');
|
||
}
|
||
|
||
return answers
|
||
.map(a => convertSingleAnswer(a))
|
||
.filter(a => a !== null)
|
||
.sort((a, b) => Number(a) - Number(b))
|
||
.join(',');
|
||
}
|
||
// 单选题直接转换
|
||
return convertSingleAnswer(answer);
|
||
}
|
||
|
||
// 单个答案转换
|
||
function convertSingleAnswer(answer) {
|
||
const letterMap = {
|
||
'A': '0',
|
||
'B': '1',
|
||
'C': '2',
|
||
'D': '3',
|
||
'E': '4',
|
||
'F': '5'
|
||
};
|
||
const numberMap = {
|
||
'一': '0',
|
||
'二': '1',
|
||
'三': '2',
|
||
'四': '3',
|
||
'五': '4',
|
||
'六': '5'
|
||
};
|
||
// 检查字母映射
|
||
if (letterMap[answer.toUpperCase()]) {
|
||
return letterMap[answer.toUpperCase()];
|
||
}
|
||
// 检查数字文字映射
|
||
if (numberMap[answer]) {
|
||
return numberMap[answer];
|
||
}
|
||
|
||
return answer;
|
||
}
|
||
|
||
// 导入JSON题库
|
||
async function importJsonQuestions(jsonFilePath) {
|
||
try {
|
||
const content = fs.readFileSync(jsonFilePath, 'utf8');
|
||
const paperData = JSON.parse(content);
|
||
await importQuestions(paperData);
|
||
} catch (error) {
|
||
console.error('导入失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理Excel文件
|
||
async function importExcelQuestions(excelFilePath) {
|
||
try {
|
||
const workbook = xlsx.readFile(excelFilePath);
|
||
const sheetName = workbook.SheetNames[0];
|
||
const worksheet = workbook.Sheets[sheetName];
|
||
const data = xlsx.utils.sheet_to_json(worksheet);
|
||
|
||
const paperData = {
|
||
title: '从Excel导入的试卷',
|
||
description: '从Excel文件导入的试题',
|
||
topics: data.map(row => ({
|
||
title: row.title || row['题目'],
|
||
type: row.type || row['类型'] || 'radio',
|
||
options: [
|
||
`A:${row.A || row['选项A']}`,
|
||
`B:${row.B || row['选项B']}`,
|
||
`C:${row.C || row['选项C']}`,
|
||
`D:${row.D || row['选项D']}`
|
||
],
|
||
answer: row.answer || row['答案']
|
||
}))
|
||
};
|
||
|
||
await importQuestions(paperData);
|
||
} catch (error) {
|
||
console.error('Excel导入失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理Word文件
|
||
async function importWordQuestions(wordFilePath) {
|
||
try {
|
||
const result = await mammoth.extractRawText({ path: wordFilePath });
|
||
const text = result.value;
|
||
const questions = text.split('\n\n').filter(q => q.trim());
|
||
|
||
const paperData = {
|
||
title: '从Word导入的试卷',
|
||
description: '从Word文件导入的试题',
|
||
topics: questions.map(q => {
|
||
const lines = q.split('\n');
|
||
return {
|
||
title: lines[0].replace(/^\d+[\.、]?\s*/, ''),
|
||
type: lines[0].includes('多选') ? 'checkbox' : 'radio',
|
||
options: lines.slice(1, -1).map((opt, idx) =>
|
||
`${String.fromCharCode(65 + idx)}:${opt.replace(/^[A-Z][\.、]?\s*/, '')}`
|
||
),
|
||
answer: lines[lines.length - 1].replace(/^答案[::]\s*/, '')
|
||
};
|
||
})
|
||
};
|
||
|
||
await importQuestions(paperData);
|
||
} catch (error) {
|
||
console.error('Word导入失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理txt文件
|
||
async function importTxtQuestions(txtFilePath) {
|
||
try {
|
||
const content = fs.readFileSync(txtFilePath, 'utf8');
|
||
const questions = content.split('-------------------\n\n');
|
||
|
||
const paperData = {
|
||
title: '从TXT导入的试卷',
|
||
description: '从文本文件导入的试题',
|
||
topics: questions.map(q => {
|
||
const titleMatch = q.match(/题目:(.*?)\n/);
|
||
const typeMatch = q.match(/类型:(.*?)\n/);
|
||
const optionsMatch = q.match(/选项:\n(.*?)\n答案:/s);
|
||
const answerMatch = q.match(/答案:(.*?)(\n|$)/);
|
||
|
||
if (!titleMatch || !typeMatch || !optionsMatch || !answerMatch) return null;
|
||
|
||
return {
|
||
title: titleMatch[1],
|
||
type: typeMatch[1].includes('多选') ? 'checkbox' : 'radio',
|
||
options: optionsMatch[1].split('\n')
|
||
.filter(opt => opt.trim())
|
||
.map(opt => {
|
||
const [key, value] = opt.split('.');
|
||
return `${key.trim()}:${value ? value.trim() : ''}`;
|
||
}),
|
||
answer: answerMatch[1]
|
||
};
|
||
}).filter(Boolean)
|
||
};
|
||
|
||
await importQuestions(paperData);
|
||
} catch (error) {
|
||
console.error('TXT导入失败:', error);
|
||
}
|
||
}
|
||
|
||
// 通用导入函数
|
||
async function importQuestions(paperData) {
|
||
const connection = await db.promise();
|
||
|
||
try {
|
||
await connection.beginTransaction();
|
||
const topicsIds = [];
|
||
for (const topic of paperData.topics) {
|
||
const convertedAnswer = convertAnswer(topic.answer, topic.type);
|
||
const [result] = await connection.execute(
|
||
'INSERT INTO topics (title, type, options, answer) VALUES (?, ?, ?, ?)',
|
||
[
|
||
topic.title,
|
||
topic.type,
|
||
JSON.stringify(topic.options),
|
||
convertedAnswer
|
||
]
|
||
);
|
||
topicsIds.push(result.insertId);
|
||
}
|
||
|
||
await connection.execute(
|
||
'INSERT INTO paper (title, description, TopicsId, date, state, userId) VALUES (?, ?, ?, ?, ?, ?)',
|
||
[
|
||
paperData.title,
|
||
paperData.description,
|
||
JSON.stringify(topicsIds),
|
||
new Date(),
|
||
1,
|
||
'admin'
|
||
]
|
||
);
|
||
|
||
await connection.commit();
|
||
console.log('题库导入成功');
|
||
|
||
} catch (error) {
|
||
await connection.rollback();
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
const securityCheck = {
|
||
// 允许的文件类型
|
||
allowedTypes: {
|
||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'xls': 'application/vnd.ms-excel',
|
||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'doc': 'application/msword',
|
||
'txt': 'text/plain'
|
||
},
|
||
|
||
// 文件大小限制(10MB)
|
||
maxFileSize: 10 * 1024 * 1024,
|
||
|
||
checkFileType(filePath) {
|
||
const ext = filePath.toLowerCase().split('.').pop();
|
||
if (!this.allowedTypes[ext]) {
|
||
throw new Error('不支持的文件类型');
|
||
}
|
||
return ext;
|
||
},
|
||
|
||
checkFileSize(filePath) {
|
||
const stats = fs.statSync(filePath);
|
||
if (stats.size > this.maxFileSize) {
|
||
throw new Error('文件大小超过限制');
|
||
}
|
||
},
|
||
|
||
// 检查文件内容
|
||
async checkFileContent(filePath, ext) {
|
||
const buffer = Buffer.alloc(8);
|
||
const fd = await fs.promises.open(filePath, 'r');
|
||
await fd.read(buffer, 0, 8, 0);
|
||
await fd.close();
|
||
|
||
// 文件头签名
|
||
const signatures = {
|
||
'xlsx': Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
||
'docx': Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
||
'txt': Buffer.from([0xEF, 0xBB, 0xBF])
|
||
};
|
||
|
||
// 检查文件头
|
||
if (signatures[ext] && !buffer.includes(signatures[ext])) {
|
||
throw new Error('文件格式不正确');
|
||
}
|
||
|
||
// 检查文件内容是否包含可疑代码
|
||
const content = await fs.promises.readFile(filePath, 'utf8');
|
||
const suspiciousPatterns = [
|
||
/<script/i,
|
||
/javascript:/i,
|
||
/eval\(/i,
|
||
/execScript/i,
|
||
/document\.write/i,
|
||
/\.exe/i,
|
||
/\.dll/i,
|
||
/\.bat/i,
|
||
/\.cmd/i,
|
||
/\.sh/i,
|
||
/\.vbs/i
|
||
];
|
||
|
||
if (suspiciousPatterns.some(pattern => pattern.test(content))) {
|
||
throw new Error('文件包含可疑内容');
|
||
}
|
||
},
|
||
|
||
// 检查JSON文件结构
|
||
async validateJsonStructure(filePath) {
|
||
try {
|
||
const content = await fs.promises.readFile(filePath, 'utf8');
|
||
const data = JSON.parse(content);
|
||
if (!data.topics || !Array.isArray(data.topics)) {
|
||
throw new Error('JSON格式错误:缺少topics数组');
|
||
}
|
||
|
||
for (const topic of data.topics) {
|
||
if (!topic.title || !topic.type || !topic.options || !topic.answer) {
|
||
throw new Error('JSON格式错误:题目结构不完整');
|
||
}
|
||
if (!['radio', 'checkbox'].includes(topic.type)) {
|
||
throw new Error('JSON格式错误:题目类型不正确');
|
||
}
|
||
if (!Array.isArray(topic.options) || topic.options.length < 2) {
|
||
throw new Error('JSON格式错误:选项格式不正确');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`JSON验证失败:${error.message}`);
|
||
}
|
||
},
|
||
|
||
// 主验证函数
|
||
async validateFile(filePath) {
|
||
try {
|
||
if (!fs.existsSync(filePath)) {
|
||
throw new Error('文件不存在');
|
||
}
|
||
// 检查文件类型
|
||
const ext = this.checkFileType(filePath);
|
||
// 检查文件大小
|
||
this.checkFileSize(filePath);
|
||
// 检查文件内容
|
||
await this.checkFileContent(filePath, ext);
|
||
// 对JSON文件进行额外的结构验证
|
||
if (ext === 'json') {
|
||
await this.validateJsonStructure(filePath);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('文件验证失败:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 统一导入入口
|
||
async function importQuestionsByFile(filePath) {
|
||
try {
|
||
await securityCheck.validateFile(filePath);
|
||
const ext = filePath.toLowerCase().split('.').pop();
|
||
|
||
switch (ext) {
|
||
case 'xlsx':
|
||
case 'xls':
|
||
await importExcelQuestions(filePath);
|
||
break;
|
||
case 'docx':
|
||
case 'doc':
|
||
await importWordQuestions(filePath);
|
||
break;
|
||
case 'txt':
|
||
await importTxtQuestions(filePath);
|
||
break;
|
||
default:
|
||
throw new Error('不支持的文件格式');
|
||
}
|
||
} catch (error) {
|
||
console.error('导入失败:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
importQuestionsByFile,
|
||
importExcelQuestions,
|
||
importWordQuestions,
|
||
importTxtQuestions,
|
||
convertAnswer,
|
||
convertSingleAnswer,
|
||
securityCheck
|
||
};
|