248 lines
7.6 KiB
Markdown
248 lines
7.6 KiB
Markdown
# iOS 新人包支付接入与一次性权益计划
|
||
|
||
## 1. 背景与目标
|
||
|
||
当前前端充值页为静态套餐展示,购买按钮未接入真实支付链路。现需新增 iOS 新人包:
|
||
|
||
- 价格:`$0.99`
|
||
- 积分:`60`
|
||
- 资格:同邮箱只能购买一次
|
||
- 删除账号后同邮箱重新注册,不刷新新人包资格
|
||
|
||
同时补齐后端真实支付路由与订单审计能力,前端不再硬编码套餐。
|
||
|
||
## 2. 本次范围
|
||
|
||
### 2.1 In Scope
|
||
|
||
1. 后端新增 iOS 支付相关路由(下单/验单/查询/回调)。
|
||
2. 新建支付订单主表与支付事件审计表。
|
||
3. 改造 `register_bonus_claims` 为可承载“权益唯一占用”能力。
|
||
4. 前端套餐由后端接口驱动,不再硬编码三档固定套餐。
|
||
5. 新人包资格前后端联动(展示、购买、验单、入账)。
|
||
|
||
### 2.2 Out of Scope
|
||
|
||
1. Android 支付渠道接入。
|
||
2. Apple 开发者账号正式联调(当前账号未就绪)。
|
||
3. 财务对账后台页面。
|
||
|
||
## 3. 数据模型设计
|
||
|
||
## 3.1 新建表:`payment_orders`
|
||
|
||
用途:订单当前态,支持幂等验单与退款状态跟踪。
|
||
|
||
建议字段:
|
||
|
||
- `id` UUID PK
|
||
- `order_no` VARCHAR(64) UNIQUE
|
||
- `user_id` UUID NOT NULL (`auth.users.id`)
|
||
- `channel` VARCHAR(16) NOT NULL (`ios_iap`)
|
||
- `product_code` VARCHAR(64) NOT NULL(例:`new_user_pack_099_60`)
|
||
- `price_usd` NUMERIC(12,6) NOT NULL
|
||
- `credits` BIGINT NOT NULL
|
||
- `currency` VARCHAR(8) NOT NULL DEFAULT `USD`
|
||
- `status` VARCHAR(24) NOT NULL
|
||
- `created|receipt_submitted|verified|credited|refund_pending|refunded|revoked|failed`
|
||
- `apple_transaction_id` VARCHAR(128) NULL UNIQUE
|
||
- `apple_original_transaction_id` VARCHAR(128) NULL
|
||
- `app_account_token` UUID NULL
|
||
- `idempotency_key` VARCHAR(128) NULL UNIQUE
|
||
- `error_code` VARCHAR(64) NULL
|
||
- `error_message` TEXT NULL
|
||
- `created_at` / `updated_at`
|
||
|
||
关键约束:
|
||
|
||
- `credits > 0`
|
||
- `price_usd >= 0`
|
||
- `status` check
|
||
- `channel='ios_iap'`(本期)
|
||
|
||
## 3.2 新建表:`payment_order_events`
|
||
|
||
用途:支付事件不可变审计流水(验单结果、回调、退款、冲正)。
|
||
|
||
建议字段:
|
||
|
||
- `id` UUID PK
|
||
- `order_id` UUID NOT NULL FK `payment_orders.id`
|
||
- `event_type` VARCHAR(32) NOT NULL
|
||
- `order_created|receipt_submitted|verify_success|verify_failed|credited|refund_notified|refunded|revoke_notified|reversed`
|
||
- `event_source` VARCHAR(24) NOT NULL
|
||
- `api|apple_server_notification|job`
|
||
- `event_idempotency_key` VARCHAR(128) NULL UNIQUE
|
||
- `payload` JSONB NOT NULL
|
||
- `operator_id` UUID NULL
|
||
- `created_at`
|
||
|
||
## 3.3 改造表:`register_bonus_claims`
|
||
|
||
目标:从“注册送分去重”升级为“权益唯一占用”。
|
||
|
||
新增字段建议:
|
||
|
||
- `offer_code` VARCHAR(64) NOT NULL(例:`register_bonus_20`、`new_user_pack_099_60`)
|
||
- `claim_source` VARCHAR(24) NOT NULL(`register_bonus|ios_purchase`)
|
||
- `claim_order_id` UUID NULL FK `payment_orders.id`
|
||
|
||
新增唯一约束:
|
||
|
||
- `UNIQUE(offer_code, email_hash)`
|
||
|
||
保留行为:
|
||
|
||
- `first_user_id` 允许 `ON DELETE SET NULL`,保证删号后资格仍占用。
|
||
|
||
## 4. 路由与服务边界
|
||
|
||
## 4.1 后端新增路由(v1)
|
||
|
||
1. `GET /api/v1/payments/packages`
|
||
- 返回可购买套餐列表与用户资格(是否可买新人包)。
|
||
2. `POST /api/v1/payments/orders`
|
||
- 创建订单,返回 `orderNo` 与客户端支付所需参数。
|
||
3. `POST /api/v1/payments/orders/{orderNo}/verify-ios-receipt`
|
||
- 提交 iOS 收据,后端调用 Apple 校验。
|
||
4. `GET /api/v1/payments/orders/{orderNo}`
|
||
- 查询订单状态与入账结果。
|
||
5. `POST /api/v1/payments/webhooks/apple`
|
||
- 接收 App Store Server Notifications V2,处理退款/撤销。
|
||
|
||
## 4.2 分层职责
|
||
|
||
- Router:鉴权、请求校验、RFC7807 错误映射。
|
||
- Service:
|
||
- 资格判断(新人包是否可买)
|
||
- 下单与验单业务编排
|
||
- 入账积分与冲正
|
||
- 幂等控制
|
||
- Repository:
|
||
- `payment_orders`/`payment_order_events`/`register_bonus_claims` 读写
|
||
- 订单状态流转条件更新
|
||
|
||
## 5. 核心流程
|
||
|
||
## 5.1 下单与资格检查
|
||
|
||
```text
|
||
客户端请求套餐 -> GET /payments/packages
|
||
-> 后端按 email_hash 检查 offer_code='new_user_pack_099_60' 是否已占用
|
||
-> 返回 eligible=true/false
|
||
|
||
客户端创建订单 -> POST /payments/orders
|
||
-> 再次做资格校验(防并发)
|
||
-> 创建 payment_orders(status=created)
|
||
-> 写 payment_order_events(order_created)
|
||
```
|
||
|
||
## 5.2 iOS 验单与积分入账
|
||
|
||
```text
|
||
客户端支付后提交 receipt -> POST /orders/{orderNo}/verify-ios-receipt
|
||
-> 后端调用 Apple 验单(可切 sandbox)
|
||
-> 验证 transaction_id 幂等
|
||
-> 状态 verified
|
||
-> 原子事务:
|
||
1) 占用权益 register_bonus_claims(offer_code,email_hash)
|
||
2) 写 points_ledger(grant)
|
||
3) 写 points_audit_ledger(direction=1,billed_to='user')
|
||
4) 订单置 credited
|
||
5) 写 payment_order_events(credited)
|
||
```
|
||
|
||
## 5.3 退款与冲正
|
||
|
||
```text
|
||
Apple 回调退款 -> POST /payments/webhooks/apple
|
||
-> 定位 order(transaction_id / original_transaction_id)
|
||
-> 幂等处理通知
|
||
-> 状态 refunded/revoked
|
||
-> 原子事务:
|
||
1) 写 points_ledger(adjust/consume reverse)
|
||
2) 写 points_audit_ledger(direction=-1,billed_to='platform',metadata.reason='refund')
|
||
3) 写 payment_order_events(refunded/reversed)
|
||
```
|
||
|
||
## 6. 信任边界与风控
|
||
|
||
1. 客户端价格、积分、product_code 全部不可信,按后端配置为准。
|
||
2. 不信任客户端“支付成功”标记,必须后端验单通过才入账。
|
||
3. Apple 回调需验签(JWS)并做 `notificationUUID` 幂等。
|
||
4. 订单与入账使用数据库事务,失败不允许半成功。
|
||
5. `offer_code + email_hash` 唯一约束是最终防线。
|
||
|
||
## 7. 前端改造
|
||
|
||
当前 `CoinCenterScreen` 中套餐硬编码,需改为 API 驱动:
|
||
|
||
- 页面加载调用 `GET /api/v1/payments/packages`
|
||
- 渲染返回的套餐列表
|
||
- 新人包 `eligible=false` 时展示“已购买/不可购买”态
|
||
- 点击购买后走真实支付流(创建订单 -> 拉起 IAP -> 提交 receipt)
|
||
|
||
## 8. 无 Apple 账号阶段的交付策略
|
||
|
||
在无开发者账号前,先做可替换的验单适配层:
|
||
|
||
- `IOSReceiptVerifier` 接口(生产实现 + mock 实现)
|
||
- 通过配置开关使用 mock 结果跑通后端链路与前端状态
|
||
- 后续只替换 verifier 实现,不改订单主流程
|
||
|
||
## 9. 测试计划
|
||
|
||
## 9.1 后端单元测试
|
||
|
||
1. 新人包资格判定(首次可买、重复不可买、删号重注册不可买)
|
||
2. 验单幂等(同 transaction_id 不重复入账)
|
||
3. 退款冲正幂等(同通知不重复冲正)
|
||
|
||
## 9.2 后端集成测试
|
||
|
||
1. 首次注册 -> 下单 -> 验单 -> 入账 60
|
||
2. 删除账号 -> 同邮箱重注册 -> 新人包不可买
|
||
3. 退款通知 -> 积分冲正 -> 订单状态更新
|
||
|
||
## 9.3 前端集成测试
|
||
|
||
1. 套餐接口渲染(替代硬编码)
|
||
2. 新人包可买/不可买状态切换
|
||
3. 支付中/成功/失败/退款状态展示
|
||
|
||
## 10. 里程碑拆分
|
||
|
||
### PR1(数据层)
|
||
|
||
- 迁移:新建 `payment_orders`、`payment_order_events`
|
||
- 迁移:改造 `register_bonus_claims`
|
||
- 模型与 repository
|
||
|
||
### PR2(后端业务)
|
||
|
||
- 支付路由 + service
|
||
- iOS 验单适配层(先 mock)
|
||
- 订单与积分入账/冲正
|
||
|
||
### PR3(前端)
|
||
|
||
- 套餐改 API 驱动
|
||
- 新人包购买态与禁用态
|
||
- 下单/验单交互链路
|
||
|
||
### PR4(联调与验证)
|
||
|
||
- 使用集成测试回归全流程
|
||
- Apple 账号就绪后切换真实 verifier
|
||
|
||
## 11. 变更类型判定
|
||
|
||
这是 **新 Feature**,不是现有功能的小修补。
|
||
|
||
理由:
|
||
|
||
1. 引入了新的支付域模型和事件审计。
|
||
2. 引入了新的后端支付路由与验单流程。
|
||
3. 前端从静态展示升级为可交易流程。
|
||
4. 增加了退款冲正与 iOS 回调处理能力。
|