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

7.6 KiB
Raw Blame 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_20new_user_pack_099_60
  • claim_source VARCHAR(24) NOT NULLregister_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 下单与资格检查

客户端请求套餐 -> 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 验单与积分入账

客户端支付后提交 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 退款与冲正

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_orderspayment_order_events
  • 迁移:改造 register_bonus_claims
  • 模型与 repository

PR2(后端业务)

  • 支付路由 + service
  • iOS 验单适配层(先 mock
  • 订单与积分入账/冲正

PR3(前端)

  • 套餐改 API 驱动
  • 新人包购买态与禁用态
  • 下单/验单交互链路

PR4(联调与验证)

  • 使用集成测试回归全流程
  • Apple 账号就绪后切换真实 verifier

11. 变更类型判定

这是 新 Feature,不是现有功能的小修补。

理由:

  1. 引入了新的支付域模型和事件审计。
  2. 引入了新的后端支付路由与验单流程。
  3. 前端从静态展示升级为可交易流程。
  4. 增加了退款冲正与 iOS 回调处理能力。