Skip to content

支付与充值—— 双通道目录、下单、开闸边界 ​

读者:模式 B 薄壳前端作者、模式 A 服务端集成方、AI coding agent。 状态基准:2026-09-03。渠道/币种列、wechat/CNY + paypal/USD 目录、微信 JSAPI/Native 出站、PayPal adapter + webhook 均已落代码。对终端用户的真收款仍未开闸。配套:能力现状 §2 · mode-b §5 · 品牌线默认走 PayPal(海外)见 品牌自托管。


0. 你该承诺什么 ​

对用户说现在能不能说
「登录后能看到余额、流水」能。saas_user 已含 billing:points:read / billing:ledger:read
「运营可以给种子积分」能。tenant_grant(expires_at 必填)
「用户自己扫码 / PayPal 付钱入账」不能,除非平台运维确认对应闸门已开
「价格以页面写死的 1000 分=10 元为准」不能。必须拉服务端目录

开闸前标准形态 = 积分页展示余额 + 运营手动入账。开闸后同一套目录与下单端点,前端按渠道过滤。


1. 权威价目:GET /api/v1/billing/recharge-plans ​

无需鉴权。每条 item 带 channel + currency;amount_cents 是该币种的最小货币单位;point_amount 锚定积分,不做实时汇率。

当前目录(代码内建,运营可调 USD 档):

channelcurrencyamount_cents → 积分
wechatCNY1000→100 / 5000→500 / 10000→1000 / 20000→2000(分)
paypalUSD200→100 / 1000→500 / 2000→1000 / 4000→2000(美分)

前端规则:

  • 结账前拉此列表,禁止硬编码价格。
  • 微信端只消费 channel=wechat 且 currency=CNY。
  • 海外品牌站只消费 channel=paypal 且 currency=USD。
  • 会话内可缓存,发版后须刷新。

2. 下单:POST /api/v1/payment/orders ​

平台 JWT(登录态即可)。tenant_id / user_id 由 JWT 覆盖,body 同名字段不可冒充他人。points_amount 由服务端按目录覆写,客户端传入不被信任。

trade_type 推导渠道,渠道推导唯一币种:

trade_type渠道 / 币种实装
NATIVEwechat / CNY有出站服务则返回 code_url(二维码);无凭据 → 503
JSAPIwechat / CNY返回 jsapi_params;需支付 openid;无凭据 → 503
H5 / APPwechat503(未启用)
PAYPAL_WEBpaypal / USDschema 放行,路由仍 503 trade_type 'PAYPAL_WEB' is not enabled

amount_cents 必须命中该渠道目录,否则 422(跨渠道金额不会「碰巧」命中)。

幂等:local_order_no / wechat_out_trade_no。SIGNTEST 前缀探针流量 400 拒绝。

轮询:GET /api/v1/payment/orders/{order_id}/status(JWT;跨租户 404 同形)。

退款:POST /api/v1/payment/orders/{order_id}/refund(需 billing:refund:write)。开闸前不要对终端用户暴露退款按钮。


3. 回调与入账(你通常不接,但要对账) ​

渠道回调入账幂等键缺凭据
微信POST /api/v1/billing/webhook/wxpay(平台内部)wx_cb:<transaction_id>503 fail-closed
PayPalPOST /api/v1/billing/webhook/paypal(PayPal 签名头,无平台 JWT)pp_cb:<capture_id>503 fail-closed

重放同一 operation_id 余额不变。SaaS 侧若自建订单,用你生成的 operation_id 与平台流水对齐。

PayPal webhook 需要的头:paypal-transmission-id / paypal-transmission-time / paypal-auth-algo / paypal-cert-url / paypal-transmission-sig。这是渠道服务器调用平台的面,不要从浏览器打。


4. 开闸前:手动入账 ​

bash
curl -sk -X POST "$BASE/api/v1/billing/points:tenant-grant" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "user_id":"'$USER_ID'",
    "project_id":"'$PROJECT_ID'",
    "amount":200,
    "operation_id":"grant:seed-20260903-0001",
    "expires_at":"2027-09-03T00:00:00Z",
    "reason":"种子积分"
  }'
  • expires_at 必填(长效积分到期,ISO-8601)。漏带 422。
  • 重放同一 operation_id 返回 idempotent_replay: true。
  • 流水 transaction_type 常见 manual_adjust / freeze / deduct / unfreeze。
  • grant 仍是运营动作,不是终端用户自助。

完整示例也在 hello-agent §5.5.1。


5. 终端用户读余额与流水 ​

saas_user 角色已带只读 scope(旧文档写「积分页 403 / SM5-1」的,作废):

  • GET /api/v1/billing/points/balance
  • GET /api/v1/billing/points/ledger?user_id=&project_id=

模式 B 模板积分页应能展示本人余额。运营侧 ledger 仍用于对账与客服。


6. 接入检查清单 ​

  1. 结账 UI 只渲染本产品线渠道的目录行。
  2. 未开闸:隐藏「去支付」,展示「联系运营入账」或种子积分。
  3. 开闸后:下单 → 微信出码 /(未来)PayPal approve;入账靠回调,前端轮询 .../status。
  4. 402 POINTS_INSUFFICIENT 引导充值或找运营,不要重放同一轮(零副作用,余额够了再开新轮)。
  5. 品牌自托管站:PayPal 是海外主线;微信按钮不要画出来,除非该线明确走大陆回迁。

7. 相关页 ​