stb/router/stb.js
2024-11-07 14:21:20 +08:00

361 lines
9.6 KiB
JavaScript
Raw Permalink 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.

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