Files
eryao/docs/plans/ios-new-user-pack-payment-plan.md
T

248 lines
7.6 KiB
Markdown
Raw Normal View History

# 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 回调处理能力。