后端
This commit is contained in:
commit
dcbc251483
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
26
app.js
Normal file
26
app.js
Normal file
@ -0,0 +1,26 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const router = require('./router/index')
|
||||
|
||||
const cors = require('cors')
|
||||
app.use(cors())
|
||||
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
|
||||
// 获取试卷列表
|
||||
app.get('/api/papers', router.getList)
|
||||
// 搜索题目
|
||||
app.get('/api/topics/search', router.searchTopics)
|
||||
// 上传试卷
|
||||
app.post('/api/papers/upload', router.uploadPaper)
|
||||
// 获取试卷详情
|
||||
app.post('/api/paperDetail', router.getPaperDetail)
|
||||
// 获取试卷题目
|
||||
app.post('/api/paperQuestions/topics', router.getPaperQuestions)
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('http://localhost:3000')
|
||||
})
|
9
db/index.js
Normal file
9
db/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
const db = require('mysql2')
|
||||
const pool = db.createPool({
|
||||
host: 'localhost',
|
||||
user: 'sgjq',
|
||||
password: '123456',
|
||||
database: 'stb2'
|
||||
})
|
||||
|
||||
module.exports = pool
|
148
index.js
Normal file
148
index.js
Normal file
@ -0,0 +1,148 @@
|
||||
const fs = require('fs');
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
const dbConfig = {
|
||||
host: 'localhost',
|
||||
user: 'root',
|
||||
password: '123456',
|
||||
database: 'stb2'
|
||||
};
|
||||
|
||||
function convertAnswerToNumber(answer, type) {
|
||||
const answerMap = {
|
||||
'A': 0, 'a': 0,
|
||||
'B': 1, 'b': 1,
|
||||
'C': 2, 'c': 2,
|
||||
'D': 3, 'd': 3,
|
||||
'E': 4, 'e': 4,
|
||||
'F': 5, 'f': 5,
|
||||
'G': 6, 'g': 6,
|
||||
'H': 7, 'h': 7,
|
||||
'I': 8, 'i': 8,
|
||||
'J': 9, 'j': 9
|
||||
};
|
||||
|
||||
if (!answer) return type === 'checkbox' ? [] : 0;
|
||||
|
||||
if (type === 'checkbox') {
|
||||
return answer.split('').map(letter => {
|
||||
if (['对', '正确', 'T', 'TRUE'].includes(letter)) return 0;
|
||||
if (['错', '错误', 'F', 'FALSE'].includes(letter)) return 1;
|
||||
return answerMap[letter] ?? 0;
|
||||
});
|
||||
} else {
|
||||
if (['对', '正确', 'T', 'TRUE'].includes(answer)) return 0;
|
||||
if (['错', '错误', 'F', 'FALSE'].includes(answer)) return 1;
|
||||
return answerMap[answer] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
function formatOption(index, value, type) {
|
||||
if (!value) return `${String.fromCharCode(65 + index)}.`;
|
||||
|
||||
if (type === 'radio' && value.match(/^(对|错|正确|错误|TRUE|FALSE|T|F)$/i)) {
|
||||
return [`A.正确`, `B.错误`];
|
||||
}
|
||||
return `${String.fromCharCode(65 + index)}.${value.trim()}`;
|
||||
}
|
||||
|
||||
function convertQuestionType(type) {
|
||||
if (!type) return 'radio';
|
||||
if (type.includes('单选')) {
|
||||
return 'radio';
|
||||
} else if (type.includes('多选')) {
|
||||
return 'checkbox';
|
||||
} else if (type.includes('判断')) {
|
||||
return 'radio';
|
||||
}
|
||||
return 'radio';
|
||||
}
|
||||
|
||||
async function insertData() {
|
||||
const connection = await mysql.createConnection(dbConfig);
|
||||
const content = fs.readFileSync('华为ICT题库提取版.txt', 'utf8');
|
||||
const questions = content.split('\n\n');
|
||||
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const topicIds = [];
|
||||
|
||||
for(const questionBlock of questions) {
|
||||
if(!questionBlock.trim()) continue;
|
||||
|
||||
const lines = questionBlock.split('\n').filter(line => line.trim());
|
||||
if(lines.length < 4) continue;
|
||||
|
||||
const title = lines[0].replace(/^题目\d+:/, '').trim() || '未知题目';
|
||||
const originalType = lines[1].replace('题目类型:', '').trim();
|
||||
const type = convertQuestionType(originalType);
|
||||
|
||||
const optionsStartIndex = lines.findIndex(line => line === '选项:');
|
||||
const optionsEndIndex = lines.findIndex(line => line.startsWith('答案:'));
|
||||
|
||||
if(optionsStartIndex === -1 || optionsEndIndex === -1) continue;
|
||||
|
||||
const optionsRaw = lines.slice(optionsStartIndex + 1, optionsEndIndex)
|
||||
.filter(line => line.trim());
|
||||
|
||||
let options;
|
||||
if (type === 'radio' && originalType.includes('判断')) {
|
||||
options = ['A.正确', 'B.错误'];
|
||||
} else {
|
||||
options = optionsRaw.map((line, index) => {
|
||||
const [, ...valueParts] = line.split(/[A-Za-z]\.\s*/);
|
||||
return formatOption(index, valueParts.join(''), type);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
if (!options || options.length === 0) {
|
||||
options = ['A.选项1'];
|
||||
}
|
||||
|
||||
const answerLetter = lines[optionsEndIndex]?.replace('答案:', '').trim();
|
||||
const answer = convertAnswerToNumber(answerLetter, type);
|
||||
|
||||
const [topicResult] = await connection.execute(
|
||||
'INSERT INTO topics (title, type, answer, options) VALUES (?, ?, ?, ?)',
|
||||
[
|
||||
title,
|
||||
type,
|
||||
JSON.stringify(answer),
|
||||
JSON.stringify(options)
|
||||
]
|
||||
);
|
||||
|
||||
topicIds.push(topicResult.insertId);
|
||||
}
|
||||
|
||||
if (topicIds.length === 0) {
|
||||
throw new Error('No valid topics were processed');
|
||||
}
|
||||
|
||||
const [paperResult] = await connection.execute(
|
||||
'INSERT INTO paper (title, description, TopicsId, date, state, userId) VALUES (?, ?, ?, NOW(), 1, ?)',
|
||||
[
|
||||
'华为ICT基础软件赛道真题题库',
|
||||
'2024-2025年最新华为ICT基础软件赛道真题题库-带解析版',
|
||||
JSON.stringify(topicIds),
|
||||
'admin'
|
||||
]
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
console.log('数据插入成功!');
|
||||
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
console.error('数据插入失败:', error);
|
||||
console.error('错误详情:', error.stack);
|
||||
throw error;
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
insertData().catch(err => {
|
||||
console.error('程序失败:', err);
|
||||
process.exit(1);
|
||||
});
|
1237
package-lock.json
generated
Normal file
1237
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "port",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.1",
|
||||
"mammoth": "^1.8.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.11.3",
|
||||
"port": "file:",
|
||||
"xlsx": "^0.18.5"
|
||||
}
|
||||
}
|
284
router/index.js
Normal file
284
router/index.js
Normal file
@ -0,0 +1,284 @@
|
||||
const db = require('../db/index')
|
||||
const multer = require('multer')
|
||||
const fs = require('fs')
|
||||
const {
|
||||
importQuestionsByFile
|
||||
} = require('./stb')
|
||||
|
||||
const sanitizeInput = {
|
||||
toPositiveInt: (value, defaultValue) => {
|
||||
const num = parseInt(value)
|
||||
return (num > 0) ? num : defaultValue
|
||||
},
|
||||
escapeSearch: (str) => {
|
||||
if (!str) return ''
|
||||
return str.replace(/[<>&"']/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, './uploads')
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, Date.now() + '-' + file.originalname)
|
||||
}
|
||||
})
|
||||
|
||||
// 文件类型过滤
|
||||
const fileFilter = (req, file, cb) => {
|
||||
if (
|
||||
file.mimetype === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || // xlsx
|
||||
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || // docx
|
||||
file.mimetype === 'text/plain'
|
||||
) {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error('不支持的文件类型'), false)
|
||||
}
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter
|
||||
})
|
||||
|
||||
// 上传试卷
|
||||
exports.uploadPaper = [
|
||||
upload.single('file'),
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.json({
|
||||
status: 1,
|
||||
message: '请选择文件'
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = req.file.path
|
||||
await importQuestionsByFile(filePath)
|
||||
fs.unlinkSync(filePath)
|
||||
|
||||
res.json({
|
||||
status: 0,
|
||||
message: '上传成功'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
if (req.file && req.file.path) {
|
||||
fs.unlinkSync(req.file.path)
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 1,
|
||||
message: '上传失败',
|
||||
error: error.message,
|
||||
format: {
|
||||
excel: "必须包含列:'题目', '选项A', '选项B', '选项C', '选项D', '答案'",
|
||||
word: `格式示例:
|
||||
题目1:问题内容
|
||||
A. 选项A内容
|
||||
B. 选项B内容
|
||||
C. 选项C内容
|
||||
D. 选项D内容
|
||||
答案:A
|
||||
|
||||
题目2:...`,
|
||||
txt: `格式示:
|
||||
<EFBFBD><EFBFBD><EFBFBD>题内容
|
||||
A. 选项A内容
|
||||
B. 选项B内容
|
||||
C. 选项C内容
|
||||
D. 选项D内容
|
||||
答案:A
|
||||
|
||||
下一道题...`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 获取试卷列表
|
||||
exports.getList = async (req, res) => {
|
||||
try {
|
||||
const [papers] = await db.promise().query('SELECT * FROM paper');
|
||||
const papersWithCount = papers.map(paper => ({
|
||||
...paper,
|
||||
topic_count: JSON.parse(paper.TopicsId || '[]').length
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: papersWithCount });
|
||||
} catch (error) {
|
||||
console.error('获取试卷列表失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取试卷列表失败' });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取指定试卷的题目列表
|
||||
exports.getPaperQuestions = async (req, res) => {
|
||||
try {
|
||||
const { paperId, page = 1, pageSize = 10 } = req.body;
|
||||
|
||||
if (!paperId) {
|
||||
return res.status(400).json({ success: false, message: '无效的试卷ID' });
|
||||
}
|
||||
|
||||
const sanitizedPaperId = sanitizeInput.toPositiveInt(paperId, 0);
|
||||
const sanitizedPage = sanitizeInput.toPositiveInt(page, 1);
|
||||
const sanitizedPageSize = sanitizeInput.toPositiveInt(pageSize, 10);
|
||||
const limitedPageSize = Math.min(sanitizedPageSize, 100);
|
||||
const offset = (sanitizedPage - 1) * limitedPageSize;
|
||||
|
||||
const [papers] = await db.promise().query('SELECT * FROM paper WHERE id = ?', [sanitizedPaperId]);
|
||||
if (papers.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '试卷不存在' });
|
||||
}
|
||||
|
||||
const topicIds = JSON.parse(papers[0].TopicsId || '[]');
|
||||
const total = topicIds.length;
|
||||
|
||||
const pagedTopicIds = topicIds.slice(offset, offset + limitedPageSize);
|
||||
|
||||
if (pagedTopicIds.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: [],
|
||||
pagination: {
|
||||
current: sanitizedPage,
|
||||
pageSize: limitedPageSize,
|
||||
total
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 查询题目详情
|
||||
const [topics] = await db.promise().query(
|
||||
'SELECT * FROM topics WHERE id IN (?)',
|
||||
[pagedTopicIds]
|
||||
);
|
||||
|
||||
const orderedTopics = pagedTopicIds.map(id =>
|
||||
topics.find(topic => topic.id === id)
|
||||
).filter(Boolean);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: orderedTopics,
|
||||
pagination: {
|
||||
current: sanitizedPage,
|
||||
pageSize: limitedPageSize,
|
||||
total
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取试卷题目失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取试卷题目失败' });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取试卷详情
|
||||
exports.getPaperDetail = async (req, res) => {
|
||||
try {
|
||||
console.log('Received body:', req.body);
|
||||
|
||||
if (!paperId) {
|
||||
return res.status(400).json({ success: false, message: '无效的试卷ID' });
|
||||
}
|
||||
|
||||
const sanitizedPaperId = sanitizeInput.toPositiveInt(paperId, 0);
|
||||
const sanitizedPage = sanitizeInput.toPositiveInt(page, 1);
|
||||
const sanitizedPageSize = sanitizeInput.toPositiveInt(pageSize, 10);
|
||||
const limitedPageSize = Math.min(sanitizedPageSize, 100);
|
||||
const offset = (sanitizedPage - 1) * limitedPageSize;
|
||||
|
||||
const [papers] = await db.promise().query('SELECT * FROM paper WHERE id = ?', [sanitizedPaperId]);
|
||||
if (papers.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '试卷不存在' });
|
||||
}
|
||||
|
||||
const topicIds = JSON.parse(papers[0].TopicsId || '[]');
|
||||
const total = topicIds.length;
|
||||
|
||||
const pagedTopicIds = topicIds.slice(offset, offset + limitedPageSize);
|
||||
|
||||
let topics = [];
|
||||
if (pagedTopicIds.length > 0) {
|
||||
const [topicsResult] = await db.promise().query(
|
||||
'SELECT * FROM topics WHERE id IN (?)',
|
||||
[pagedTopicIds]
|
||||
);
|
||||
topics = pagedTopicIds.map(id =>
|
||||
topicsResult.find(topic => topic.id === id)
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...papers[0],
|
||||
topics: {
|
||||
list: topics,
|
||||
pagination: {
|
||||
current: sanitizedPage,
|
||||
pageSize: limitedPageSize,
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取试卷详情失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取试卷详情失败' });
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索题目
|
||||
exports.searchTopics = async (req, res) => {
|
||||
try {
|
||||
let { keyword, page = 1, pageSize = 10 } = req.query;
|
||||
|
||||
keyword = sanitizeInput.escapeSearch(keyword);
|
||||
if (!keyword) {
|
||||
return res.status(400).json({ success: false, message: '请提供有效的搜索关键词' });
|
||||
}
|
||||
|
||||
page = sanitizeInput.toPositiveInt(page, 1);
|
||||
pageSize = Math.min(sanitizeInput.toPositiveInt(pageSize, 10), 100);
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const [countResult] = await db.promise().query(
|
||||
'SELECT COUNT(*) as total FROM topics WHERE title LIKE ?',
|
||||
[`%${keyword}%`]
|
||||
);
|
||||
const total = countResult[0].total;
|
||||
|
||||
const [topics] = await db.promise().query(
|
||||
`SELECT t.*,
|
||||
(SELECT title FROM paper WHERE JSON_CONTAINS(TopicsId, CAST(t.id AS JSON))) as paper_title
|
||||
FROM topics t
|
||||
WHERE t.title LIKE ?
|
||||
LIMIT ?, ?`,
|
||||
[`%${keyword}%`, offset, pageSize]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: topics,
|
||||
pagination: {
|
||||
current: page,
|
||||
pageSize,
|
||||
total
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('搜索题目失败:', error);
|
||||
res.status(500).json({ success: false, message: '搜索题目失败' });
|
||||
}
|
||||
}
|
360
router/stb.js
Normal file
360
router/stb.js
Normal file
@ -0,0 +1,360 @@
|
||||
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
|
||||
};
|
18239
华为ICT题库提取版.txt
Normal file
18239
华为ICT题库提取版.txt
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user