Files
eryao/.trellis/tasks/archive/2026-04/04-27-feat-ios-apple-pay/IMPLEMENTATION_PLAN.md
T

341 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,下次启动自动重试
- 不可重试错误(4xxcomplete 并暴露 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`,前端保留交易 |