
- 学习结果
- 写出一个可靠的支付通知处理流程,列出必须处理的事件和订阅状态对应的权限规则,并完成重复、乱序和退款三种情况的测试。
- 时长
- 2-3 小时
- 难度
- 经验分享
- 核验
- 已核验 2026-10-24 · 下次复核 2026-11-23
正文
用户付完钱,浏览器跳回你的“支付成功”页面,于是开通会员。这个做法看起来没问题,但并不可靠:用户可能在跳转前关掉了浏览器;有些支付方式要过一段时间才确认到账;“成功页”的网址还可能被人直接访问。
可靠的做法是:以支付平台发给你服务器的通知为准。这种通知叫 webhook:交易发生变化时,平台主动向你的服务器发送一条消息。浏览器的跳转只用来给用户展示结果,开通权限由服务器根据可信的通知来决定。
这篇以 Stripe 为例讲清楚怎么处理这些通知。其他平台的事件名和签名方式不同,但要解决的问题是一样的。
一条可靠的处理流程

- 验证签名:确认这条通知真的来自支付平台。
- 记录事件:把事件 ID 写进数据库,用来去重。
- 尽快返回成功:告诉平台“收到了”,不要在这一步做耗时的事。
- 异步处理:把事件放进队列,慢慢处理。
- 读取最新状态:向平台查询这笔订单或订阅当前的真实状态。
- 更新订单和权益:根据最新状态更新你自己的记录。
下面一步一步说明为什么要这样做。
验证签名:用原始请求体
没有验证的话,任何人都可以伪造一条“支付成功”的通知,骗你的系统开通权限。Stripe 会在每条通知的请求头里带上签名,你用平台给你的签名密钥来验证。
最容易踩的坑是:验证签名必须使用原始的请求体。很多框架会自动把请求体解析成 JSON,再验证就会失败。Stripe 的官方库默认允许签名时间和当前时间相差 5 分钟,用来防止旧通知被重放。测试环境和正式环境的密钥不同,要分别配置。验证失败的通知直接拒绝,日志里不要打印签名密钥和完整的客户信息。
去重:同一个事件可能来好几次
平台可能会把同一条事件发送不止一次。如果每收到一次就开通一次会员、发一封邮件,用户就会收到重复的邮件,你的数据也会乱。
做法是:把处理过的事件 ID 存下来,并设为唯一;收到已经处理过的事件 ID,就直接跳过。Stripe 的文档还提到,有时同一个对象的变化会生成两个不同的事件,这时可以用“对象 ID 加事件类型”来判断是否重复。
更新订单、更新权益、标记事件已处理,这几步最好放在同一个数据库事务里完成。否则可能出现这种情况:事件被标记为已处理,但更新失败了,之后平台重试时又被跳过,用户付了钱却一直没开通。
不要依赖事件到达的顺序
Stripe 明确说明,不保证按事件发生的顺序投递。比如创建一个订阅,可能依次产生订阅创建、账单创建、账单支付成功等多个事件,但到达你服务器的顺序可能是乱的。官方还提醒,不要用事件里的创建时间来判断先后顺序。
所以不能简单地“以最后收到的事件为准”。否则一条迟到的“支付成功”通知,可能把已经退款的订单又改回已支付。更稳妥的做法是:收到事件后,向平台查询这个订单或订阅当前的状态,以平台返回的最新事实为准。
尽快返回,失败会重试
收到通知后,要在做任何复杂操作之前尽快返回成功(2xx 状态码)。如果你在返回之前先去更新会计系统、发邮件,很容易超时,平台就会认为投递失败。
投递失败后,Stripe 在正式环境中会用逐渐拉长的间隔重试,最长持续约 3 天;测试环境只在几小时内重试 3 次。所以短暂的故障通常不会丢通知,但你要在后台能看到哪些事件一直没有处理成功。
订单、订阅和权益,分开管理

这是最容易混在一起的地方。三种记录各有自己的生命周期:
- 订单:一次性购买的记录,状态是已支付或已退款。
- 订阅:周期性付费的关系,有试用、正常、逾期、取消等状态。
- 权益:用户现在能用什么、能用到什么时候。
Stripe 推荐的做法是:在你的系统里给每个用户保存一个权益到期时间,用户登录时检查这个时间是否还没到。订阅续费成功时,收到 invoice.paid 事件后,先查询订阅,确认它的状态是 active,再延长到期时间。
订阅状态和权限的对应关系,可以参考下表:
| 订阅状态 | 含义 | 你应该做什么 |
|---|---|---|
trialing |
试用期 | 可以开通 |
active |
状态正常 | 开通,按周期延长到期时间 |
incomplete |
首次付款未完成 | 暂不开通,提示用户完成支付 |
past_due |
续费扣款失败或未扣款 | 通知用户更新支付方式 |
canceled |
已取消 | 收回权限 |
unpaid |
多次重试后仍未付款(取决于你的账单设置) | 收回权限 |
除了订阅事件,还要处理退款(charge.refunded)和争议(charge.dispute.created)。这些规则也要和你的服务条款一致:用户取消自动续费,通常可以用到当期结束;部分退款和争议,不一定等于立刻封禁账号。

图:Stripe“将 webhooks 用于订阅”官方文档页面截图,截取于 2026-10-24,内容以官方当前页面为准。
对账和人工补单
再可靠的流程也会有漏网之鱼。每天对比一次支付平台的交易记录和你的订单记录,发现不一致的地方就处理。
用户说“付了钱没开通”时,先在平台后台查到这笔交易,确认已支付,再手工补开权益,并记录操作人、时间和依据。不要因为某个事件处理失败,就给所有相关用户无条件开通。
学习步骤
- 在测试环境配置 webhook 地址,用平台的命令行工具把事件转发到本地。
- 实现签名验证,确认使用的是原始请求体。
- 建一张事件表,事件 ID 设为唯一,实现去重。
- 收到事件后先返回成功,把处理逻辑放到异步任务里。
- 处理时查询订单或订阅的最新状态,再更新订单和权益到期时间。
- 测试重复投递、乱序到达和退款三种情况,并写一份对账和人工补单流程。
场景拆解
一个“付了钱没开通”的工单
小周的工时工具上线订阅后,收到一位用户的邮件:昨天付了年费,到现在还是免费版。
- 查平台:在 Stripe 后台找到这笔交易,状态是已支付。
- 查事件:在 webhook 的投递记录里,看到
invoice.paid事件投递了 4 次,都返回了 500 错误。 - 查日志:服务器日志显示签名验证失败。原因是前一天升级框架后,请求体在验证前被自动解析成了 JSON。
- 修复:改为读取原始请求体再验证。修复后,平台的下一次重试成功,系统查询到订阅为
active,自动为这位用户延长了权益。 - 补救:检查同一时间段内还有没有其他失败的事件,确认都已自动重试成功;给用户回邮件说明原因,并赠送一个月。
这次没有丢单,靠的是平台约 3 天的重试,以及他能看到事件投递记录。之后他加了一条告警:webhook 连续失败超过 3 次就通知他。
补充材料
思维导图、练习、清单和参考资料可按需展开。
01 思维导图和离线文件
适合读完后下载到自己的知识库里继续整理。
可靠的处理流程
- 验证签名,用原始请求体
- 记录事件 ID 去重
- 尽快返回成功
- 查询最新状态再更新
三种记录分开管
- 订单:已支付或已退款
- 订阅:试用、正常、逾期、取消
- 权益:用户能用到什么时候
出错时
- 平台会重试,但要看得到失败
- 每天对账
- 人工补单要有依据和记录

02 实践练习
根据文章内容记录自己的判断与依据。
在测试环境跑通完整流程
用 Stripe 的测试环境(或你所用平台的沙盒)实现并测试一个订阅的支付通知处理。
准备材料
- 一个测试环境的支付平台账号。
- 平台的命令行工具,用来转发和触发测试事件。
- 一个能保存订单、事件和权益的数据库。
练习步骤
01 / 接收并验证通知
时间: 40 分钟
操作:
- 配置 webhook 地址,把测试事件转发到本地。
- 实现签名验证,用一条篡改过的请求确认会被拒绝。
产出: 一个能验证签名的接收端点。
检查: 使用原始请求体验证;篡改后的请求被拒绝。
02 / 去重和异步处理
时间: 40 分钟
操作:
- 记录事件 ID 并设为唯一,收到后先返回成功。
- 重复发送同一个事件,确认只处理一次。
产出: 事件表和去重逻辑。
检查: 重复事件不会导致重复开通或重复发邮件。
03 / 状态和权益
时间: 40 分钟
操作:
- 处理时查询订阅最新状态,按状态表更新权益到期时间。
- 测试退款和取消,确认权益按规则变化。
产出: 一份测试记录。
检查: 先到达的旧事件不会覆盖已经退款的状态。
完成检查
- 权限只由服务器验证过的通知决定。
- 重复和乱序的事件都能正确处理。
- 有对账和人工补单的流程。
03 清单、模板和关键词
用于项目记录和自查。
必须处理的事件(以 Stripe 订阅为例)
checkout.session.completed:用户完成结账invoice.paid:账单支付成功,确认订阅为 active 后延长权益invoice.payment_failed:扣款失败,通知用户customer.subscription.updated:订阅状态或套餐变化customer.subscription.deleted:订阅结束,收回权限charge.refunded:退款charge.dispute.created:争议或拒付
事件表字段
事件 ID(唯一)、事件类型、关联的订单或订阅 ID、收到时间、处理状态(待处理、成功、失败)、失败原因、重试次数、处理完成时间。
人工补单记录
用户与订单:
平台交易号与状态(截图或链接):
问题原因:
补开的权益与到期时间:
操作人与时间:
是否已通知用户:
关键概念
- Webhook:支付平台主动发送给服务器的事件通知
- 签名验证:确认通知来自平台,需要原始请求体
- 幂等:同一个事件处理多次,结果和处理一次相同
- 乱序:事件到达顺序和发生顺序不一致
- 权益到期时间:决定用户能否使用的依据
- 对账:对比平台交易和本地记录
容易误判
- 根据浏览器的“支付成功”页面开通权限
- 框架自动解析请求体,导致签名验证失败
- 不记录事件 ID,重复开通、重复发邮件
- 以最后收到的事件为准,被旧事件覆盖状态
- 在返回成功之前做耗时操作,导致超时
- 收到
invoice.paid不检查订阅状态就延长权益
04 工具和参考资料
本篇涉及的工具和参考链接。
可参考工具
- Stripe CLI
- Stripe Billing
- Paddle Webhooks
- Lemon Squeezy Webhooks