Webhook、订阅与积分
了解持久履约、每月积分发放、幂等处理和手动退款流程。
VibeAny 将带签名的 Webhook 和数据库状态视为账单系统的唯一事实来源。结账返回页只负责向用户提供反馈。
持久 Webhook 流水线
所有支付服务商都把事件发送到同一路由:
POST /api/payments/webhook当前服务商的验证器按以下顺序处理事件:
- 读取未经修改的请求体;
- 验证当前服务商的签名;
- 把服务商事件转换成应用层统一账单事件;
- 以服务商和事件 ID 组成幂等键;没有事件 ID 时改用载荷哈希,并通过该键从
webhook_inbox取得处理权; - 执行订阅或积分履约;
- 只有履约成功后,才把收件箱记录标记为已处理。
重复投递会返回成功,并带有 deduped: true。如果履约失败,系统会释放处理权,供服务商重试。签名无效时返回 HTTP 400;服务商配置不完整时返回 HTTP 503。
Webhook 验证同样遵循单服务商模式,因此未启用服务商发来的 Webhook 不会被路由到对应验证器。这正是修改 PAYMENTS_PROVIDER 不能替代迁移方案的原因。
订阅状态
订阅 Webhook 会保存:
- 服务商以及外部订阅/客户 ID;
- 方案和计费周期;
- 生命周期状态和到期取消状态;
- 当前周期边界;
- 服务商事件时间和下一次计划积分发放时间。
同一用户的写入会串行执行,较旧的服务商事件不能覆盖较新的状态。数据库会强制每个用户只有一条订阅记录。
invoice 成功事件会记录支付生命周期,但不会直接发放积分。invoice payment failed 事件当前不会触发后续业务动作。催收、访问限制和客户通知都属于产品政策;实现这些策略前,不要对用户作出相关承诺。
独立调度每月订阅积分
订阅积分由应用自己的月度调度发放,而不是在每次 invoice Webhook 到达时发放。这样,年付订阅仍能按月获得积分配额。
Vercel 每天调用一次受保护的调度器:
GET /api/cron/subscription-credits
Authorization: Bearer <CRON_SECRET>内置调度表达式是 5 0 * * *,即每天 UTC 00:05。只要启用了订阅商品,就必须配置强随机、仅供服务端使用的 CRON_SECRET。
调度器会对每个 active、trialing 或已安排取消的订阅执行:
- 以订阅周期开始时间作为月度锚点;
- 创建幂等的
subscription_grant积分批次; - 在下一次月度发放或订阅周期结束时使该批次过期,以较早者为准;
- 通过 compare-and-set 原子更新下一次发放时间。
Webhook 流程也会运行到期积分发放用例,因此新激活订阅不必等待下一次每日 cron,就能收到已到期的首次发放。
积分账本行为
系统优先消耗最早且未过期的积分批次。积分账本同时维护缓存余额和 FIFO 投影,来源标识可防止同一笔支付或同一次计划发放重复入账。
内置积分包会增加:
| SKU | 积分 | 有效期 |
|---|---|---|
credits_small | 100 | 履约后 365 天 |
credits_medium | 500 | 履约后 365 天 |
credits_large | 1,500 | 履约后 365 天 |
AI 图片生成每张消耗 10 积分。系统会在调用模型服务商前扣除积分;如果服务商生成失败,则返还积分。
Free 方案的商品文案不等于自动周期发放任务。如果产品承诺定期赠送免费积分,请新增并测试明确的调度任务或新用户发放逻辑。
手动退款政策
Starter 将退款资格评估与退款执行分开处理。
设置订阅积分使用比例的严格上限(不含边界):
PAYMENTS_SUBSCRIPTION_REFUND_MAX_USAGE_PERCENT=5值为 5 时,使用比例低于 5% 才会得到全额退款建议;恰好 5% 不符合条件。默认政策将积分包标记为不可退款,但适用法律另有要求时除外。
运行只读评估:
npm run payments:refund:assess -- \
--product subscription \
--granted-credits 500 \
--consumed-credits 24评估积分包时,传入 --product credit-package。
该命令不会读取数据库、发起退款、取消订阅或修改积分。执行退款前:
- 找到原始服务商、扣款、计费周期和准确的积分批次;
- 根据该批次计算消耗量,不要使用用户的总余额;
- 保留评估结果;
- 在原始支付服务商后台执行审核通过的全额退款;
- 如有需要,另行取消订阅;
- 手动核对剩余权益,并记录最终的服务商退款 ID。
服务商退款事件不会自动映射回积分批次。当前 Starter 中,自动冲销、支付争议和拒付都需要手动核对。
验证
npm run preflight
npm run test:unit
npm run test:credit-ledger:integration
npm run test:subscription:integrationPostgreSQL 集成测试套件需要可销毁的测试数据库。完整支付验收还要求完成服务商测试支付、公网 HTTPS Webhook 投递、重复事件重放,并从 Webhook 收件箱、subscriptions 和 credit_transactions 回读数据库状态。