feat(tip): support dynamic gift products
This commit is contained in:
@@ -23,9 +23,10 @@
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/payment/plans` | VIP 与 Top-up 套餐目录 |
|
||||
| `GET` | `/api/payment/tip-plans` | Tip 套餐目录 |
|
||||
| `GET` | `/api/payment/gift-products?characterId={id}` | 当前角色的完整礼物目录 |
|
||||
| `POST` | `/api/payment/create-order` | 创建 VIP、Top-up 或 Tip 订单 |
|
||||
| `GET` | `/api/payment/order-status?order_id={id}` | 查询订单状态 |
|
||||
| `POST` | `/api/payment/tip-message` | 获取已支付礼物订单的稳定感谢文案 |
|
||||
|
||||
所有响应先由通用 envelope 解包,再进入 Payment Schema。页面和状态机不直接读取原始 envelope。
|
||||
|
||||
@@ -65,29 +66,44 @@ promotionType
|
||||
|
||||
支付成功后会清除默认套餐缓存、刷新用户权益,并在当前 Actor 中消费首充展示状态。服务端下一次目录响应仍是最终权威结果。
|
||||
|
||||
### 3.2 Tip 套餐
|
||||
### 3.2 Gift Products 目录
|
||||
|
||||
```http
|
||||
GET <API_BASE_URL>/api/payment/tip-plans
|
||||
Authorization: Bearer <TOKEN>
|
||||
GET <API_BASE_URL>/api/payment/gift-products?characterId=elio
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"characterId": "elio",
|
||||
"categories": [
|
||||
{
|
||||
"category": "coffee",
|
||||
"name": "Coffee",
|
||||
"productCount": 1,
|
||||
"imageUrl": null
|
||||
}
|
||||
],
|
||||
"plans": [
|
||||
{
|
||||
"planId": "tip_coffee_usd_4_99",
|
||||
"planName": "Small Coffee",
|
||||
"planName": "Velvet Espresso",
|
||||
"orderType": "tip",
|
||||
"tipType": "coffee_small",
|
||||
"category": "coffee",
|
||||
"characterId": "elio",
|
||||
"description": "Buy Elio a small coffee",
|
||||
"imageUrl": null,
|
||||
"amountCents": 499,
|
||||
"currency": "USD"
|
||||
"currency": "USD",
|
||||
"autoRenew": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Tip 目录不写入默认套餐缓存。Repository 会把精简 Tip 套餐归一化为 `PaymentPlan`,其中 `orderType="tip"`、`autoRenew=false`,且不带 VIP、积分或首充权益。
|
||||
目录不需要登录,也不写入默认套餐缓存。前端必须传当前角色 ID,一次读取 `categories` 和全部 `plans`;当前 Tip 页面固定使用第一分类,并按商品原始顺序默认选择第一件商品,不展示分类切换栏。
|
||||
|
||||
Tip 页面只允许选择本地 Coffee Tier 能映射到的后端 `planId`。后端未返回对应套餐时,该 Tier 标记为不可用,不能使用本地价格创建订单。
|
||||
名称、说明、图片、金额、币种和 `planId` 全部来自同一条商品数据。状态机把当前分类商品映射为 `PaymentPlan` 以复用 Checkout 和埋点,同时保留完整 Gift Product 供 UI 展示。目录为空或请求失败时禁止创建订单,不回退到本地固定商品或价格。
|
||||
|
||||
## 4. 创建订单
|
||||
|
||||
@@ -180,21 +196,21 @@ Authorization: Bearer <TOKEN>
|
||||
"status": "paid",
|
||||
"orderType": "tip",
|
||||
"planId": "tip_coffee_usd_4_99",
|
||||
"tipCount": 2,
|
||||
"thankYouMessage": "You made my day a little sweeter."
|
||||
"creditsAdded": 0
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 前端规则 |
|
||||
| --- | --- |
|
||||
| `status` | 只接受 `pending`、`paid`、`failed` |
|
||||
| `tipCount` | 仅保留正整数,其他值归一化为 `null` |
|
||||
| `thankYouMessage` | trim 后的非空纯文本,其他值归一化为 `null` |
|
||||
| `status` | 只接受 `pending`、`paid`、`failed`、`expired` |
|
||||
| `planId` | 可为 `null` |
|
||||
| `creditsAdded` | 整数;Tip 固定为 `0` |
|
||||
|
||||
状态机创建订单或恢复订单后立即查询一次。`pending` 每 4 秒再次查询,最长持续 5 分钟:
|
||||
|
||||
- `paid`:进入最终成功状态并停止轮询;
|
||||
- `failed`:进入失败状态并停止轮询;
|
||||
- `expired`:进入订单过期状态、销毁旧支付参数并停止轮询;
|
||||
- 超过 5 分钟:本地标记为失败并显示超时错误;
|
||||
- 查询请求本身失败:进入失败状态,不在当前 Actor 中自动重试。
|
||||
|
||||
@@ -206,7 +222,8 @@ Authorization: Bearer <TOKEN>
|
||||
orderId
|
||||
payChannel = ezpay
|
||||
subscriptionType = vip | topup | tip
|
||||
tipCoffeeType = small | medium | large(仅 Tip)
|
||||
giftCategory(仅 Tip,可空)
|
||||
giftPlanId(仅 Tip,可空)
|
||||
returnTo = chat | private-room | sidebar(可选)
|
||||
characterSlug(可选)
|
||||
createdAt
|
||||
@@ -216,10 +233,10 @@ createdAt
|
||||
|
||||
```text
|
||||
VIP / Top-up -> /subscription?type=...&payChannel=ezpay&paymentReturn=1
|
||||
Tip -> /characters/{slug}/tip?payChannel=ezpay&paymentReturn=1&coffee_type=...
|
||||
Tip -> /characters/{slug}/tip?category=...&planId=...&payChannel=ezpay&paymentReturn=1
|
||||
```
|
||||
|
||||
恢复页面只接受与当前 `paymentType` 相同的待处理订单。带有 `paymentReturn=1` 时派发 `PaymentReturned` 并继续轮询;普通进入支付页时会清理同类型的旧待处理订单。订单进入 paid 或 failed 后清理持久化记录。
|
||||
恢复页面只接受与当前 `paymentType` 相同的待处理订单。带有 `paymentReturn=1` 时派发 `PaymentReturned` 并继续轮询;普通进入支付页时会清理同类型的旧待处理订单。订单进入 paid、failed 或 expired 后清理持久化记录。缺少 Gift 字段的旧记录由最新目录默认选择第一件商品,不再读取 `coffee_type`。
|
||||
|
||||
无效或未知 `characterSlug` 回退到默认角色 slug。有效角色回跳必须保留原角色,不能统一返回 Elio。`returnTo=private-room` 的最终页面行为由 [Private Room 权威协议](./FRONTEND_PRIVATE_ROOM_API.md) 定义。
|
||||
|
||||
@@ -237,21 +254,19 @@ Subscription 显示成功 Dialog,关闭后根据 `returnTo` 和原角色 slug
|
||||
|
||||
## 9. Tip 成功结果
|
||||
|
||||
paid Tip 订单可以返回:
|
||||
Tip 订单进入 paid 后调用:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `tipCount` | 包含当前订单在内,当前付款身份向当前收款角色的累计成功次数 |
|
||||
| `thankYouMessage` | 当前订单对应角色的纯文本感谢语 |
|
||||
```http
|
||||
POST <API_BASE_URL>/api/payment/tip-message
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <TOKEN>
|
||||
|
||||
前端展示规则:
|
||||
{"orderId":"pay_xxx"}
|
||||
```
|
||||
|
||||
1. `tipCount=1`:使用首次打赏本地文案;
|
||||
2. `tipCount>1` 且感谢语非空:展示英文序数次数和后端感谢语;
|
||||
3. 字段缺失、非法或不完整:展示通用成功文案,不阻止订单进入 paid;
|
||||
4. 感谢语按纯文本展示并保留换行,不渲染 HTML。
|
||||
响应包含 `orderId`、`characterId`、`planId`、`productName`、`tipCount`、`poolIndex` 和可直接展示的完整 `message`。前端不再根据次数拼接文案,也不从订单状态读取感谢字段。
|
||||
|
||||
`tipCount` 和 `thankYouMessage` 必须对同一个 `orderId` 保持稳定。pending、failed 和非 Tip 订单应返回 `null`,前端也会将无效值归一化为 `null`。
|
||||
Tip 成功页在文案加载期间立即确认支付成功;接口成功后按纯文本直接展示 `message`。接口失败时展示本地通用感谢语和 Retry,重试只重新请求 Tip Message,不重复轮询订单或创建订单。
|
||||
|
||||
## 10. Provider 与状态边界
|
||||
|
||||
@@ -271,6 +286,7 @@ Payment 协议相关变更至少验证:
|
||||
4. `src/lib/payment/__tests__`;
|
||||
5. `src/app/_hooks/__tests__` 中的 Payment 流程测试;
|
||||
6. `src/app/subscription/__tests__` 与 `src/app/tip/__tests__`;
|
||||
7. Stripe、Ezpay、paid、failed、timeout 和回跳恢复路径;
|
||||
7. Stripe、Ezpay、paid、failed、expired、timeout 和回跳恢复路径;
|
||||
8. Tip 创建订单携带当前角色 ID,VIP/Top-up 不携带;
|
||||
9. 支付成功后用户权益、套餐缓存和 Chat 解锁协调结果正确。
|
||||
9. Tip Message 成功、失败和重试不重复创建订单;
|
||||
10. 支付成功后用户权益、套餐缓存和 Chat 解锁协调结果正确。
|
||||
|
||||
Reference in New Issue
Block a user