You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

核心逻辑说明

  1. 用findOneAndUpdate替代直接save,避免重复插入,同时更新现有数据
  2. 通过changeLogs集合存储每次变更的快照,方便前端查询展示
  3. 对比字段时只关注你需要展示的name、position、score,并计算分数变化量
  4. 所有异步操作添加try/catch,防止cron任务因报错中断

内容的提问来源于stack exchange,提问作者yandry santana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 17:46:25