MongoDB存储值至变量复用及数据变更对比展示技术问询
实现每日数据同步及变更展示方案
一、先修复现有代码的基础问题
你当前的代码每次cron任务运行时都重复定义Schema和Model,会导致Mongoose重复注册模型报错。先把Schema和Model移到全局位置统一定义:
const mongoose = require('mongoose'); // 玩家信息模型 const playerSchema = new mongoose.Schema({ id: { type: Number, required: true, unique: true }, // 标记ID唯一,避免重复插入 name: { type: String, required: true }, status: { type: String }, updatedAt: { type: Date, default: Date.now } }); const Player = mongoose.model('players', playerSchema); // 高分数据模型 const highscoreSchema = new mongoose.Schema({ position: { type: Number }, id: { type: Number, required: true, unique: true }, score: { type: Number }, updatedAt: { type: Date, default: Date.now } }); const Highscore = mongoose.model('highscores', highscoreSchema); // 新增变更日志模型,专门存储数据变更记录 const changeLogSchema = new mongoose.Schema({ playerId: { type: Number, required: true }, playerName: { type: String, required: true }, changeDate: { type: Date, default: Date.now }, // 存储新旧数据快照 oldData: { name: String, position: Number, score: Number }, newData: { name: String, position: Number, score: Number }, // 记录具体变更字段详情 changes: [{ field: String, oldValue: String | Number, newValue: String | Number, delta: Number // 仅针对分数的变化量 }] }); const ChangeLog = mongoose.model('changeLogs', changeLogSchema);
二、修改Cron任务:同步数据+记录变更
把原来的“直接插入”改成“先查旧数据→对比变更→更新数据→记录日志”的逻辑:
玩家数据同步任务
cron.schedule('56 23 * * *', async () => { try { const res = await fetch("http://localhost:3008/api/players"); const { players } = await res.json(); for (const newPlayer of players) { const oldPlayer = await Player.findOne({ id: newPlayer.id }); const changes = []; if (oldPlayer) { // 对比名字变化 if (oldPlayer.name !== newPlayer.name) { changes.push({ field: 'name', oldValue: oldPlayer.name, newValue: newPlayer.name }); } // 对比状态变化(如果需要展示的话) if (oldPlayer.status !== newPlayer.status) { changes.push({ field: 'status', oldValue: oldPlayer.status, newValue: newPlayer.status }); } // 更新数据库 await Player.findOneAndUpdate( { id: newPlayer.id }, { name: newPlayer.name, status: newPlayer.status, updatedAt: Date.now() }, { new: true } ); } else { // 新玩家直接插入 await new Player(newPlayer).save(); } // 有变更则记录日志 if (changes.length > 0 && oldPlayer) { await new ChangeLog({ playerId: newPlayer.id, playerName: newPlayer.name, oldData: { name: oldPlayer.name }, newData: { name: newPlayer.name }, changes }).save(); } } console.log("玩家数据同步完成"); } catch (err) { console.error("玩家数据同步失败:", err); } });
高分数据同步任务
cron.schedule('55 23 * * *', async () => { try { const res = await fetch("http://localhost:3008/api/highscore/players"); const { players } = await res.json(); for (const newScore of players) { const oldScore = await Highscore.findOne({ id: newScore.id }); const player = await Player.findOne({ id: newScore.id }); const playerName = player ? player.name : '未知玩家'; const changes = []; if (oldScore) { // 对比排名变化 if (oldScore.position !== newScore.position) { changes.push({ field: 'position', oldValue: oldScore.position, newValue: newScore.position }); } // 对比分数变化,计算增减量 if (oldScore.score !== newScore.score) { const delta = newScore.score - oldScore.score; changes.push({ field: 'score', oldValue: oldScore.score, newValue: newScore.score, delta }); } // 更新数据库 await Highscore.findOneAndUpdate( { id: newScore.id }, { position: newScore.position, score: newScore.score, updatedAt: Date.now() }, { new: true } ); } else { // 新玩家高分数据直接插入 await new Highscore(newScore).save(); } // 有变更则记录日志 if (changes.length > 0 && oldScore) { await new ChangeLog({ playerId: newScore.id, playerName, oldData: { position: oldScore.position, score: oldScore.score }, newData: { position: newScore.position, score: newScore.score }, changes }).save(); } } console.log("高分数据同步完成"); } catch (err) { console.error("高分数据同步失败:", err); } });
三、添加变更记录查询接口
新增接口供前端拉取变更日志,也可以整合到你现有的/export接口中:
// 获取最新变更记录 router.get('/change-logs', async (req, res) => { try { // 按时间倒序取最近50条,可根据需求调整 const logs = await ChangeLog.find() .sort({ changeDate: -1 }) .limit(50); res.status(200).json(logs); } catch (e) { res.status(500).send('获取变更记录失败'); } }); // 按日期范围查询变更记录(可选) router.get('/change-logs/date', async (req, res) => { const { start, end } = req.query; try { const logs = await ChangeLog.find({ changeDate: { $gte: new Date(start), $lte: new Date(end) } }).sort({ changeDate: -1 }); res.status(200).json(logs); } catch (e) { res.status(500).send('获取变更记录失败'); } });
四、前端展示变更记录
前端调用接口后,渲染成你需要的格式示例:
<!DOCTYPE html> <html> <head> <title>玩家数据变更记录</title> <style> .log-card { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; } .change-row { display: flex; justify-content: space-between; margin: 8px 0; } .no-change { color: #666; } .changed { color: #dc3545; } .delta { color: #28a745; margin-left: 10px; } </style> </head> <body> <h1>玩家数据变更日志</h1> <div id="logs-container"></div> <script> async function loadLogs() { const res = await fetch('/api/change-logs'); const logs = await res.json(); const container = document.getElementById('logs-container'); logs.forEach(log => { const card = document.createElement('div'); card.className = 'log-card'; // 头部信息:日期+玩家 const date = new Date(log.changeDate).toLocaleDateString(); card.innerHTML = `<h3>${date} | 玩家ID: ${log.playerId} | ${log.playerName}</h3>`; // 渲染需要展示的字段:name、position、score ['name', 'position', 'score'].forEach(field => { const oldVal = log.oldData[field]; const newVal = log.newData[field]; const change = log.changes.find(c => c.field === field); const row = document.createElement('div'); row.className = 'change-row'; let leftText = `${field.toUpperCase()}: ${oldVal ?? '无'}`; let rightText = 'NO CHANGES'; let className = 'no-change'; if (change) { rightText = `CHANGES TO: ${newVal}`; className = 'changed'; if (field === 'score') { const type = change.delta > 0 ? 'GAINED' : 'LOST'; rightText += ` (${type} ${Math.abs(change.delta)} POINTS)`; } } row.innerHTML = `<span>${leftText}</span><span class="${className}">${rightText}</span>`; card.appendChild(row); }); container.appendChild(card); }); } loadLogs(); </script> </body> </html>
核心逻辑说明
- 用
findOneAndUpdate替代直接save,避免重复插入,同时更新现有数据 - 通过
changeLogs集合存储每次变更的快照,方便前端查询展示 - 对比字段时只关注你需要展示的
name、position、score,并计算分数变化量 - 所有异步操作添加
try/catch,防止cron任务因报错中断
内容的提问来源于stack exchange,提问作者yandry santana
相关产品推荐
相关产品推荐

