订单拍了价,回呼只入一次账
面向 VOZEB PRO。真钱入账要练的不是「接一下 Stripe 回调」,而是:下单那一刻把应付拍死;用户去供应商付钱;回呼验签后按事件 id 只完整入账一次。 15 是生成花积分。这篇是人民币变成永久积分或套餐。两本账不要写在一个函数里。
先用小王买套餐钉住四步,后面才不会把「回呼来了三次」写成「加了三次分」。
| 步 | 小王看见什么 | 账上拍死什么 | 再来一次会怎样 |
|---|---|---|---|
| 下单 | 确认页 69 元 | 原价 − 促销 − 券 = 应付,写进订单行 | 同一业务幂等则同一张单,不新开价 |
| 开收银台 | 跳转微信 / Stripe | checkout 身份(metadata.checkout) |
刷新别开第二摊收银 |
| 付钱 | 在供应商 App 里付 | 钱在供应商侧 | 与本站无关 |
| 回呼入账 | 回到站点「已到账」 | 订单 paid:授信、绑套餐、核销券、邀请 |
同一事件 id / 已 paid → 不加第二笔 |
页面上的 69 只是展示。权威是订单行里的快照,禁止前端再算一遍当应付。
0. 什么时候用这套思路
先问三句,有一句对上就该掏出这篇,而不是先在 Route 里 creditPermanentPoints(100)。
- 钱是供应商收的,还是站内积分划转? 前者走订单;后者走 15。
- 回呼再来一遍,会不会再发一套餐、再核一张券? 会,说明事件去重或订单状态没锁住。
- 小王还在收银台,你改了后台价,这张单收多少? 应收下单时的快照,不是新价。
适合:
- ✅ 买套餐、买积分包、任何真钱 → 站内权益
- ✅ 支付供应商会重试 webhook、用户会刷新收银台
- ✅ 促销、优惠券要减应付
- ✅ 邀请人要在被邀请人付成功后才拿奖励
- ✅ 换支付渠道(Stripe / 微信 / 别的),怕页面重算价
- ✅ 读
billing-service.ts、payment-webhook-service.ts、payment-checkout-service.ts
不要硬套:
- ⚠️ 生成扣 8 分、规划退款(15)
- ⚠️ 后台改逻辑模型价(影响下次生成,不影响已拍死的订单)
- ⚠️ 未支付就发积分、或前端说「我付过了」就入账
- ⚠️ Worker 脚本里处理支付(回呼进的是本站 payment Route,不是 maintenance 催单)
在 VOZEB PRO 中的定位:
createBillingOrder快照计价;createPaymentCheckoutForOrder开收银台;processPaymentWebhook验签 + 事件去重 + claim 订单;completeBillingOrderPayment一次性授信/绑套餐/核销/邀请。适配在payment-*,页面不得当计价器。
1. 核心公式
真钱入账 = 快照订单 + 可幂等的收银台 + 验签回呼 + 只从 pending 完整走到 paid 一次。
calculateOrderPricing(纯函数,原价 − 促销 − 券)
│
▼
createBillingOrder → 行上写下应付快照,状态 pending
│
▼
createPaymentCheckoutForOrder → 供应商收银(checkout 幂等)
│
▼
用户在供应商付款
│
▼
webhook:验签 → 事件 id 去重 → claim 这张单
│
▼
verify → completeBillingOrderPayment
授信永久积分 / 绑套餐 / 核销券 / 播种邀请
状态 paid
之后只有 closed / canceled。canceled 要释放未核销的券,不能把已 paid 的权益收回又当没付过(退款是另一条、更严的流程)。
饭店对照:菜单印「今日 69」写进预订单(快照)。客人去前台扫码付。支付公司电话来说「69 到了」三次,后厨只上一次菜、只撕一张券。
2. 什么场景用,什么场景不用
该走订单
| 场景 | 落点 |
|---|---|
| 选套餐、选加量包、用券 | 先 createBillingOrder,再跳转收银 |
| 供应商 webhook / 同步回跳 | 只信验签后的事件,不信 querystring 里 paid=1 |
| 改促销、改目录价 | 新单用新公式;旧 pending 单看快照(产品若允许「未付改价」要另开明确规则) |
| 邀请奖励 | paid 成功之后再记账,钥匙挂邀请事件,不是挂「注册了就送」 |
不该走订单
| 场景 | 走哪 |
|---|---|
| 点生成 | 15 钱包 consume |
| 管理员手工补分 | 15 creditPermanentPoints + 管理员钥匙,或后台专用入口 |
| 生成失败退 8 分 | 15 refund,不是把订单改 canceled |
3. 下单:价拍在行上,页面不许重算
calculateOrderPricing 是纯函数:原价、促销、券 → 应付。
createBillingOrder 把结果写成订单字段。之后所有入账、对账、展示应付,读行,不读「现在后台配的价」。
券在 pending 占用,paid 才核销。取消 pending 要释放占用,否则小王取消后券永远锁死。
例子:改价
下单时 99 − 促销 20 − 券 10 = 69。小王在支付宝里待了十分钟,你把套餐改成 199。这张单仍应收 69。新点「立即购买」才是 199 的新单。若前端用 JS 再算一次当 amount 传给支付,供应商和订单会对不上,回呼要么拒要么入错账。
例子:同一加购连点
开单也应有业务幂等(用户 + 套餐 + 进行中的 pending 等,按实现)。不要每次连点都 insert 一张 69 的新单,否则收银台和回呼会对上「哪一张」。
4. 收银台:刷新不新开摊
createPaymentCheckoutForOrder 向供应商要支付页。metadata.checkout(或同类键)保证:小王 F5、返回再点「去支付」,仍是这一摊收银,不是第二笔 69 的支付意图。
换支付供应商:只换 payment-provider / checkout / webhook 适配。订单状态机和 completeBillingOrderPayment 不要复制一份。
5. 回呼:验签、事件去重、再入账
processPaymentWebhook 顺序不能倒:
- 验签 — 不是供应商的请求,当没有
- 事件 id 去重 — 同一条 webhook 体来三遍,只进一次后续
- claim 订单 — 两笔不同事件别同时把同一张 pending 打成 paid
- 核对金额 / 支付身份 与订单快照一致
completeBillingOrderPayment— 授信、绑套餐、核销、邀请,放在同一段「只成功一次」的逻辑里
complete 必须假设自己会被重试。钱包授信用 15 的 credit 钥匙(挂订单 id / 支付记录 id)。邀请奖励同样。核销券按券占用记录,核过了再来是 no-op。
例子:回呼三次
第一次:验签过、事件 evt_1 未处理、订单 pending → paid、+100 永久分、券核销、邀请人 +10。
第二次、第三次:同一 evt_1 → 去重返回,钱包不动。
若供应商换了一个事件 id 表示「同一笔支付的重复通知」,还要靠订单已是 paid / 支付记录唯一约束挡住。两道门都要。
例子:伪造回跳
小王改 URL ?status=paid。没有供应商签名,不能 complete。展示「支付结果页」可以先查订单状态,状态仍 pending 就显示等待,不要信查询参数。
6. 和钱包、后台、生成的接缝
后台改套餐目录价、促销、券规则 → 只影响下一张 createBillingOrder
│
▼
本篇 16 快照 → 收银 → 回呼 → paid
│
▼
15 creditPermanentPoints / 绑套餐(以后每日发日券)
│
▼
小王再去生成 → 15 consume 日券+永券
生成路径看不见支付供应商。支付路径不 runImageTask。
退款 lane 催的是生成失败退积分,不是支付退款;支付退款是单独、更严的产品流程,不要和 15 的 refundPoints 混成一个按钮。
接口外壳仍是 { code, data, msg }。积分不足、未登录、验签失败都走这套,不要 webhook 返回一大段 HTML 当成功。
7. 写代码时的五问
- 应付金额读的是订单行,还是此刻再跑一遍促销公式?
- webhook 没验签就入账了吗?querystring
paid=1能改状态吗? - 同一
event id第二次进来,还会applied: true地 credit 吗? - 券是 paid 才核销、取消才释放吗?邀请是 paid 之后才播种吗?
- 换支付渠道时,有没有复制一份
completeBillingOrderPayment?
8. 读完能指挥自己(或 AI)做什么
- 「
createBillingOrder写入价格快照。入账和展示应付只读快照,禁止页面重算价。」 - 「收银台按 checkout 幂等。刷新不得开第二笔支付意图。」
- 「webhook:先验签,再按事件 id 去重,再 claim 订单,最后
completeBillingOrderPayment。」 - 「
complete里授信/核销/邀请全部走带幂等键的服务;订单已 paid 则整段 no-op。」 - 「未验签的回跳不能改订单。生成扣分走 15,不要在支付模块 consume。」
- 「换渠道只加
payment-*适配,不改状态机。」
相关文档
- 日券先花,钥匙在柜上 — 入账之后怎么花
- 副作用边界上的幂等
- 鉴权认的是谁 — 回呼不是用户 Cookie
- 菜名、店和菜单 — 后台价目与逻辑模型
StudyVault/06-商业与后台/商业与后台.mdweb/src/lib/server/billing-service.tsweb/src/lib/server/payment-webhook-service.tsweb/src/lib/server/payment-checkout-service.tsweb/src/lib/server/billing-pricing.ts