feat: 实现 iOS Apple Pay 内购支付功能
前端: - 集成 in_app_purchase 插件,实现 IAP 支付流程 - 添加支付模块 (payments/) 处理产品获取、购买、验证 - 积分中心页面集成 Apple Pay 购买入口 - 设置页面重构: 关于/隐私/协议直接展示,删除 legal_center 子页面 - 修复欢迎引导页滚动检测阈值问题 - 修复解卦结果页 iOS 侧滑返回手势被阻止的问题 - 邀请码绑定按钮临时禁用(待后端实现) 后端: - 新增 apple_iap_transactions 表记录交易 - 实现 Apple 服务器端验证 (App Store Server API) - 支付成功后自动发放积分 - 支持 Sandbox/Production 环境切换 - 添加退款处理和交易状态机 协议: - 更新积分流水协议,支持 purchase/refund 类型 - 新增 PAYMENT_* 错误码
This commit is contained in:
@@ -100,6 +100,21 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
||||
| `FEEDBACK_INVALID_IMAGE_TYPE` | 400 | Image type not supported (only jpg/png) | Show supported format hint |
|
||||
| `FEEDBACK_SUBMIT_FAILED` | 500 | Feedback submission failed | Show retry prompt |
|
||||
|
||||
## Payment (Apple IAP)
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|---|---:|---|---|
|
||||
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | Refresh packages and show product-unavailable message |
|
||||
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | Block grant and prompt retry |
|
||||
| `PAYMENT_ENVIRONMENT_MISMATCH` | 422 | Transaction environment (Sandbox/Production) does not match server environment | Show purchase-verification-failed message |
|
||||
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | Show purchase-verification-failed message |
|
||||
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | Show purchase-unavailable message |
|
||||
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | Prompt to contact support or refresh balance |
|
||||
| `PAYMENT_STARTER_PACK_INELIGIBLE` | 409 | Current email identity has already purchased starter pack | Refresh packages and hide starter pack |
|
||||
| `PAYMENT_APPLE_UNAVAILABLE` | 503 | Apple Server API or certificate fetch unavailable | Show retry-later message; do NOT complete/finish transaction |
|
||||
| `PAYMENT_GRANT_FAILED` | 500 | Verification succeeded but grant transaction failed | Show retry-later message; retain transaction for compensation |
|
||||
| `PAYMENT_REFUND_INSUFFICIENT_BALANCE` | 409 | User has insufficient balance for refund clawback | Log for manual review; do not auto-clawback |
|
||||
|
||||
## Global
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|
||||
@@ -34,6 +34,25 @@ Protocol verification status:
|
||||
- Billing idempotency key for per-run consume: `chat.run.success:{sha1(session_id:run_id)}`.
|
||||
- Failed/canceled runs do not deduct user points. If real provider cost is observed, audit record is written with `billed_to='platform'`.
|
||||
|
||||
## Points Change Types (change_type)
|
||||
|
||||
| Type | Direction | Meaning | biz_type | Description |
|
||||
|------|-----------|---------|----------|-------------|
|
||||
| `register` | +1 | 注册奖励 | `null` | 新用户注册赠送积分 |
|
||||
| `consume` | -1 | 消费扣减 | `chat` | 用户占卜消耗积分 |
|
||||
| `adjust` | ±1 | 手动调整 | `null` | 系统或管理员手动调整积分,通用调整不绑定业务场景 |
|
||||
| `purchase` | +1 | 购买入账 | `payment` | 用户支付购买积分 |
|
||||
| `refund` | -1 | 退款扣回 | `payment` | 退款后扣回积分 |
|
||||
|
||||
## Points Business Types (biz_type)
|
||||
|
||||
| Type | Meaning | Associated change_type |
|
||||
|------|---------|------------------------|
|
||||
| `chat` | 聊天/占卜业务 | `consume` |
|
||||
| `payment` | 支付业务 | `purchase`, `refund` |
|
||||
|
||||
Note: `register` and `adjust` do not bind to any `biz_type` (they are `null`).
|
||||
|
||||
## Table contract
|
||||
|
||||
### profiles
|
||||
@@ -62,21 +81,23 @@ Protocol verification status:
|
||||
- PK: `id`
|
||||
- FK:
|
||||
- `user_id -> auth.users.id` (`on delete cascade`)
|
||||
- `biz_id -> sessions.id` (`on delete restrict`, nullable)
|
||||
- `biz_id -> sessions.id` (`on delete restrict`, nullable) — only for `biz_type='chat'`
|
||||
- `operator_id -> auth.users.id` (`on delete set null`)
|
||||
- Core fields: `direction`, `amount`, `balance_after`, `change_type`, `biz_type`, `biz_id`, `event_id`, `operator_id`, `metadata`, `created_at`, `updated_at`
|
||||
- Constraints:
|
||||
- `amount > 0`
|
||||
- `direction in (1, -1)`
|
||||
- `balance_after >= 0`
|
||||
- `change_type in ('register', 'consume', 'grant', 'adjust')`
|
||||
- `biz_type is null or biz_type='chat'`
|
||||
- `change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')`
|
||||
- `biz_type is null or biz_type in ('chat', 'payment')`
|
||||
- biz binding:
|
||||
- `register => biz_type is null and biz_id is null`
|
||||
- `consume/grant/adjust => biz_type='chat' and biz_id not null`
|
||||
- `consume => biz_type='chat' and biz_id not null`
|
||||
- `adjust => biz_type is null and biz_id is null` (通用调整,不绑定业务场景)
|
||||
- `purchase/refund => biz_type='payment' and biz_id not null` (biz_id references `apple_iap_transactions.id` as logical FK, not database FK)
|
||||
- direction and change_type coupling:
|
||||
- `register/grant => direction = 1`
|
||||
- `consume => direction = -1`
|
||||
- `register/purchase => direction = 1`
|
||||
- `consume/refund => direction = -1`
|
||||
- `adjust => direction in (1, -1)`
|
||||
- idempotency: `unique (user_id, event_id)`
|
||||
|
||||
@@ -89,8 +110,8 @@ Protocol verification status:
|
||||
- `amount >= 0`
|
||||
- `direction in (1, 0, -1)`
|
||||
- `balance_after >= 0`
|
||||
- `change_type in ('register', 'consume', 'grant', 'adjust')`
|
||||
- `biz_type is null or biz_type='chat'`
|
||||
- `change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')`
|
||||
- `biz_type is null or biz_type in ('chat', 'payment')`
|
||||
- `billed_to in ('user', 'platform')`
|
||||
- metadata must be object
|
||||
- idempotency: `unique (event_id)`
|
||||
@@ -141,8 +162,9 @@ JSON constraints:
|
||||
- Per `change_type`:
|
||||
- `register`: no `charge`, and no chat binding (`biz_type/biz_id` both null)
|
||||
- `consume`: requires `charge` object with required fields
|
||||
- `grant`: no extra metadata shape requirement
|
||||
- `adjust`: requires `ext.ticket_id` non-empty
|
||||
- `adjust`: requires `ext.reason` non-empty (通用调整,系统或管理员均可操作,不绑定业务)
|
||||
- `purchase`: requires `ext.source`, `ext.platform`, `ext.product_code`, `ext.transaction_id`
|
||||
- `refund`: requires `ext.source`, `ext.platform`, `ext.product_code`, `ext.transaction_id`, `ext.original_event_id`
|
||||
|
||||
## Signup initialization contract
|
||||
|
||||
@@ -225,9 +247,9 @@ Returns available purchase packages for the current user's region, including sta
|
||||
"currency": "USD",
|
||||
"packages": [
|
||||
{
|
||||
"productCode": "new_user_pack_099_60",
|
||||
"productCode": "new_user_pack",
|
||||
"type": "starter",
|
||||
"priceUsd": "0.99",
|
||||
"price": "0.99",
|
||||
"credits": 60,
|
||||
"badge": null,
|
||||
"isStarter": true,
|
||||
@@ -235,9 +257,9 @@ Returns available purchase packages for the current user's region, including sta
|
||||
"sortOrder": 0
|
||||
},
|
||||
{
|
||||
"productCode": "basic_pack_499_100",
|
||||
"productCode": "basic_pack",
|
||||
"type": "regular",
|
||||
"priceUsd": "4.99",
|
||||
"price": "4.99",
|
||||
"credits": 100,
|
||||
"badge": null,
|
||||
"isStarter": false,
|
||||
@@ -252,9 +274,9 @@ Returns available purchase packages for the current user's region, including sta
|
||||
- `region`: ISO 3166-1 alpha-2 country code (e.g., "US", "CN")
|
||||
- `currency`: ISO 4217 currency code (e.g., "USD")
|
||||
- `packages`: List of available packages
|
||||
- `productCode`: Unique product identifier
|
||||
- `productCode`: Unique product identifier (e.g., `new_user_pack`, `basic_pack`, `popular_pack`, `premium_pack`)
|
||||
- `type`: "starter" (new user pack) or "regular"
|
||||
- `priceUsd`: Price in USD (decimal string)
|
||||
- `price`: Price in the response currency (decimal string, for display reference only; actual payment uses StoreKit price)
|
||||
- `credits`: Number of credits
|
||||
- `badge`: Optional badge text (e.g., "Popular")
|
||||
- `isStarter`: Whether this is a starter pack
|
||||
@@ -277,16 +299,16 @@ Returns available purchase packages for the current user's region, including sta
|
||||
region: US
|
||||
currency: USD
|
||||
packages:
|
||||
- product_code: new_user_pack_099_60
|
||||
- product_code: new_user_pack
|
||||
type: starter
|
||||
price_usd: "0.99"
|
||||
price: "0.99"
|
||||
credits: 60
|
||||
badge: null
|
||||
sort_order: 0
|
||||
enabled: true
|
||||
- product_code: basic_pack_499_100
|
||||
- product_code: basic_pack
|
||||
type: regular
|
||||
price_usd: "4.99"
|
||||
price: "4.99"
|
||||
credits: 100
|
||||
badge: null
|
||||
sort_order: 10
|
||||
|
||||
Reference in New Issue
Block a user