·更新于 2026年10月25日· 19 次浏览 Node.jsMongoDBRedis支付系统微信小程序工程实践BUG解决
支付成功但会员没到账:一次 Node.js 补偿链路改造记录
用户付款成功却没开通会员,问题往往不是支付回调没来,而是权益写入失败后没有兜底。这篇记录我如何用状态机、异步补发、重试和对账把掉单补回来。

作者
ZHOUYI
晚上收到一条用户投诉:小程序里买了年度会员,微信支付扣款成功,页面却显示未开通。查日志发现支付回调其实到了,但 Node.js 在写 MongoDB 会员到期时间时超时,接口没把权益落库。过去我把「回调处理成功」和「会员开通成功」当成同一件事,一旦中间抖一下,就会掉单。
把支付成功和权益生效拆开
回调到达后,先只做两件事:校验签名、把订单更新为 paid,并把支付凭据存下来。至于加会员天数、发优惠券、刷新缓存,都交给异步补偿任务。这样即使开通失败,也能先给支付平台返回成功,避免回调被反复重推;真正的权益结果由自己的状态机负责。
异步补发:状态机、重试和幂等
- 订单状态从 paid 进入 grant_pending,成功后变 granted,失败进 grant_failed 或等待重试。
- 用 MongoDB 的原子条件更新防止重复开通,只允许从指定前置状态迁移。
- 用 Redis 记录订单级处理锁或去重键,避免定时任务和回调同时补发。
- 重试超过阈值就告警,后台提供人工补发入口,别让用户一直等。
js
async function grant(orderId) {
const order = await Orders.findOneAndUpdate(
{ _id: orderId, status: 'paid' },
{ $set: { status: 'grant_pending' } }
);
if (!order) return;
try {
await addVip(order.userId, order.days);
await Orders.updateOne({ _id: orderId }, { $set: { status: 'granted' } });
} catch (err) {
await Compensations.updateOne({ orderId }, { $inc: { retry: 1 } }, { upsert: true });
}
}对账兜底:别只相信一条链路
我还加了一个低频对账任务:拉取支付平台账单或主动查询订单,对比本地已支付但未 granted 的记录。发现漏发就重新投递补偿任务,并把差异写进审计日志。个人站流量不大,但支付场景不能靠运气,宁可多查几次,也不要让用户拿着扣款截图来找你。
“支付回调只是开始,权益到账必须有独立状态机和兜底任务。”
这次改造后,即使 MongoDB 短暂超时或开通接口失败,系统也能自动补发,投诉明显少了。具体支付平台的回调签名、账单接口和重试规则,请以官方文档为准。对独立开发者来说,最值钱的不是华丽架构,而是用户付钱后能稳定拿到权益。