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

奖励回调重复发送导致Node.js服务端积分重复入账如何解决?

问题根因

你现有的实现存在两个核心错误直接导致重复入账:

  • 代码逻辑错误:你在异步数据库操作的外层直接写了return res.send(event_id + ":OK").status(200);,这会导致请求刚进来、数据库操作还没执行完甚至还没开始校验事件ID的时候,就直接返回了200响应,同时后续异步执行的数据库逻辑没有做重复校验,重发请求进来后会重复执行所有写入操作。
  • 幂等性实现逻辑顺序错误:正确的幂等校验需要先判断事件ID是否已处理,只有未处理的请求才执行后续积分操作,你没有在请求入口第一步就做幂等校验,也没有原子化的幂等标记写入逻辑。

服务端可落地的解决方案

完全可以在服务端直接解决,按以下步骤修改即可:

第一步:调整请求处理逻辑顺序,入口先做幂等校验

请求进来第一步就以event_id为唯一键查询是否存在已处理的记录,注意这个校验和幂等标记的写入要做原子操作,避免并发请求穿透校验。
如果用的是MongoDB,直接用updateOne的upsert特性实现原子校验+写入:

// 请求进来第一步先做幂等校验,原子操作
const idempotentRes = await db.collection('pointIdempotent').updateOne(
  { event_id: event_id }, // 提前给event_id字段建唯一索引
  { 
    $setOnInsert: {
      event_id: event_id,
      uid: uid,
      create_time: Date.now(),
      status: 'processing' // 标记处理中
    }
  },
  { upsert: true }
)

// 如果matchedCount不为0,说明这个event_id已经被处理过/正在处理,直接返回200即可
if (idempotentRes.matchedCount > 0) {
  return res.status(200).send(event_id + ":OK");
}

第二步:把所有数据库操作放到事务里执行,避免部分成功部分失败导致数据不一致

所有积分写入、历史记录写入的逻辑都放到同一个事务中,要么全部成功,要么全部失败。

第三步:删除外层的提前返回逻辑,只有所有业务逻辑执行成功后再返回200

修正后的完整逻辑参考:

try {
  // 1. 入口原子幂等校验,已处理直接返回
  const idempotentRes = await db.collection('pointIdempotent').updateOne(
    { event_id: event_id },
    { $setOnInsert: { event_id, uid, create_time: Date.now(), status: 'processing' } },
    { upsert: true }
  )
  if (idempotentRes.matchedCount > 0) {
    return res.status(200).send(event_id + ":OK")
  }

  // 2. 执行所有业务操作,建议放到事务中
  const session = await db.startSession()
  await session.withTransaction(async () => {
    await db.collection('pointHistory').doc(uid).collection('pointHistory').add(newPointHistory, { session })
    await db.collection('users').doc(uid).update({ point: addTotalPoint }, { session })
    await db.collection('offerwallHistory').doc(uid).collection('offerwallHistory').add(offerwallHistory_data, { session })
    await db.collection('adminPointHistory').add(adminPointHistory_data, { session })
    await setDailyPointStatus(reward, 12, session)
    await setOtherPersonalPointStatus(reward, uid, "dailyPersonalPointStatus", session)
    await setOtherPersonalPointStatus(reward, uid, "weeklyPersonalPointStatus", session)
    await setOtherPersonalPointStatus(reward, uid, "monthlyPersonalPointStatus", session)
    // 更新幂等表状态为成功
    await db.collection('pointIdempotent').updateOne({ event_id }, { $set: { status: 'success' } }, { session })
  })
  await session.endSession()

  // 3. 所有逻辑执行成功后再返回200
  return res.status(200).send(event_id + ":OK")
} catch (err) {
  console.log(err)
  // 处理失败的话更新幂等表状态为失败,不要返回200,让奖励服务器重试
  await db.collection('pointIdempotent').updateOne({ event_id }, { $set: { status: 'fail', error: err.message } })
  return res.status(500).send(event_id + ":FAIL")
}

额外优化点

  • 给pointIdempotent表的event_id字段建唯一索引,彻底避免重复写入
  • 可以定期清理超过奖励服务器重发周期的幂等记录,减少表体积

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 03:18:02