308 lines
8.4 KiB
Markdown
308 lines
8.4 KiB
Markdown
# Cozsweet 独立付费图片包前端接口
|
||
|
||
## 1. 用途
|
||
|
||
Cozsweet 前端通过本接口展示独立的 8/15 张私密图片包,并使用用户积分解锁。FB 只导入原始图片素材,后端素材池负责跨批次自动凑成 8 张或 15 张图片包。该功能不走现金支付,不使用旧 `/moments` 接口。
|
||
|
||
## 2. 接口地址
|
||
|
||
| 功能 | 方法 | 路径 |
|
||
| --- | --- | --- |
|
||
| 图片包列表 | GET | `/api/private-room/albums` |
|
||
| 积分解锁 | POST | `/api/private-room/albums/{albumId}/unlock` |
|
||
|
||
## 4. 固定价格
|
||
|
||
| 图片数量 | `unlockCost` | 单张价格 |
|
||
| ---: | ---: | ---: |
|
||
| 8 | 320 credits | 40 credits |
|
||
| 15 | 600 credits | 40 credits |
|
||
|
||
接口只会返回 `currency: "credits"`。前端不得显示现金价格,也不得调用支付套餐或创建订单接口。
|
||
|
||
## 5. GET /api/private-room/albums
|
||
|
||
### 请求参数
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `character` | string | 否 | `elio` | 角色 ID。 |
|
||
| `collectionKey` | string | 否 | 全部 | 只查看一个素材分组。 |
|
||
| `limit` | integer | 否 | 20 | 1-50。 |
|
||
| `cursor` | ISO datetime | 否 | - | 上一页返回的 `nextCursor`。 |
|
||
|
||
### 请求示例
|
||
|
||
```bash
|
||
curl 'https://api.banlv-ai.com/api/private-room/albums?character=elio&limit=20' \
|
||
-H 'Authorization: Bearer <TOKEN>'
|
||
```
|
||
|
||
### 锁定图片包响应
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"success": true,
|
||
"data": {
|
||
"items": [
|
||
{
|
||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
||
"momentId": "album:a1b2c3d4-0000-0000-0000-000000000000",
|
||
"characterId": "elio",
|
||
"collectionKey": "manila_202607",
|
||
"title": "Private Manila set",
|
||
"content": null,
|
||
"previewText": "Only for you.",
|
||
"imageCount": 8,
|
||
"mediaCount": 8,
|
||
"images": [
|
||
{
|
||
"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg",
|
||
"type": "image",
|
||
"locked": true,
|
||
"index": 0
|
||
}
|
||
],
|
||
"locked": true,
|
||
"unlocked": false,
|
||
"unlockCost": 320,
|
||
"requiredCredits": 320,
|
||
"creditCostPerImage": 40,
|
||
"currency": "credits",
|
||
"canUnlockWithCredits": false,
|
||
"publishedAt": "2026-07-13T00:00:00+00:00",
|
||
"lockDetail": {
|
||
"locked": true,
|
||
"showContent": false,
|
||
"showUpgrade": true,
|
||
"reason": "private_album",
|
||
"requiredCredits": 320,
|
||
"currentCredits": 100,
|
||
"shortfallCredits": 220,
|
||
"mediaCount": 8,
|
||
"unlockCostPerImage": 40
|
||
}
|
||
}
|
||
],
|
||
"nextCursor": null,
|
||
"hasMore": false,
|
||
"creditBalance": 100,
|
||
"currency": "credits",
|
||
"creditCostPerImage": 40,
|
||
"packageOptions": [
|
||
{"imageCount": 8, "creditCost": 320},
|
||
{"imageCount": 15, "creditCost": 600}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
锁定和解锁状态都会返回完整 `images[].url`。前端必须根据 `locked` / `unlocked` / `lockDetail.locked` 决定是否显示锁层和是否允许查看原图,不能用 URL 是否为空判断解锁状态。
|
||
|
||
### 已解锁图片包
|
||
|
||
同一个用户解锁后,再次请求列表会返回:
|
||
|
||
```json
|
||
{
|
||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
||
"locked": false,
|
||
"unlocked": true,
|
||
"content": "Only for you.",
|
||
"imageCount": 8,
|
||
"images": [
|
||
{
|
||
"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg",
|
||
"type": "image",
|
||
"locked": false,
|
||
"index": 0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 分页
|
||
|
||
`hasMore=true` 时,把 `nextCursor` 原样传回:
|
||
|
||
```http
|
||
GET /api/private-room/albums?cursor=<nextCursor>&limit=20
|
||
```
|
||
|
||
## 6. POST /api/private-room/albums/{albumId}/unlock
|
||
|
||
### 请求格式
|
||
|
||
```http
|
||
Content-Type: application/json
|
||
Authorization: Bearer <TOKEN>
|
||
```
|
||
|
||
Body:
|
||
|
||
```json
|
||
{
|
||
"expectedCost": 320
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 示例 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `expectedCost` | integer | 否但建议 | 320 | 用户确认时看到的价格;价格变化则拒绝扣分。 |
|
||
|
||
### 请求示例
|
||
|
||
```bash
|
||
curl -X POST 'https://api.banlv-ai.com/api/private-room/albums/a1b2c3d4-0000-0000-0000-000000000000/unlock' \
|
||
-H 'Authorization: Bearer <TOKEN>' \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"expectedCost":320}'
|
||
```
|
||
|
||
### 解锁成功
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"success": true,
|
||
"data": {
|
||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
||
"locked": false,
|
||
"unlocked": true,
|
||
"reason": "ok",
|
||
"unlockCost": 320,
|
||
"creditsCharged": 320,
|
||
"previousCreditBalance": 500,
|
||
"creditBalance": 180,
|
||
"images": [
|
||
{
|
||
"url": "https://...",
|
||
"type": "image",
|
||
"locked": false,
|
||
"index": 0
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
前端直接使用响应里的 `images` 替换锁卡,并用 `creditBalance` 刷新余额。
|
||
|
||
### 积分不足
|
||
|
||
业务失败仍返回 HTTP 200:
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"success": true,
|
||
"data": {
|
||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
||
"locked": true,
|
||
"unlocked": false,
|
||
"reason": "insufficient_credits",
|
||
"unlockCost": 320,
|
||
"requiredCredits": 320,
|
||
"creditBalance": 100,
|
||
"shortfallCredits": 220,
|
||
"creditsCharged": 0,
|
||
"images": [
|
||
{"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg", "type": "image", "locked": true, "index": 0}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
前端可以打开积分充值页,但后端不会创建图片包现金订单。用户充值积分后,再次点击同一个解锁接口。
|
||
|
||
### 重复解锁
|
||
|
||
```json
|
||
{
|
||
"unlocked": true,
|
||
"locked": false,
|
||
"reason": "already_unlocked",
|
||
"creditsCharged": 0,
|
||
"creditBalance": 180,
|
||
"images": [{"url": "https://...", "locked": false, "index": 0}]
|
||
}
|
||
```
|
||
|
||
## 7. `reason` 处理
|
||
|
||
| `data.reason` | 含义 | 前端动作 |
|
||
| --- | --- | --- |
|
||
| `ok` | 已扣积分并解锁 | 显示真实图片并更新余额。 |
|
||
| `already_unlocked` | 以前已解锁,本次不扣 | 直接显示返回图片。 |
|
||
| `insufficient_credits` | 积分不足,未扣分 | 保持锁定,可引导购买积分。 |
|
||
| `cost_changed` | 价格与 `expectedCost` 不一致 | 刷新列表并重新确认。 |
|
||
| `unlock_in_progress` | 同一图片包正在并发解锁 | 禁用按钮并稍后刷新。 |
|
||
| `deduct_failed` | 扣积分失败 | 保持锁定并允许重试。 |
|
||
| `persist_failed_refunded` | 解锁记录失败,积分已尽力退回 | 保持锁定、刷新余额并告警。 |
|
||
| `not_found` | 图片包不存在或已停用 | 移除卡片。 |
|
||
|
||
## 8. TypeScript 建议
|
||
|
||
```ts
|
||
export interface PrivateAlbumImage {
|
||
url: string;
|
||
type: "image";
|
||
locked: boolean;
|
||
index: number;
|
||
}
|
||
|
||
export interface PrivateAlbum {
|
||
albumId: string;
|
||
momentId: string;
|
||
characterId: string;
|
||
collectionKey: string;
|
||
title: string;
|
||
content: string | null;
|
||
previewText: string;
|
||
imageCount: 8 | 15;
|
||
mediaCount: 8 | 15;
|
||
images: PrivateAlbumImage[];
|
||
locked: boolean;
|
||
unlocked: boolean;
|
||
unlockCost: 320 | 600;
|
||
requiredCredits: 320 | 600;
|
||
creditCostPerImage: 40;
|
||
currency: "credits";
|
||
canUnlockWithCredits: boolean;
|
||
publishedAt: string;
|
||
}
|
||
```
|
||
|
||
## 9. 前端流程
|
||
|
||
1. 进入图片包页面,调用 `GET /api/private-room/albums`。
|
||
2. `locked=true` 时可使用返回 URL 渲染封面/模糊图,但必须覆盖锁层并禁止打开原图。
|
||
3. 用户确认后调用解锁接口并传当前 `expectedCost`。
|
||
4. `reason=ok/already_unlocked` 时使用返回的真实图片。
|
||
5. `reason=insufficient_credits` 时保持锁定并引导购买积分。
|
||
6. 充值完成后重新调用解锁接口,不需要恢复现金订单。
|
||
7. 解锁请求进行中禁用按钮,避免重复点击。
|
||
|
||
## 10. HTTP 错误
|
||
|
||
| HTTP 状态 | 原因 | 处理 |
|
||
| ---: | --- | --- |
|
||
| 401 | Token 无效或缺失 | 刷新 Token/重新登录。 |
|
||
| 422 | `albumId`、query 或 body 格式错误 | 修正请求。 |
|
||
| 500 | 数据库或服务异常 | 保持当前锁定状态并允许重试。 |
|
||
|
||
## 11. 测试方法
|
||
|
||
1. 先在 pro 执行 `database/private-albums-migration.sql`。
|
||
2. 使用 FB 测试导入生成一个 8 张图片包。
|
||
3. 用少于 320 积分的测试账号请求解锁,确认不扣积分、不出现现金订单。
|
||
4. 给测试账号补足积分,再请求解锁,确认扣 320 且返回 8 个真实 URL。
|
||
5. 重复解锁,确认 `creditsCharged=0`。
|
||
6. 刷新列表,确认仍为 `unlocked=true`。
|
||
7. 15 张图片包按同样方式验证扣 600。
|
||
8. 测试结束后删除测试素材、图片包、图片和解锁记录。
|