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:
ZL-Q
2026-04-28 10:45:29 +08:00
parent b453ff7345
commit 87f92987b2
58 changed files with 3741 additions and 336 deletions
@@ -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