11 KiB
CozSweet Payment 权威协议
1. 状态与范围
本文是前端仓库中 VIP、Top-up、Tip、支付渠道、订单轮询和支付回跳的唯一人工维护协议。旧的 Tip 成功结果扩展文档已删除,不再单独定义 Payment 行为。
协议描述当前前端实际执行的行为。字段和状态由以下机器可验证入口约束:
| 边界 | 实现位置 |
|---|---|
| API 路径与方法 | src/data/services/api/api_contract.json |
| 请求与响应字段 | src/data/schemas/payment |
| API 调用 | src/data/services/api/payment_api.ts |
| Payment 状态机 | src/stores/payment |
| 支付拉起与回跳 | src/app/_hooks/use-payment-launch-flow.ts、src/lib/payment |
| Subscription 页面 | src/app/subscription |
| Tip 页面 | src/app/tip |
修改上述实现时必须在同一变更中更新本文,不能再新增按支付页面或支付渠道拆分的并行协议。
2. API 总览
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/api/payment/plans |
VIP 与 Top-up 套餐目录 |
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。
3. 套餐目录
3.1 默认套餐
GET <API_BASE_URL>/api/payment/plans
Authorization: Bearer <TOKEN>
响应数据:
{
"isFirstRecharge": true,
"firstRechargeOffer": {
"enabled": true,
"type": "topup",
"discountPercent": 50
},
"plans": []
}
plans[] 的标准字段:
planId, planName, orderType, vipDays, dolAmount, creditBalance,
amountCents, originalAmountCents, dailyPriceCents, currency,
isFirstRechargeOffer, mostPopular, firstRechargeDiscountPercent,
promotionType
默认目录写入本地套餐缓存。Payment Actor 启动时先读缓存;有缓存则先渲染并后台刷新,没有缓存则直接请求网络。
支付成功后会清除默认套餐缓存、刷新用户权益,并在当前 Actor 中消费首充展示状态。服务端下一次目录响应仍是最终权威结果。
3.2 Gift Products 目录
GET <API_BASE_URL>/api/payment/gift-products?characterId=elio
{
"characterId": "elio",
"categories": [
{
"category": "coffee",
"name": "Coffee",
"productCount": 1,
"imageUrl": null
}
],
"plans": [
{
"planId": "tip_coffee_usd_4_99",
"planName": "Velvet Espresso",
"orderType": "tip",
"tipType": "coffee_small",
"category": "coffee",
"characterId": "elio",
"description": "Buy Elio a small coffee",
"imageUrl": null,
"amountCents": 499,
"currency": "USD",
"autoRenew": false
}
]
}
目录不需要登录,也不写入默认套餐缓存。前端必须传当前角色 ID,一次读取 categories 和全部 plans;当前 Tip 页面固定使用第一分类,并按商品原始顺序默认选择第一件商品,不展示分类切换栏。
名称、说明、图片、金额、币种和 planId 全部来自同一条商品数据。状态机把当前分类商品映射为 PaymentPlan 以复用 Checkout 和埋点,同时保留完整 Gift Product 供 UI 展示。目录为空或请求失败时禁止创建订单,不回退到本地固定商品或价格。
4. 创建订单
POST <API_BASE_URL>/api/payment/create-order
Content-Type: application/json
Authorization: Bearer <TOKEN>
标准请求:
{
"planId": "tip_coffee_usd_4_99",
"payChannel": "stripe",
"autoRenew": false,
"recipientCharacterId": "maya-tan"
}
| 字段 | 规则 |
|---|---|
planId |
必须来自当前 Actor 已加载的目录 |
payChannel |
stripe 或 ezpay |
autoRenew |
Tip 和一次性 Top-up 为 false;VIP 根据套餐和用户选择决定 |
recipientCharacterId |
Schema 可选;当前 Tip 页面必传角色业务 ID,VIP 和 Top-up 不传 |
Payment Actor 只有在 selectedPlanId 非空且用户已同意协议时创建订单。套餐加载、创建和轮询期间页面会禁用重复提交;Tip 在 paid 状态展示成功页,Subscription 在 paid 状态展示成功 Dialog,随后通过 reset 明确开始下一笔订单。
创建订单响应统一归一化为:
interface CreatePaymentOrderResponse {
orderId: string;
payParams: Record<string, unknown>;
}
后端可以把支付 URL 放在 payParams,也可以使用以下顶层兼容字段;Schema 会把非空值合并进 payParams:
cashierUrl/cashier_url
checkoutUrl/checkout_url
paymentUrl/payment_url
approvalUrl/approval_url
redirectUrl/redirect_url
url
5. 支付渠道与拉起方式
5.1 渠道选择
| 环境与地区 | 行为 |
|---|---|
| 生产环境、菲律宾 | 可选择 Stripe 或 Ezpay,默认 Ezpay |
| 生产环境、其他地区 | 强制 Stripe,不展示渠道选择器 |
| 非生产环境 | 允许选择两个渠道;菲律宾默认 Ezpay,其他地区默认 Stripe |
当生产环境不允许选择渠道时,URL 中请求的 payChannel 不生效。
5.2 Stripe
payParams.clientSecret 或 payParams.client_secret 存在,且 provider 未声明为非 Stripe 时,前端动态加载嵌入式 Stripe Dialog。
关闭尚未支付的 Stripe Dialog 会重置当前订单状态;确认支付后隐藏 Dialog,并继续由 Payment Actor 轮询订单状态。
5.3 Ezpay 与其他跳转支付
payParams.provider="ezpay" 且存在支付 URL 时:
- 生产环境先保存待恢复订单,然后直接跳转外部支付页;
- 非生产环境先显示确认 Dialog,确认后保存并跳转;
- 待恢复订单保存失败时不得离开当前页面。
存在支付 URL 但 provider 不是 Ezpay 时,前端直接设置 window.location.href。既没有 Stripe client secret 也没有支付 URL 时,订单进入失败状态并展示参数错误。
6. 订单状态轮询
GET <API_BASE_URL>/api/payment/order-status?order_id=<ORDER_ID>
Authorization: Bearer <TOKEN>
响应数据:
{
"orderId": "tip_order_123",
"status": "paid",
"orderType": "tip",
"planId": "tip_coffee_usd_4_99",
"creditsAdded": 0
}
| 字段 | 前端规则 |
|---|---|
status |
只接受 pending、paid、failed、expired |
planId |
可为 null |
creditsAdded |
整数;Tip 固定为 0 |
状态机创建订单或恢复订单后立即查询一次。pending 每 4 秒再次查询,最长持续 5 分钟:
paid:进入最终成功状态并停止轮询;failed:进入失败状态并停止轮询;expired:进入订单过期状态、销毁旧支付参数并停止轮询;- 超过 5 分钟:本地标记为失败并显示超时错误;
- 查询请求本身失败:进入失败状态,不在当前 Actor 中自动重试。
7. 外部支付回跳
只有 Ezpay 跳转需要持久化 PendingPaymentOrder:
orderId
payChannel = ezpay
subscriptionType = vip | topup | tip
giftCategory(仅 Tip,可空)
giftPlanId(仅 Tip,可空)
returnTo = chat | private-room | sidebar(可选)
characterSlug(可选)
createdAt
/subscription/return 读取该记录并恢复到对应入口:
VIP / Top-up -> /subscription?type=...&payChannel=ezpay&paymentReturn=1
Tip -> /characters/{slug}/tip?category=...&planId=...&payChannel=ezpay&paymentReturn=1
恢复页面只接受与当前 paymentType 相同的待处理订单。带有 paymentReturn=1 时派发 PaymentReturned 并继续轮询;普通进入支付页时会清理同类型的旧待处理订单。订单进入 paid、failed 或 expired 后清理持久化记录。缺少 Gift 字段的旧记录由最新目录默认选择第一件商品,不再读取 coffee_type。
无效或未知 characterSlug 回退到默认角色 slug。有效角色回跳必须保留原角色,不能统一返回 Elio。returnTo=private-room 的最终页面行为由 Private Room 权威协议 定义。
8. 成功后的跨域同步
PaymentSuccessSync 在每个订单首次进入 paid 时:
- 消费当前 Actor 的首充展示状态;
- 清除默认套餐缓存;
- 派发
UserFetch刷新积分、VIP 和权益。
Chat 路由额外挂载 ChatPaymentSuccessSync。如果当前角色没有待恢复的单消息解锁,它会派发 ChatPaymentSucceeded,由 Chat 决定是否展示历史解锁提示;存在待恢复单消息解锁时,由原解锁流程接管。
Subscription 显示成功 Dialog,关闭后根据 returnTo 和原角色 slug 返回。Tip 直接显示角色成功页,重置后可以再次创建订单。
9. Tip 成功结果
Tip 订单进入 paid 后调用:
POST <API_BASE_URL>/api/payment/tip-message
Content-Type: application/json
Authorization: Bearer <TOKEN>
{"orderId":"pay_xxx"}
响应包含 orderId、characterId、planId、productName、tipCount、poolIndex 和可直接展示的完整 message。前端不再根据次数拼接文案,也不从订单状态读取感谢字段。
Tip 成功页在文案加载期间立即确认支付成功;接口成功后按纯文本直接展示 message。接口失败时展示本地通用感谢语和 Retry,重试只重新请求 Tip Message,不重复轮询订单或创建订单。
10. Provider 与状态边界
/subscription使用独立 Payment Actor;/characters/{slug}/tip使用以characterId为 key 的 Payment Actor;- Chat 路由使用角色级 Payment Actor,以便支付成功桥接当前 Chat;
- 切换角色或离开对应 Provider 后,不得把旧订单状态显示到另一个角色页面;
- Payment Actor 只根据订单状态接口判断最终结果,不根据支付 Dialog 或外部跳转本身推断已扣款。
11. 变更验收
Payment 协议相关变更至少验证:
src/data/services/api/__tests__/payment_api.test.ts;src/data/repositories/__tests__/payment_repository.test.ts;src/stores/payment/__tests__;src/lib/payment/__tests__;src/app/_hooks/__tests__中的 Payment 流程测试;src/app/subscription/__tests__与src/app/tip/__tests__;- Stripe、Ezpay、paid、failed、expired、timeout 和回跳恢复路径;
- Tip 创建订单携带当前角色 ID,VIP/Top-up 不携带;
- Tip Message 成功、失败和重试不重复创建订单;
- 支付成功后用户权益、套餐缓存和 Chat 解锁协调结果正确。