# iOS Apple Pay 落实计划 ## 当前状态 ### 已完成 - [x] 协议文档更新(`user-points-chat-data-protocol.md`、`http-error-codes.md`) - [x] PRD 退款扣回策略已明确 - [x] 套餐配置 YAML 已正确命名(`new_user_pack`, `starter_pack` 等) - [x] **Phase 1: 数据库与枚举**(2026-04-27 完成) - [x] **Phase 2: 后端支付服务**(2026-04-27 完成) - [x] **Phase 3: iOS / Flutter IAP 接入**(2026-04-27 完成) ### 待实现 - [ ] 联调与发布准备(Phase 4) --- ## Phase 1: 数据库与枚举(后端基础)✅ 已完成 ### 1.1 枚举扩展 ✅ **文件**: `backend/src/schemas/enums.py` - [x] `PointsChangeType` 新增 `PURCHASE`, `REFUND`,移除 `GRANT` - [x] `PointsBizType` 新增 `PAYMENT` **文件**: `backend/src/schemas/domain/points.py` - [x] 更新 `ApplyPointsChangeCommand` 验证逻辑,支持 `purchase/refund` - [x] 移除 `GrantLedgerMetadata` ### 1.2 数据库迁移 ✅ **迁移文件**: `backend/alembic/versions/20260427_0001_apple_iap_transactions.py` - [x] 创建 `apple_iap_transactions` 表(按 PRD 5.3 定义) - [x] 更新 `points_ledger` check constraints: - `change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')` - `biz_type is null or biz_type in ('chat', 'payment')` - 更新 `ck_points_ledger_biz_binding` 支持 `purchase/refund` - 更新 `ck_points_ledger_direction_by_change_type` 支持 `purchase(direction=1)` 和 `refund(direction=-1)` - 新增 `ck_points_ledger_metadata_payment_shape` 和 `ck_points_ledger_metadata_refund_shape` - 更新 `ck_points_ledger_metadata_adjust_shape`(`ticket_id` -> `reason`) - [x] 更新 `points_audit_ledger` check constraints 同步变更 **模型文件**: - [x] `backend/src/models/points_ledger.py` - 同步更新 SQLAlchemy CheckConstraint - [x] `backend/src/models/apple_iap_transaction.py` - 新建模型 - [x] `backend/src/models/__init__.py` - 导出新模型 ### 1.3 验证 ✅ - [x] 迁移已应用到数据库 - [x] `apple_iap_transactions` 表已创建 - [x] `points_ledger` 约束已更新 --- ## Phase 2: 后端支付服务 ✅ 已完成 ### 2.1 配置扩展 ✅ - [x] `backend/src/core/config/settings.py` - 新增 `AppleIapSettings` 和 `apple_iap` 配置项 - [x] `backend/src/core/config/static/packages/mapping.yaml` - productCode -> App Store Product ID 映射 ### 2.2 API Schemas ✅ - [x] `backend/src/v1/payments/schemas.py` - `VerifyTransactionRequest` / `VerifyTransactionResponse` - [x] `backend/src/schemas/domain/points.py` - 新增 `PurchaseLedgerMetadata` ### 2.3 Apple JWS 验签器 ✅ - [x] `backend/src/v1/payments/apple_verifier.py` - JWS x5c 证书链验证(root fingerprint + issuer/subject chain) - bundleId / productId / environment / revocationDate 验证 - 返回 `VerifiedTransaction | VerificationError` ### 2.4 支付数据仓库 ✅ - [x] `backend/src/v1/payments/repository.py` - `PaymentRepository` - `get_or_create_user_points_for_update` - `get_user_points_for_update` (for refund, no auto-create) - `get_transaction_by_transaction_id` - `insert_transaction` - `get_register_bonus_claim` - `upsert_register_bonus_claim_for_starter_pack` ### 2.5 支付服务 ✅ - [x] `backend/src/v1/payments/service.py` - `PaymentService` - `verify_and_grant`: productCode / appStoreProductId 校验, Apple JWS 验签, transaction_id 幂等, 新手包资格检查, 积分入账 + points_ledger + register_bonus_claims - `process_refund`: 退款扣回积分, 余额不足时扣到 0 并标记 `refunded_insufficient`, 幂等处理 - `handle_server_notification`: 解析 Apple Server Notifications V2, REFUND/REVOKE 触发退款, DID_RENEW 记录日志 ### 2.6 API 路由 + 依赖注入 ✅ - [x] `backend/src/v1/payments/router.py` - `POST /api/v1/payments/apple/transactions/verify` - `POST /api/v1/payments/apple/notifications` - [x] `backend/src/v1/payments/dependencies.py` - DI wiring - [x] `backend/src/v1/router.py` - 注册 payments_router ### 2.7 测试 ✅ - [x] `backend/tests/unit/payments/test_payment_service.py` - 16 个测试全部通过 - 验证流程: product_not_found / product_mismatch / verification_failed / already_granted / transaction_conflict / successful_grant / starter_pack_ineligible / starter_pack_success - 退款流程: refund_unknown / refund_not_granted / refund_sufficient_balance / refund_insufficient_balance / refund_idempotency - 通知处理: notification_refund / notification_empty / notification_non_refund - [x] `backend/tests/unit/payments/__init__.py` - verifier 基础测试 - [x] `backend/tests/integration/payments/test_verify_flow.py` - 集成测试骨架 - [x] basedpyright 类型检查通过(0 errors) - [x] 所有模块 import 正常 ### 未实现(后续迭代) - `GET /api/v1/payments/apple/transactions/{transactionId}` 查询接口 - `apple_client.py` Apple Server API 主动查询客户端(可选) ### 2.2 核心实现 #### 2.2.1 Apple JWS 验签器 **文件**: `backend/src/v1/payments/apple_verifier.py` 职责: - 下载/缓存 Apple 根证书链 - 验证 JWS 签名 - 解析 payload 并验证字段:`bundleId`, `productId`, `transactionId`, `environment`, `revocationDate` - 返回结构化验证结果 #### 2.2.2 支付服务 **文件**: `backend/src/v1/payments/service.py` 核心方法: - `verify_and_grant(user_id, request) -> VerifyResponse` - 1. 验证 productCode 存在且启用 - 2. 验证 appStoreProductId 与映射匹配 - 3. 调用 Apple verifier 验签 - 4. 检查 transaction 幂等(已发放返回 `already_granted`) - 5. 检查新手包资格 - 6. 事务:创建/更新 `apple_iap_transactions` + 更新 `user_points` + 写入 `points_ledger` + 更新 `register_bonus_claims` - `process_refund(transaction_id) -> None` - 1. 查询 `apple_iap_transactions` - 2. 事务:扣减积分 + 写入 `refund` 流水 + 更新状态 - 3. 余额不足时设置 `refunded_insufficient` 并告警 #### 2.2.3 API 接口 **文件**: `backend/src/v1/payments/router.py` ``` POST /api/v1/payments/apple/transactions/verify POST /api/v1/payments/apple/notifications # App Store Server Notifications V2 GET /api/v1/payments/apple/transactions/{transactionId} # 可选 ``` ### 2.3 配置扩展 **文件**: `backend/src/core/config/settings.py` 新增配置项: ```python apple_iap: AppleIapSettings class AppleIapSettings: bundle_id: str root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer" jws_issuer_id: str | None = None # Server API (可选) jws_key_id: str | None = None jws_private_key: str | None = None ``` **文件**: `backend/src/core/config/static/packages/mapping.yaml` (新建) ```yaml product_mappings: new_user_pack: app_store_product_id: com.meeyao.qianwen.new_user_pack credits: 60 type: starter starter_pack: app_store_product_id: com.meeyao.qianwen.starter_pack credits: 100 type: regular popular_pack: app_store_product_id: com.meeyao.qianwen.popular_pack credits: 210 type: regular premium_pack: app_store_product_id: com.meeyao.qianwen.premium_pack credits: 415 type: regular ``` ### 2.4 测试 **单元测试**: - `backend/tests/unit/payments/test_apple_verifier.py` - `backend/tests/unit/payments/test_payment_service.py` **集成测试**: - `backend/tests/integration/payments/test_verify_flow.py` --- ## Phase 3: iOS / Flutter 接入 ✅ 已完成 ### 3.1 依赖添加 ✅ - [x] `apps/pubspec.yaml` — `in_app_purchase: ^3.2.3` + `in_app_purchase_storekit: ^0.4.8` + `crypto: ^3.0.7` ### 3.2 后端配合变更 ✅ - [x] `backend/src/v1/points/schemas.py` — `PackageInfo` 新增 `appStoreProductId` 字段 - [x] `backend/src/v1/points/service.py` — `PackageInfoResult` 新增 `app_store_product_id`,`get_available_packages` 从 mapping.yaml 加载映射 - [x] `backend/src/v1/points/router.py` — 响应中包含 `appStoreProductId` ### 3.3 前端目录结构 ✅ ``` apps/lib/features/payments/ ├── data/ │ ├── apis/ │ │ └── apple_payment_api.dart # 后端 verify 接口 │ ├── models/ │ │ └── apple_purchase_models.dart # VerifyTransactionRequest/Response │ └── services/ │ └── apple_iap_service.dart # StoreKit 集成服务 ``` ### 3.4 核心实现 ✅ - [x] `apple_purchase_models.dart` — VerifyTransactionRequest(含 appAccountToken)/ VerifyTransactionResponse(含 newBalance/ledgerEventId) - [x] `apple_payment_api.dart` — POST /api/v1/payments/apple/transactions/verify - [x] `apple_iap_service.dart` — AppleIapService (ChangeNotifier): - 初始化 purchaseStream 监听 - queryProductDetails 查询 StoreKit 商品 - `buyConsumable` 传递 `applicationUserName`(appAccountToken = userId MD5 hash) - 购买成功 → 发送 JWS + appAccountToken 到后端验证 → completePurchase - 可重试错误(5xx/网络)不 complete,下次启动自动重试 - 不可重试错误(4xx)complete 并暴露 ApiProblem 供 UI 映射 l10n - 暴露 `lastApiProblem` 供错误码映射 - [x] `package_info.dart` — 新增 `appStoreProductId` 字段 - [x] `settings_section_widgets.dart` — CoinPackageCard 新增 `onPurchase`/`isPurchasing`/`isAvailable`/`unavailableMessage` - [x] `coin_center_screen.dart` — 集成 AppleIapService: - 接收 `userId`/`onBalanceChanged` 参数 - StoreKit 价格覆盖后端参考价格 - 商品不可用时禁用卡片并显示提示 - pending 状态显示 "Apple 正在处理中" - 购买成功后调用 `_refreshBalance` 并回调 `onBalanceChanged` - 使用 `mapApiProblemToMessage` 映射错误码到 l10n ### 3.5 调用链更新 ✅ - [x] `app.dart` — HomeScreen 传递 `userId` + `onBalanceChanged` - [x] `home_screen.dart` — `_ProfileTab` 传递 `userId` + `onBalanceChanged` - [x] `settings_screen.dart` — 传递 `userId` + `onBalanceChanged` 给 CoinCenterScreen ### 3.6 错误处理与本地化 ✅ - [x] `api_problem_mapper.dart` — 6 个支付错误码映射 - [x] 3 个 ARB 文件新增 7 个 l10n key: - `paymentSuccess` / `paymentVerifyFailed` / `paymentProductNotFound` / `paymentStarterPackIneligible` - `paymentProductUnavailable` / `paymentPending` - [x] `flutter gen-l10n` 生成通过 ### 3.7 前端测试 ✅ - [x] `apps/test/features/payments/data/models/apple_purchase_models_test.dart` - VerifyTransactionRequest.toJson 含/不含 appAccountToken - VerifyTransactionResponse 解析 granted / already_granted - [x] 4 个测试全部通过 ### 3.8 验证 ✅ - [x] `flutter analyze` 0 issues - [x] 后端 basedpyright 0 errors - [x] 后端 16 个单元测试全部通过 --- ## Phase 4: 联调与发布准备 ### 4.1 App Store Connect 配置 - [x] 创建 4 个消耗型 IAP 商品(Product ID 已确认与映射表一致) - [x] Product ID 与映射表一致 - `com.meeyao.qianwen.new_user_pack` — 新手包 - `com.meeyao.qianwen.starter_pack` — 入门包 - `com.meeyao.qianwen.popular_pack` — 热门包 - `com.meeyao.qianwen.premium_pack` — 高级包 - [ ] 配置价格和描述 - [x] 创建沙盒测试账号:`qiuzhiliang@xunmee.com` - [ ] 配置 Server Notifications V2 URL(生产环境公网 URL) ### 4.2 后端配置 ✅ - [x] 环境变量:`APPLE_IAP_BUNDLE_ID=com.meeyao.qianwen` - [x] Server API 密钥配置(`T6M7J28MAQ` / `862a2cd0-ad6e-47c8-ac5a-bef5676c470b`) - [x] 日志检查:不打印完整 JWS - [x] 环境判断:根据 `ERYAO_RUNTIME__ENVIRONMENT` 自动切换 Sandbox/Production ### 4.3 开发环境准备 ✅ - [x] `.env.example` 更新 Apple IAP 配置模板 - [x] `.env` 写入实际配置值 - [x] `AppleIapSettings` 支持所有配置字段 - [x] `EryaoProducts.storekit` Xcode 本地测试配置文件 ### 4.4 测试清单 - [ ] Xcode StoreKit Configuration 本地测试 - [ ] Sandbox 购买成功验证 - [ ] Sandbox 退款测试 - [ ] 网络中断后重启恢复 - [ ] 新手包重复购买阻断 - [ ] TestFlight 环境验证 --- ## 预估工时 | Phase | 工作项 | 预估时间 | |-------|--------|----------| | 1 | 枚举扩展 + 数据库迁移 | 2-3h | | 2 | 后端支付服务 + 测试 | 1-2 天 | | 3 | Flutter IAP 接入 | 1 天 | | 4 | 联调与发布准备 | 0.5-1 天 | **总计**: 3-4 天 --- ## 风险与依赖 | 风险/依赖 | 缓解措施 | |----------|---------| | App Store Connect 配置权限 | 提前确认账号权限,Phase 1 同步申请 | | `in_app_purchase` 插件无法暴露 signedData | 预研后确定是否需要 platform channel | | Apple 服务不可用 | 返回 `PAYMENT_APPLE_UNAVAILABLE`,前端保留交易 |