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