feat: 实现所有用户共享刷题历史与权重数据后端服务及多端同步
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const DATA_DIR = path.join(__dirname, '../data');
|
||||
const DATA_FILE = path.join(DATA_DIR, 'quiz_stats.json');
|
||||
const QUESTIONS_FILE = path.join(__dirname, '../questions.json');
|
||||
|
||||
// 保证数据目录存在
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 内存中的状态缓存
|
||||
let state = {
|
||||
weights: {},
|
||||
history: [],
|
||||
totalAnswered: 0
|
||||
};
|
||||
|
||||
// 初始化状态
|
||||
function initState() {
|
||||
if (fs.existsSync(DATA_FILE)) {
|
||||
try {
|
||||
const content = fs.readFileSync(DATA_FILE, 'utf-8');
|
||||
state = JSON.parse(content);
|
||||
console.log(`[Init] 已从文件载入全局数据,累计答题数: ${state.totalAnswered}`);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('[Init] 读取已有数据文件失败,将重新初始化:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认权重
|
||||
let defaultWeights = {};
|
||||
if (fs.existsSync(QUESTIONS_FILE)) {
|
||||
try {
|
||||
const qData = JSON.parse(fs.readFileSync(QUESTIONS_FILE, 'utf-8'));
|
||||
qData.forEach(q => {
|
||||
defaultWeights[q.id] = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Init] 读取 questions.json 失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
state = {
|
||||
weights: defaultWeights,
|
||||
history: [],
|
||||
totalAnswered: 0
|
||||
};
|
||||
saveState();
|
||||
}
|
||||
|
||||
// 异步持久化保存
|
||||
function saveState() {
|
||||
const tmpFile = `${DATA_FILE}.tmp`;
|
||||
fs.writeFile(tmpFile, JSON.stringify(state, null, 2), 'utf-8', (err) => {
|
||||
if (err) {
|
||||
console.error('[Save] 保存临时数据文件失败:', err);
|
||||
return;
|
||||
}
|
||||
fs.rename(tmpFile, DATA_FILE, (renameErr) => {
|
||||
if (renameErr) {
|
||||
console.error('[Save] 原子重命名数据文件失败:', renameErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initState();
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// 设置 CORS 标头
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const pathname = url.pathname.replace(/^\/api/, ''); // 统一兼容 /api 前缀与无前缀
|
||||
|
||||
if (req.method === 'GET' && (pathname === '/stats' || pathname === '/stats/')) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(state));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && (pathname === '/answer' || pathname === '/answer/')) {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const payload = JSON.parse(body || '{}');
|
||||
const questionId = Number(payload.questionId);
|
||||
const isCorrect = Boolean(payload.isCorrect);
|
||||
|
||||
if (!questionId) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Missing questionId' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新历史记录(保留最近 100 条)
|
||||
state.history.push(isCorrect);
|
||||
if (state.history.length > 100) {
|
||||
state.history = state.history.slice(-100);
|
||||
}
|
||||
|
||||
// 更新全局总数
|
||||
state.totalAnswered = (state.totalAnswered || 0) + 1;
|
||||
|
||||
// 更新题目权重
|
||||
const currentWeight = state.weights[questionId] || 1;
|
||||
if (isCorrect) {
|
||||
state.weights[questionId] = Math.max(1, currentWeight - 1);
|
||||
} else {
|
||||
state.weights[questionId] = currentWeight + 3; // 答错大幅增加权重
|
||||
}
|
||||
|
||||
saveState();
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
weights: state.weights,
|
||||
history: state.history,
|
||||
totalAnswered: state.totalAnswered
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('[Error] 处理 /api/answer 异常:', err);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && (pathname === '/reset' || pathname === '/reset/')) {
|
||||
initState();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ success: true, message: 'Stats reset' }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Not Found' }));
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[Server] 全局刷题服务已启动,监听端口: ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user