This commit is contained in:
wzclm 2024-11-07 14:21:20 +08:00
commit dcbc251483
9 changed files with 20325 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

26
app.js Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff