feat(payment): 优化套餐配置和支付服务

- 简化套餐配置结构,删除冗余的 default.yaml 和 us.yaml
- 优化 Apple IAP 服务和验证逻辑
- 更新套餐数据模型和协议文档
- 添加支付相关测试用例
This commit is contained in:
ZL-Q
2026-04-28 17:21:14 +08:00
parent a940f2ea47
commit 295dbc09ab
23 changed files with 285 additions and 304 deletions
@@ -19,6 +19,7 @@ This document is the source of truth for backend RFC7807 `code` values consumed
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `POINTS_INSUFFICIENT_BALANCE` | 402 | Not enough points to start this run | Show recharge/insufficient-points prompt |
| `POINTS_INVALID_CURSOR` | 422 | Points ledger pagination cursor is not a valid ISO 8601 datetime | Show invalid-request message and reload from first page |
## Agent Session
@@ -82,6 +83,7 @@ This document is the source of truth for backend RFC7807 `code` values consumed
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `NOTIFICATION_NOT_FOUND` | 404 | Notification not found or not owned by current user | Show not-found message and refresh list |
| `NOTIFICATION_INVALID_CURSOR` | 422 | Notification pagination cursor is not a valid ISO 8601 datetime | Show invalid-request message and reload from first page |
## Invite
@@ -230,6 +230,49 @@ Managed by `python -m core.runtime.cli sync-notifications [flags]`:
Run after migrations on fresh environments or after adding new notification YAML definitions. Not included in `bootstrap` to keep bootstrap fast and free of unintended side effects.
## Points Ledger API
### GET /api/v1/points/ledger
Returns the authenticated user's points ledger in reverse chronological order.
**Request:**
- Auth: Required (JWT)
- Query:
- `limit`: integer, `1..100`, default `20`
- `cursor`: optional ISO 8601 datetime returned by the previous response `nextCursor`
**Response:**
```json
{
"items": [
{
"id": "9cfd5d1d-0dd8-4b30-88ce-6e4a63d22d76",
"direction": 1,
"amount": 60,
"balanceAfter": 160,
"changeType": "purchase",
"createdAt": "2026-04-28T08:30:00+00:00"
}
],
"nextCursor": "2026-04-28T08:30:00+00:00",
"hasMore": true
}
```
**Fields:**
- `items`: ledger rows ordered by `createdAt desc`
- `direction`: `1` for income, `-1` for spending/deduction
- `amount`: positive points delta
- `balanceAfter`: account balance after the ledger event
- `changeType`: one of `register`, `purchase`, `consume`, `adjust`, `refund`
- `createdAt`: ISO 8601 datetime for display and pagination
- `nextCursor`: last returned row `createdAt` when `hasMore=true`; otherwise `null`
- `hasMore`: whether another page is available
**Errors:**
- `POINTS_INVALID_CURSOR` (`422`): `cursor` is not a valid ISO 8601 datetime
## Packages API
### GET /api/v1/points/packages
@@ -243,25 +286,21 @@ Returns available purchase packages for the current user's region, including sta
**Response:**
```json
{
"region": "US",
"currency": "USD",
"packages": [
{
"productCode": "new_user_pack",
"appStoreProductId": "com.meeyao.qianwen.new_user_pack",
"type": "starter",
"price": "0.99",
"credits": 60,
"badge": null,
"isStarter": true,
"starterEligible": true,
"sortOrder": 0
},
{
"productCode": "basic_pack",
"productCode": "starter_pack",
"appStoreProductId": "com.meeyao.qianwen.starter_pack",
"type": "regular",
"price": "4.99",
"credits": 100,
"badge": null,
"isStarter": false,
"starterEligible": false,
"sortOrder": 10
@@ -271,51 +310,38 @@ Returns available purchase packages for the current user's region, including sta
```
**Fields:**
- `region`: ISO 3166-1 alpha-2 country code (e.g., "US", "CN")
- `currency`: ISO 4217 currency code (e.g., "USD")
- `packages`: List of available packages
- `productCode`: Unique product identifier (e.g., `new_user_pack`, `basic_pack`, `popular_pack`, `premium_pack`)
- `type`: "starter" (new user pack) or "regular"
- `price`: Price in the response currency (decimal string, for display reference only; actual payment uses StoreKit price)
- `credits`: Number of credits
- `badge`: Optional badge text (e.g., "Popular")
- `isStarter`: Whether this is a starter pack
- `starterEligible`: Whether user is eligible to purchase starter pack
- `sortOrder`: Display order (ascending)
- `productCode`: Unique product identifier (e.g., `new_user_pack`, `starter_pack`, `popular_pack`, `premium_pack`)
- `appStoreProductId`: Apple App Store product identifier used for StoreKit purchase
- `type`: "starter" (new user pack) or "regular"
- `credits`: Number of credits
- `isStarter`: Whether this is a starter pack
- `starterEligible`: Whether user is eligible to purchase starter pack
- `sortOrder`: Display order (ascending)
**Business Logic:**
1. Determine user's region from `profile.settings.preferences.country` (default: "US")
2. Load package configuration from `backend/src/core/config/static/packages/{country}.yaml` (fallback to `default.yaml`)
3. Check starter pack eligibility:
- If `register_bonus_claims.has_purchased_starter_pack = true`, exclude starter pack from response
- Otherwise, include starter pack with `starterEligible: true`
1. Load package mapping from `backend/src/core/config/static/packages/mapping.yaml`
2. Check starter pack eligibility:
- If `register_bonus_claims.has_purchased_starter_pack = true`, exclude starter pack from response
- Otherwise, include starter pack with `starterEligible: true`
**Configuration Files:**
- Path: `backend/src/core/config/static/packages/`
- Format: YAML
- Example: `us.yaml`
- Example: `mapping.yaml`
```yaml
region: US
currency: USD
packages:
- product_code: new_user_pack
type: starter
price: "0.99"
product_mappings:
new_user_pack:
app_store_product_id: com.meeyao.qianwen.new_user_pack
credits: 60
badge: null
type: starter
sort_order: 0
enabled: true
- product_code: basic_pack
type: regular
price: "4.99"
starter_pack:
app_store_product_id: com.meeyao.qianwen.starter_pack
credits: 100
badge: null
type: regular
sort_order: 10
enabled: true
```
**Country/Region Codes:**
- Uses ISO 3166-1 alpha-2 standard
- Default: `US` (United States)
- Examples: `CN` (China), `TW` (Taiwan), `HK` (Hong Kong), `JP` (Japan)