8.4 KiB
8.4 KiB
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。 |
请求示例
curl 'https://api.banlv-ai.com/api/private-room/albums?character=elio&limit=20' \
-H 'Authorization: Bearer <TOKEN>'
锁定图片包响应
{
"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 是否为空判断解锁状态。
已解锁图片包
同一个用户解锁后,再次请求列表会返回:
{
"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 原样传回:
GET /api/private-room/albums?cursor=<nextCursor>&limit=20
6. POST /api/private-room/albums/{albumId}/unlock
请求格式
Content-Type: application/json
Authorization: Bearer <TOKEN>
Body:
{
"expectedCost": 320
}
| 字段 | 类型 | 必填 | 示例 | 说明 |
|---|---|---|---|---|
expectedCost |
integer | 否但建议 | 320 | 用户确认时看到的价格;价格变化则拒绝扣分。 |
请求示例
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}'
解锁成功
{
"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:
{
"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}
]
}
}
前端可以打开积分充值页,但后端不会创建图片包现金订单。用户充值积分后,再次点击同一个解锁接口。
重复解锁
{
"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 建议
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. 前端流程
- 进入图片包页面,调用
GET /api/private-room/albums。 locked=true时可使用返回 URL 渲染封面/模糊图,但必须覆盖锁层并禁止打开原图。- 用户确认后调用解锁接口并传当前
expectedCost。 reason=ok/already_unlocked时使用返回的真实图片。reason=insufficient_credits时保持锁定并引导购买积分。- 充值完成后重新调用解锁接口,不需要恢复现金订单。
- 解锁请求进行中禁用按钮,避免重复点击。
10. HTTP 错误
| HTTP 状态 | 原因 | 处理 |
|---|---|---|
| 401 | Token 无效或缺失 | 刷新 Token/重新登录。 |
| 422 | albumId、query 或 body 格式错误 |
修正请求。 |
| 500 | 数据库或服务异常 | 保持当前锁定状态并允许重试。 |
11. 测试方法
- 先在 pro 执行
database/private-albums-migration.sql。 - 使用 FB 测试导入生成一个 8 张图片包。
- 用少于 320 积分的测试账号请求解锁,确认不扣积分、不出现现金订单。
- 给测试账号补足积分,再请求解锁,确认扣 320 且返回 8 个真实 URL。
- 重复解锁,确认
creditsCharged=0。 - 刷新列表,确认仍为
unlocked=true。 - 15 张图片包按同样方式验证扣 600。
- 测试结束后删除测试素材、图片包、图片和解锁记录。