# 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 ' ``` ### 锁定图片包响应 ```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=&limit=20 ``` ## 6. POST /api/private-room/albums/{albumId}/unlock ### 请求格式 ```http Content-Type: application/json Authorization: Bearer ``` 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 ' \ -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. 测试结束后删除测试素材、图片包、图片和解锁记录。