# VIP / Credits Frontend API

本文档是前端接入会员、积分、权益和历史解锁的推荐接口说明。新版接入原则：

- 前端权益状态只认 `GET /api/user/entitlements`。
- 游客和登录非会员权益一致，只按 `isVip` 区分会员/非会员。
- 支付完成后只刷新一次 `GET /api/user/entitlements`。
- `dolBalance` 是旧字段名，等价于 `creditBalance`，新代码优先使用 `creditBalance`。

## 推荐调用链

```text
打开充值/VIP 弹窗
  -> GET /api/payment/plans
  -> POST /api/payment/create-order
  -> 打开 data.payParams.url / checkout_url / payment_url
  -> GET /api/payment/order-status 轮询，或等待 WebSocket payment_success
  -> GET /api/user/entitlements 刷新权益
```

不要再按订单类型分别调用 `/api/payment/vip-status` 和 `/api/user/credits` 刷新状态。

## GET /api/user/entitlements

用途：当前用户 VIP、积分和权益快照。支持游客 token 和登录 token。

认证：需要 `Authorization: Bearer <token>`，游客 token 也可以。

响应示例：

```json
{
  "success": true,
  "data": {
    "userId": "user-1",
    "isGuest": false,
    "isVip": false,
    "vipExpiresAt": null,
    "creditBalance": 120,
    "dolBalance": 120,
    "policy": {
      "membershipState": "non_vip",
      "nonVipEntitlementsShared": true,
      "guestSameAsLoggedInNonVip": true,
      "refreshAfterPayment": "GET /api/user/entitlements"
    },
    "costs": {
      "normal_message": 2,
      "private_message": 10,
      "voice_message": 20,
      "photo": 40,
      "voice_call_minute": 50,
      "private_album_10": 300,
      "private_album_20": 600
    },
    "quotas": {
      "normalChatFreeDaily": 30,
      "privateUnlockFreeDaily": 1,
      "voiceMessageFreeDaily": 0,
      "photoFreeDaily": 0
    },
    "historyUnlock": {
      "enabled": true,
      "order": "oldest_first",
      "chargeMode": "highest_cost_per_locked_message",
      "insufficientBalanceBehavior": "no_deduction",
      "costs": {
        "private_message": 10,
        "voice_message": 20,
        "photo": 40
      }
    }
  }
}
```

前端使用：

- App 启动、登录成功、游客 token 创建成功后调用。
- 支付成功、充值成功、历史解锁成功后调用。
- `isGuest` 只用于 UI 身份展示，不用于权益判断。

## GET /api/payment/plans

用途：获取当前国家对应的会员/积分套餐。

认证：不需要登录。后端通过 `CF-IPCountry` 判断国家；登录后创建订单会再次按用户国家校验价格。

响应重点字段：

```json
{
  "plan_id": "vip_monthly",
  "plan_name": "月度会员",
  "order_type": "vip_monthly",
  "amount_cents": 1999,
  "original_amount_cents": 1999,
  "daily_price_cents": 66,
  "currency": "USD",
  "pricing_tier": "T1",
  "vip_days": 30,
  "dol_amount": null
}
```

字段说明：

- `amount_cents`: 实付金额，传给支付服务。
- `original_amount_cents`: 划线价；无划线价时为 `null`，字段一定存在。
- `currency`: 支付币种。
- `plan_id`: 创建订单时传回后端。

## POST /api/payment/create-order

用途：创建支付订单。

认证：需要登录 token。

请求：

```json
{
  "planId": "vip_monthly",
  "payChannel": "stripe",
  "autoRenew": true
}
```

响应：

```json
{
  "success": true,
  "data": {
    "orderId": "ord-1",
    "payParams": {
      "url": "https://pay.example/ord-1"
    }
  }
}
```

前端使用：

- `planId` 必须来自 `/api/payment/plans`。
- `payParams` 原样交给支付 SDK 或打开其中的支付 URL。
- 后端创建订单时使用 plans 里的 `amount_cents/currency`，不会再固定覆盖成 CNY。

## GET /api/payment/order-status

用途：轮询支付订单状态。

认证：需要登录 token，只能查自己的订单。

请求：

```text
GET /api/payment/order-status?order_id=ord-1
```

响应：

```json
{
  "success": true,
  "data": {
    "orderId": "ord-1",
    "status": "paid",
    "orderType": "vip",
    "planId": "vip_monthly"
  }
}
```

状态：

- `pending`: 支付处理中，继续轮询。
- `paid`: 支付成功，停止轮询并调用 `/api/user/entitlements`。
- `failed`: 支付失败，停止轮询并提示用户。

## WebSocket payment_success / payment_failed

用途：支付服务回调后，后端主动推送支付结果。

成功事件示例：

```json
{
  "type": "payment_success",
  "orderId": "ord-1",
  "payType": "vip",
  "planName": "月度会员",
  "vipExpiresAt": "2026-07-26T00:00:00+00:00"
}
```

积分充值成功示例：

```json
{
  "type": "payment_success",
  "orderId": "ord-2",
  "payType": "dol",
  "planName": "1000 Credits",
  "dolAmount": 1000,
  "dolBalance": 1120
}
```

前端收到 `payment_success` 后：

```text
停止 order-status 轮询
-> GET /api/user/entitlements
-> 用 creditBalance/isVip 更新页面
```

## POST /api/chat/unlock-history

用途：一键解锁历史锁定消息，按创建时间从旧到新处理。

认证：需要登录 token。

请求：无 body。

余额不足响应：

```json
{
  "success": true,
  "data": {
    "unlocked": false,
    "reason": "insufficient_balance",
    "totalLocked": 2,
    "unlockedCount": 0,
    "privateCount": 1,
    "imageCount": 1,
    "voiceCount": 0,
    "requiredCredits": 50,
    "currentCredits": 20,
    "remainingCredits": 20,
    "shortfallCredits": 30,
    "costsByMessage": {
      "private-1": 10,
      "photo-1": 40
    }
  }
}
```

余额充足响应：

```json
{
  "success": true,
  "data": {
    "unlocked": true,
    "reason": "ok",
    "requiredCredits": 60,
    "currentCredits": 100,
    "remainingCredits": 40,
    "shortfallCredits": 0,
    "messageIds": ["voice-1", "photo-1"]
  }
}
```

计费规则：

- 私密消息：10 credits。
- 语音消息：20 credits。
- 照片：40 credits。
- 同一条消息命中多个类型时，按最高成本计一次。
- 余额不足时不扣费、不解锁，只返回差额。
- 已解锁过的消息会跳过。

## 兼容接口

以下接口保留给旧前端，但新前端不推荐作为权益状态来源：

- `GET /api/payment/vip-status`: 只返回 `isVip/vipExpiresAt`。
- `GET /api/user/credits`: 只返回 `dolBalance`。
- `GET /api/user/stats`: 用户统计、亲密度、记忆等，不再用于支付后刷新权益。

## 测试证明

本地测试命令：

```powershell
.\.venv\Scripts\python.exe -m pytest -q tests/test_vip_credit_entitlements.py
.\.venv\Scripts\python.exe -m pytest -q tests/test_private_unlock.py tests/test_elio_image_paywall.py
```

已通过结果：

```text
tests/test_vip_credit_entitlements.py: 13 passed
tests/test_private_unlock.py tests/test_elio_image_paywall.py: 11 passed
```

覆盖证明：

- `/api/user/entitlements`: 已测游客、登录非会员、VIP；游客和登录非会员的 `policy/costs/quotas` 一致。
- `/api/payment/plans`: 已测 T1/T2 国家；所有计划都有 `original_amount_cents`。
- `/api/payment/create-order`: 已测国家价格计划会传入创建订单；支付服务收到的金额和币种来自 plan。
- `/api/payment/order-status`: 已测 `pending/paid/failed` 和禁止查询他人订单。
- WebSocket `payment_success`: 已测 VIP 和积分发放事件 payload。
- `/api/chat/unlock-history`: 已测无锁定消息、余额不足不扣费、余额充足扣费、已解锁跳过。
- 兼容接口：已测 `/api/payment/vip-status` 和 `/api/user/credits` 仍可用。
