如何在MongoDB多文档间使用$inc实现Discord moderation系统案件号自增
实现方案
1. 新增全局计数器集合
要实现跨文档的全局连续自增,MongoDB官方推荐的最佳实践是单独维护一个存放自增序列的集合,通过原子操作避免并发场景下的序号冲突问题。
首先创建计数器的Schema和模型:
// countersSchema.js const counterSchema = database.Schema({ _id: { type: String, required: true }, sequenceValue: { type: Number, default: 0 } }) module.exports = database.model('counters', counterSchema)
首次使用前需要初始化Case序列的起始值,执行一次即可:
const Counter = require('./countersSchema') // 如果已有历史案件,sequenceValue填当前已有案件的最大Case号,没有则填0 await Counter.findOneAndUpdate( { _id: 'caseId' }, { $setOnInsert: { sequenceValue: 0 } }, { upsert: true } )
2. 封装全局Case号获取工具函数
通过findOneAndUpdate的原子性保证多请求并发时不会出现重复、断号问题:
const Counter = require('./countersSchema') async function getNextCaseId() { const counter = await Counter.findOneAndUpdate( { _id: 'caseId' }, { $inc: { sequenceValue: 1 } }, { new: true, upsert: true } ) return counter.sequenceValue }
3. 调整原有Schema和业务代码
首先修正你原有Schema的逻辑问题:原Schema没有定义UserId字段,且MessageID设为必填但第二个业务分支没有传值,会导致插入失败:
const warnsSchema = database.Schema({ UserId: { type: String, required: true }, Username: { type: String, required: true }, MessageID: { type: String, required: false // 第二个分支不需要的话设为非必填,也可以补充传值逻辑 }, Case: { type: Number, required: true }, WarnedBy: { type: String, required: true }, warning: { type: [Object], required: true, default: [] } }) module.exports = database.model('punishments', warnsSchema)
然后调整业务逻辑,插入新处罚记录前先调用工具函数获取全局自增的Case号:
async run(client, message, args, Discord) { const targetMention = message.mentions.users.first() const userid = args[0] const targetId = client.users.cache.find(user => user.id === userid) if(targetMention){ args.shift() const userId = targetMention.id const username = targetMention.tag const WarnedBy = message.author.tag const reason = args.join(' ') if(!reason) { message.delete() message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => { message.delete({ timeout: 6000}) }); return; } const warningApplied = new Discord.MessageEmbed() .setColor('#ffd200') .setDescription(`A warning has been applied to ${targetMention.tag} :shield:`) let reply = await message.reply(warningApplied) let replyID = reply.id const warning = { UserId: userId, WarnedBy: WarnedBy, Timestamp: new Date().getTime(), Reason: reason, } // 获取全局唯一自增Case号 const newCaseId = await getNextCaseId() await database().then(async database => { try{ await warnsSchema.findOneAndUpdate({ Username: username, MessageID: replyID }, { Case: newCaseId, WarnedBy: WarnedBy, $push: { warning: warning } }, { upsert: true }) } finally { database.connection.close() } }) } if(targetId){ args.shift() const userId = targetId.id const username = targetId.tag const WarnedBy = message.author.tag const reason = args.join(' ') if(!reason) { message.delete() message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => { message.delete({ timeout: 6000}) }); return; } const warning = { UserId: userId, WarnedBy: WarnedBy, timestamp: new Date().getTime(), reason: reason } // 获取全局唯一自增Case号 const newCaseId = await getNextCaseId() await database().then(async database => { try{ await warnsSchema.findOneAndUpdate({ UserId: userId, Username: username }, { Case: newCaseId, WarnedBy: WarnedBy, $push: { warning: warning } }, { upsert: true }) } finally { database.connection.close() } const warningApplied = new Discord.MessageEmbed() .setColor('#ffd200') .setDescription(`A warning has been applied to ${targetId.tag} :shield:`) message.reply(warningApplied) message.delete(); }) } }
注意事项
- 计数器的递增操作为原子操作,哪怕同时触发多个处罚请求,也不会出现Case号重复、断号的问题
- 原代码第二个分支的查询条件存在逻辑错误,原本误用了操作人的ID作为查询条件,上面的代码已经修正为被处罚用户的ID
- 如果已有存量处罚数据,初始化计数器时要把
sequenceValue设为存量数据中最大的Case号,避免和历史案件号冲突
内容的提问来源于stack exchange,提问作者Rodagave
相关产品推荐
相关产品推荐

