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

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

---

## 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` | wechat | **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](./tutorial/hello-agent.md)。

---

## 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. 相关页

- 能力现状：[platform-status.md](./platform-status.md)
- 模式 B 钱包语义：[saas-mode-b-integration-guide.md](./saas-mode-b-integration-guide.md)
- 对账键与轮次计费：[conversation-runtime-protocol.md](./conversation-runtime-protocol.md) §7
