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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:06:24