后端
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