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
+74 -16
View File
@@ -119,7 +119,7 @@
| `app_account_token` | UUID null | 客户端购买时传入的用户绑定 token,用于降低串单风险 |
| `purchase_date` | timestamptz not null | Apple 交易购买时间 |
| `revocation_date` | timestamptz null | Apple 撤销/退款时间 |
| `status` | varchar not null | `received``verified``granted``failed``refunded``revoked` |
| `status` | varchar not null | `received``verified``granted``failed``refunded``refunded_insufficient``revoked` |
| `credits` | bigint not null | 本次应发积分,由后端套餐配置决定 |
| `currency` | varchar null | Apple 或后端记录的币种 |
| `price_milliunits` | bigint null | 如 Apple JWS 提供则保存,单位按 Apple 字段定义 |
@@ -142,6 +142,7 @@
新增积分变更类型:
- `PointsChangeType.PURCHASE = 'purchase'`
- `PointsChangeType.REFUND = 'refund'`
新增业务类型:
@@ -149,12 +150,14 @@
约束调整:
- `change_type` 允许 `purchase`
- `change_type` 允许 `purchase``refund`
- `biz_type` 允许 `payment`
- `purchase` 必须 `direction = 1`
- `purchase` 必须 `biz_type = 'payment'``biz_id` 不为空
- `purchase` `biz_id` 保存 `apple_iap_transactions.id`
- `refund` 必须 `direction = -1`
- `purchase/refund` 必须 `biz_type = 'payment'` `biz_id` 不为空
- `purchase/refund``biz_id` 保存 `apple_iap_transactions.id`
- `purchase``event_id` 建议为 `payment.apple_iap:{transaction_id}`
- `refund``event_id` 建议为 `refund.apple_iap:{transaction_id}`
`points_ledger.metadata.ext` 建议保存:
@@ -171,6 +174,16 @@
}
```
对于 `refund`,额外保存:
```json
{
"original_event_id": "payment.apple_iap:1000000123456789",
"refund_reason": "CUSTOMER_REQUEST",
"overdue_amount": 0
}
```
### 5.5 `points_audit_ledger` 是否扩展
本期不强制把购买写入 `points_audit_ledger`。支付审计以 `apple_iap_transactions` 为准,用户积分变更以 `points_ledger` 为准。
@@ -304,12 +317,46 @@
### 7.4 退款与撤销
本期至少保存 `revocationDate` 和交易状态,不自动扣回已发放积分
当收到 Apple 退款/撤销通知时,必须扣回已发放积分,避免积分被白嫖
后续建议接入 App Store Server Notifications V2
**处理流程:**
- 收到退款/撤销通知后按 `transactionId` 更新 `apple_iap_transactions.status`
- 是否扣回积分需要产品另行决策。考虑积分可能已消耗,直接扣回可能导致负余额或复杂追偿,本期不默认实现
1. 后台任务或 App Store Server Notifications V2 收到退款/撤销事件
2.`transactionId` 查询 `apple_iap_transactions`
3. 如果 `status != 'granted'`,忽略(未发放无需扣回)。
4. 开启数据库事务:
- 锁定 `user_points` 行。
- 如果 `balance < credits`(余额不足以扣回):
- 设置 `apple_iap_transactions.status = 'refunded_insufficient'`
- 记录 `failure_code = 'INSUFFICIENT_BALANCE'`
- 写入 `points_ledger``refund` 流水,`amount = balance`(扣到 0 为止)。
- 事务提交后触发告警,等待人工处理。
- 如果 `balance >= credits`
- 扣减 `user_points.balance``lifetime_earned`
- 写入 `points_ledger``refund` 流水。
- 更新 `apple_iap_transactions.status = 'refunded'`
- 事务提交。
5. `points_ledger``refund` 记录:
- `change_type = 'refund'`
- `direction = -1`
- `biz_type = 'payment'`
- `biz_id = apple_iap_transactions.id`
- `event_id = refund.apple_iap:{transaction_id}`
- `metadata.ext` 保存退款相关快照。
**App Store Server Notifications V2 接入(建议):**
- 配置 App Store Connect 的 Server Notifications URL。
- 后端实现 `POST /api/v1/payments/apple/notifications` 接收 Apple 推送。
- 解析 notification type`REFUND``REVOKE` 等。
- 按上述流程处理退款/撤销。
**余额不足时的处理策略:**
- 不允许用户余额变为负数。
- 扣到 0 为止,剩余欠款记录在 `apple_iap_transactions.metadata.overdue_amount`
- 触发运营告警,人工决定是否追偿或标记坏账。
- 后续用户充值时,可考虑优先补扣欠款(需要产品决策,本期不实现)。
## 8. iOS / Flutter 接入设计
@@ -415,6 +462,9 @@ apps/lib/features/payments/
- 新手包第一次购买成功,第二次返回 `PAYMENT_STARTER_PACK_INELIGIBLE`
- 入账后 `user_points.balance/lifetime_earned` 正确增加。
- 入账后 `points_ledger` 写入 `purchase/payment`
- 退款时余额充足:正确扣减积分,写入 `refund` 流水,状态变为 `refunded`
- 退款时余额不足:扣到 0,状态变为 `refunded_insufficient`,触发告警路径。
- 重复退款通知:幂等处理,不重复扣减。
### 12.2 后端集成测试
@@ -448,11 +498,15 @@ apps/lib/features/payments/
### Phase 2:后端支付服务
1. 新增 `v1/payments/` 模块
2. 实现 Apple transaction verifier 抽象
3. 实现支付校验、幂等和积分入账服务
4. 接入 `v1/router.py`
5. 添加单元测试和集成测试
1. 更新协议文档和错误码
2. 新增 `apple_iap_transactions` 模型与 Alembic 迁移
3. 扩展 `PointsChangeType``PointsBizType``points_ledger` check constraints
4. 新增 `v1/payments/` 模块
5. 实现 Apple transaction verifier 抽象
6. 实现支付校验、幂等和积分入账服务。
7. 实现退款/撤销处理服务(扣回积分)。
8. 接入 `v1/router.py`
9. 添加单元测试和集成测试。
### Phase 3iOS / Flutter
@@ -466,8 +520,10 @@ apps/lib/features/payments/
1. 配置 App Store Connect 商品。
2. 配置后端 Apple 验签参数。
3. Sandbox 和 TestFlight 测试
4. 检查日志、错误码、补偿路径
3. 配置 App Store Server Notifications V2 URL
4. Sandbox 和 TestFlight 测试
5. 测试退款/撤销流程(使用 Sandbox 退款功能)。
6. 检查日志、错误码、补偿路径。
## 14. 风险与缓解
@@ -478,7 +534,8 @@ apps/lib/features/payments/
| 商品配置不一致 | 后端维护 `productCode -> App Store Product ID` 映射并强校验 |
| 新手包被重复购买 | 后端事务内锁定/更新 `register_bonus_claims` |
| Apple 服务临时不可用 | 返回 `PAYMENT_APPLE_UNAVAILABLE`,前端保留交易稍后重试 |
| 退款后积分已消耗 | 本期只记录退款状态,不自动扣回;后续单独设计追偿策略 |
| 退款时余额不足扣回 | 扣到 0 为止,记录欠款,触发告警等待人工处理 |
| 退款通知丢失或延迟 | 后台定时任务查询 Apple Server API 获取交易状态变更 |
| Flutter 插件无法暴露 StoreKit2 signedData | 增加最小 iOS platform channel 获取 `Transaction.signedData` |
## 15. 成功标准
@@ -488,4 +545,5 @@ apps/lib/features/payments/
- 新手包同一邮箱身份只能发放一次。
- 支付入账后 `user_points``points_ledger``apple_iap_transactions` 数据一致。
- 网络中断/重复提交不会导致漏发或重复发放。
- 退款/撤销时正确扣回积分,余额不足时正确记录并告警。
- 新增/修改的协议文档、错误码、迁移、后端测试、前端测试均完成。