Appearance
支付与充值—— 双通道目录、下单、开闸边界
读者:模式 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 档):
| channel | currency | amount_cents → 积分 |
|---|---|---|
wechat | CNY | 1000→100 / 5000→500 / 10000→1000 / 20000→2000(分) |
paypal | USD | 200→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 | 渠道 / 币种 | 实装 |
|---|---|---|
NATIVE | wechat / CNY | 有出站服务则返回 code_url(二维码);无凭据 → 503 |
JSAPI | wechat / CNY | 返回 jsapi_params;需支付 openid;无凭据 → 503 |
H5 / APP | 503(未启用) | |
PAYPAL_WEB | paypal / USD | schema 放行,路由仍 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 |
| PayPal | POST /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/balanceGET /api/v1/billing/points/ledger?user_id=&project_id=
模式 B 模板积分页应能展示本人余额。运营侧 ledger 仍用于对账与客服。
6. 接入检查清单
- 结账 UI 只渲染本产品线渠道的目录行。
- 未开闸:隐藏「去支付」,展示「联系运营入账」或种子积分。
- 开闸后:下单 → 微信出码 /(未来)PayPal approve;入账靠回调,前端轮询
.../status。 - 402
POINTS_INSUFFICIENT引导充值或找运营,不要重放同一轮(零副作用,余额够了再开新轮)。 - 品牌自托管站:PayPal 是海外主线;微信按钮不要画出来,除非该线明确走大陆回迁。
7. 相关页
- 能力现状:platform-status.md
- 模式 B 钱包语义:saas-mode-b-integration-guide.md
- 对账键与轮次计费:conversation-runtime-protocol.md §7