fix(paywall): clarify response fields and payment channel options

This commit is contained in:
2026-06-23 14:12:37 +08:00
parent 7dd4878d7d
commit 1a56a2a5f8
+35 -20
View File
@@ -325,7 +325,9 @@ if (data.blocked === true && data.blockReason === "daily_limit") {
## 5. POST /api/chat/send 完整响应结构 ## 5. POST /api/chat/send 完整响应结构
`data` 字段的**全部字段**(现有 + 新增),所有 `/api/chat/send` 响应都包含: `data` 字段的**全部字段**(现有 + 新增),所有 `/api/chat/send` 响应都包含:
> 注意:`privateLocked` / `privateHint` / `isPrivate` **不属于** `/api/chat/send` 的响应字段;它们只出现在 `GET /api/chat/history` 的 `data.messages[]` 单条历史消息里。私密消息由后端异步打标,因此前端要在刷新/拉取历史时根据 `privateLocked=true` 渲染模糊锁定卡片。
| 字段 | 类型 | 现有/新增 | 说明 | | 字段 | 类型 | 现有/新增 | 说明 |
|------|------|----------|------| |------|------|----------|------|
@@ -341,8 +343,8 @@ if (data.blocked === true && data.blockReason === "daily_limit") {
| `isGuest` | bool \| null | 现有 | 是否游客 | | `isGuest` | bool \| null | 现有 | 是否游客 |
| `timestamp` | int | 现有 | Unix 毫秒时间戳 | | `timestamp` | int | 现有 | Unix 毫秒时间戳 |
| `blocked` | bool \| null | 现有 | 是否每日限流拦截(功能①) | | `blocked` | bool \| null | 现有 | 是否每日限流拦截(功能①) |
| `blockReason` | string \| null | 现有 | `"daily_limit"` | | `blockReason` | string \| null | 现有 | `"daily_limit"` / `"total_limit"` |
| `blockDetail` | object \| null | 现有 | `{ type, usedToday, limit }` | | `blockDetail` | object \| null | 现有 | 每日限制:`{ type, usedToday, limit }`;游客总量限制:`{ type, usedTotal, limit }` |
| `paywallTriggered` | bool | 新增 | 是否触发图片付费墙(功能②) | | `paywallTriggered` | bool | 新增 | 是否触发图片付费墙(功能②) |
| `showUpgrade` | bool | 新增 | **前端是否展示开会员引导** | | `showUpgrade` | bool | 新增 | **前端是否展示开会员引导** |
| `imageType` | string \| null | 新增 | `null` / `"elio_schedule"` | | `imageType` | string \| null | 新增 | `null` / `"elio_schedule"` |
@@ -398,20 +400,21 @@ if (data.blocked === true && data.blockReason === "daily_limit") {
| `plan_name` | string | 展示名称 | | `plan_name` | string | 展示名称 |
| `order_type` | string | 传给支付服务 B 的订单类型 | | `order_type` | string | 传给支付服务 B 的订单类型 |
| `amount_cents` | int | 金额,单位为分 | | `amount_cents` | int | 金额,单位为分 |
| `currency` | string | 当前为 `CNY` | | `original_amount_cents` | int \| null | 原价/划线价,单位为分;仅 VIP 套餐返回 |
| `daily_price_cents` | int \| null | 日均展示价,单位为分;仅 VIP 套餐返回 |
| `currency` | string | VIP 套餐当前为 `USD`DOL 充值项当前为 `CNY` |
| `vip_days` | int \| null | VIP 增加天数,`null` 表示永久或非 VIP 商品 | | `vip_days` | int \| null | VIP 增加天数,`null` 表示永久或非 VIP 商品 |
| `dol_amount` | int \| null | DOL 积分数量,VIP 商品为 `null` | | `dol_amount` | int \| null | DOL 积分数量,VIP 商品为 `null` |
当前套餐: 当前套餐:
| plan_id | plan_name | order_type | amount_cents | 发放内容 | | plan_id | plan_name | order_type | amount_cents | original_amount_cents | daily_price_cents | currency | 发放内容 |
|---|---|---:|---:|---| |---|---|---:|---:|---:|---:|---|---|
| `vip_monthly` | 月度会员 | `vip_monthly` | 999 | VIP +30 天 | | `vip_monthly` | Monthly | `vip_monthly` | 1990 | 2490 | 66 | USD | VIP +30 天 |
| `vip_quarterly` | 季度会员 | `vip_quarterly` | 2499 | VIP +90 天 | | `vip_quarterly` | Quarterly | `vip_quarterly` | 4990 | 5990 | 55 | USD | VIP +90 天 |
| `vip_annual` | 年度会员 | `vip_annual` | 5999 | VIP +365 天 | | `vip_annual` | Annual | `vip_annual` | 16990 | 19990 | 47 | USD | VIP +365 天 |
| `vip_lifetime` | 永久会员 | `vip_lifetime` | 19999 | 永久 VIP | | `dol_100` | 100 DOL | `dol` | 600 | null | null | CNY | DOL +100 |
| `dol_100` | 100 DOL | `dol` | 600 | DOL +100 | | `dol_500` | 500 DOL | `dol` | 2800 | null | null | CNY | DOL +500 |
| `dol_500` | 500 DOL | `dol` | 2800 | DOL +500 |
#### POST /api/payment/create-order #### POST /api/payment/create-order
@@ -438,7 +441,7 @@ Content-Type: application/json
| 字段 | 必填 | 说明 | | 字段 | 必填 | 说明 |
|---|---|---| |---|---|---|
| `planId` | 是 | 来自 `GET /api/payment/plans``plan_id` | | `planId` | 是 | 来自 `GET /api/payment/plans``plan_id` |
| `payChannel` | 是 | 当前支持 `stripe` / `ezpay` | | `payChannel` | 是 | 支付通道取值约定:`stripe` / `ezpay` / `paycools`。当前生产代码若仍只放开 `stripe` / `ezpay`,需要同步把 A/B 两侧白名单加上 `paycools` |
| `autoRenew` | 是 | 是否自动续费;DOL 和永久会员建议传 `false` | | `autoRenew` | 是 | 是否自动续费;DOL 和永久会员建议传 `false` |
成功响应: 成功响应:
@@ -455,7 +458,18 @@ Content-Type: application/json
} }
``` ```
`payParams` 是支付服务 B 返回给前端的拉起支付信息。对于 PayPal / Stripe Checkout / ezPay 这类浏览器跳转支付,B 必须在 `payParams` 内提供可直接打开的支付链接。 `payParams` 是支付服务 B 返回给前端的拉起支付信息。对于 Stripe Checkout / ezPay / PayCools 这类浏览器跳转支付,B 必须在 `payParams` 内提供可直接打开的支付链接。PayCools 返回的支付链接应放在 `checkout_url``payment_url``url` 中。
当前 Stripe 返回为 Checkout 链接模式。前端创建订单成功后应优先打开:
1. `data.paymentUrl`
2. `data.checkoutUrl`
3. `data.url`
4. `data.payParams.paymentUrl`
5. `data.payParams.checkoutUrl`
6. `data.payParams.url`
打开支付链接后继续轮询 `GET /api/payment/order-status?order_id=...`;支付服务 B 收到 Stripe webhook 后会发布 MQ,后端 A 消费后把订单从 `pending` 更新为 `paid`
前端读取支付链接的优先级: 前端读取支付链接的优先级:
@@ -470,7 +484,7 @@ Content-Type: application/json
| 状态码 | 场景 | | 状态码 | 场景 |
|---|---| |---|---|
| `400` | `planId` 无效,或 `payChannel` 不是 `stripe` / `ezpay` | | `400` | `planId` 无效,或 `payChannel` 不是后端当前放开的支付通道 |
| `401` | 未登录或 token 无效 | | `401` | 未登录或 token 无效 |
| `502` | `PAYMENT_SERVICE_URL` 未配置,或支付服务 B 创建订单失败 | | `502` | `PAYMENT_SERVICE_URL` 未配置,或支付服务 B 创建订单失败 |
| `500` | 后端 A 创建订单过程中发生未知错误 | | `500` | 后端 A 创建订单过程中发生未知错误 |
@@ -545,10 +559,10 @@ Content-Type: application/json
{ {
"user_id": "用户 UUID", "user_id": "用户 UUID",
"order_type": "vip_monthly", "order_type": "vip_monthly",
"pay_channel": "stripe", "pay_channel": "paycools",
"automatic_renewal": true, "automatic_renewal": true,
"amount_cents": 999, "amount_cents": 1990,
"currency": "CNY" "currency": "USD"
} }
``` ```
@@ -567,7 +581,8 @@ B 必须返回:
- `pay_params` 结构由支付服务 B 决定,后端 A 不解析,前端原样使用。 - `pay_params` 结构由支付服务 B 决定,后端 A 不解析,前端原样使用。
- 浏览器跳转支付必须返回可打开的 URL,推荐统一使用 `pay_params.checkout_url` - 浏览器跳转支付必须返回可打开的 URL,推荐统一使用 `pay_params.checkout_url`
- 当前 A/B 已确认支持的 `pay_channel``stripe` / `ezpay`;如果要接 PayPal,需要先由支付服务 B 的 `/orders` 支持 `paypal`,再把后端 A 的渠道校验和前端选项一起放开 - PayCools 返回支付链接时,建议放在 `pay_params.checkout_url`;前端也兼容 `payment_url` / `approval_url` / `url`
- 当前支付对接文档约定的 `pay_channel``stripe` / `ezpay` / `paycools`;生产代码也需要同步支持这些取值。如果要接其他通道,需要先由支付服务 B 的 `/orders` 支持该通道,再把后端 A 的渠道校验和前端选项一起放开。
- 后端 A 会把 `order_id``user_id``order_type``plan_id``status=pending` 写入本地 `orders` 表。 - 后端 A 会把 `order_id``user_id``order_type``plan_id``status=pending` 写入本地 `orders` 表。
- 发放什么权益由 A 侧 `plan_id` 决定,B 不需要知道 VIP 天数或 DOL 数量。 - 发放什么权益由 A 侧 `plan_id` 决定,B 不需要知道 VIP 天数或 DOL 数量。
@@ -684,7 +699,7 @@ PAYMENT_WEBHOOK_SECRET=<MQ 消息签名密钥,可空>
2. GET /api/payment/plans 加载套餐 2. GET /api/payment/plans 加载套餐
3. 用户选择 planId、payChannel、autoRenew 3. 用户选择 planId、payChannel、autoRenew
4. POST /api/payment/create-order 4. POST /api/payment/create-order
5. 从 data.payParams 读取 checkout_url/payment_url/approval_url/url 并打开支付页,或按约定调用支付 SDK 5. 从 data.payParams 读取 checkout_url/payment_url/approval_url/url 并打开支付页,PayCools 链接也走这个逻辑,或按约定调用支付 SDK
6. 每 3~5 秒 GET /api/payment/order-status?order_id=... 6. 每 3~5 秒 GET /api/payment/order-status?order_id=...
7. 收到 paid 或 WebSocket payment_success 后停止轮询 7. 收到 paid 或 WebSocket payment_success 后停止轮询
8. payType=vip 时调用 /api/payment/vip-status 并刷新本地会员状态 8. payType=vip 时调用 /api/payment/vip-status 并刷新本地会员状态