Discord.js Sequelize冷却库触发id唯一约束错误
问题根因
- 核心错误是将Discord用户ID直接赋值给了Sequelize模型默认的
id主键字段:Sequelize默认会把名为id的字段设为整表主键,主键天生自带全局唯一约束,和你手动配置unique: false没有关系。就算你在模型定义里修改了unique规则,只要没有强制重建表,SQLite中已有的主键约束不会被删除,这也是你改配置、甚至单独建新库仍然报错的根本原因——主键要求整表id值唯一,你要给同一个用户存hunt、daily两条不同指令的冷却记录,必然触发主键冲突。 /daily指令冷却查询逻辑有缺陷:查询冷却时没有加command: 'daily'的筛选条件,只要用户存在任意一条其他指令的冷却记录,都会被误判为daily的冷却,不符合业务设计。- 多数数据库异步操作没加
await:destroy、create都是异步执行的,你没有等待操作完成就进入后续逻辑,很容易出现旧记录没删完就插入新记录的竞态问题,也会偶发约束报错。
修复方案
第一步:修正Cooldowns模型定义
不要把用户ID作为表主键,保留Sequelize默认的自增id主键即可,把用户ID、指令名设为联合唯一约束,符合「一个用户对一个指令仅能有一条冷却记录」的业务逻辑:
// 修正后的Cooldowns模型参考 const Cooldown = sequelize.define('cooldown', { // 保留默认自增id主键,业务代码不要手动给这个字段传值 userId: { type: DataTypes.STRING, allowNull: false }, command: { type: DataTypes.STRING, allowNull: false }, expiry: { type: DataTypes.BIGINT, allowNull: false } }, { indexes: [ { unique: true, fields: ['userId', 'command'] // 联合唯一键:同用户+同指令仅允许一条记录 } ] })
注意:修改模型后需要删除旧的sqlite数据库文件,或者执行
sequelize.sync({ force: true })强制重建表结构,否则旧表的约束不会更新。
第二步:修正两个指令的业务代码
所有数据库异步操作必须加await,查询/删除/更新时统一使用userId和command两个条件匹配,不要仅用用户ID查询。
修正后的/daily指令代码
const { SlashCommandBuilder } = require('@discordjs/builders') const ms = require('ms') const { defaultColor } = require('../../command-imports') module.exports = { data: new SlashCommandBuilder() .setName('daily') .setDescription('Claim your daily reward.'), async execute (interaction, Cooldowns) { // 必须同时匹配用户ID和指令名 let getCooldown = await Cooldowns.findOne({where: {userId: interaction.user.id, command: 'daily'}}) let cooldownTime = getCooldown?.expiry if(getCooldown && cooldownTime > new Date().getTime()) { return interaction.reply({content: `You are still under cooldown! Please wait **${ms(cooldownTime - new Date().getTime(), {long: true})}**`}) } else if (getCooldown) { // 等待删除操作执行完成 await Cooldowns.destroy({where: {userId: interaction.user.id, command: 'daily'}}) } const claimedDaily = { color: defaultColor, description: "You have recieved N/A from your daily reward" } await interaction.reply({embeds: [claimedDaily]}) // 创建记录时传userId,不要给id字段赋值 await Cooldowns.create({ userId: interaction.user.id, expiry: new Date().getTime() + (60000 * 5), command: 'daily' }) } }
修正后的/hunt指令代码
const { SlashCommandBuilder } = require('@discordjs/builders') const { errorColor, defaultColor } = require('../../command-imports') const ms = require('ms') module.exports = { data: new SlashCommandBuilder() .setName('hunt') .setDescription('Hunt for a chance at finding a bear.'), async execute(interaction, Cooldowns, Economy) { // 同时匹配用户ID和指令名 let getCooldown = await Cooldowns.findOne({where: {userId: interaction.user.id, command: 'hunt'}}) let getUser = await Economy.findOne({where: {id: interaction.user.id}}) if(!getUser) { getUser = await Economy.create({id: interaction.user.id, coins: 0}) } let cooldownTime = getCooldown?.expiry if(getCooldown && cooldownTime > new Date().getTime()) { return interaction.reply({content: `You are still under cooldown! Please wait **${ms(cooldownTime - new Date().getTime(), {long: true})}**`}) } else if (getCooldown) { // 等待删除完成 await Cooldowns.destroy({where: {userId: interaction.user.id, command: 'hunt'}}) } let whichExecute = Math.floor(Math.random() * 8) + 1 if(whichExecute <= 7) { let coinsFound = Math.floor(Math.random() * 10) + 1 const nothingFound = { color: errorColor, description: `No bear was found however you found ${coinsFound} :coin: \n You have ${getUser.coins} :coin:` } await interaction.reply({embeds: [nothingFound]}) await Economy.update({coins: getUser.coins + coinsFound}, {where: {id: interaction.user.id}}) await Cooldowns.create({ userId: interaction.user.id, expiry: new Date().getTime() + (60000 * 5), command: 'hunt' }) } else if(whichExecute === 8) { const bearFound = { color: defaultColor, description: "You found placeholder_beartype :bear:;" } await interaction.reply({embeds: [bearFound]}) await Cooldowns.create({ userId: interaction.user.id, expiry: new Date().getTime() + (60000 * 5), command: 'hunt' }) } } }
可选优化
不需要每次冷却过期都先删再建,直接用upsert更新对应记录的过期时间即可,减少数据库操作,也从根源避免插入冲突:
// 替换原来的destroy+create逻辑 await Cooldowns.upsert({ userId: interaction.user.id, command: 'hunt', expiry: new Date().getTime() + (60000 * 5) })
内容的提问来源于stack exchange,提问作者Sae
相关产品推荐
相关产品推荐

