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:
@@ -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 3:iOS / 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` 数据一致。
|
||||
- 网络中断/重复提交不会导致漏发或重复发放。
|
||||
- 退款/撤销时正确扣回积分,余额不足时正确记录并告警。
|
||||
- 新增/修改的协议文档、错误码、迁移、后端测试、前端测试均完成。
|
||||
|
||||
Reference in New Issue
Block a user