Files
cozsweet-frontend-nextjs/docs/backend/FRONTEND_LOCKED_MEDIA_UNLOCK_API.md
T

448 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端锁消息解锁 API 接入文档
## 2. 功能目标
前端可以先展示一条本地锁消息。用户点击解锁时,即使该消息还没有后端 `messageId`,后端也会创建对应的聊天记录,并根据余额决定是否生成真实内容。
核心规则:
1. 余额不足时不生成真实语音、图片或私密内容,不扣积分,但会保存锁消息并返回真实 `messageId`
2. 用户充值后,前端使用同一个 `clientLockId` 再次解锁,后端更新原消息,不新增另一条聊天记录。
3. 成功扣积分并持久化解锁后,`history` 才返回解锁状态。
4. 刷新页面不会自动解锁。未扣积分、未消耗免费私密额度时,`history` 仍返回 `locked=true`
5. 前端只能使用 `unlocked``lockDetail.locked` 判断锁状态,不能根据 URL 是否为空判断。
## 4. 解锁接口
### 4.1 请求地址
```http
POST <API_BASE_URL>/api/chat/unlock-private
```
生产环境完整地址:
```http
POST https://api.banlv-ai.com/api/chat/unlock-private
```
### 4.2 请求参数
请求体为 JSON
```json
{
"messageId": "可选,有就传",
"lockType": "voice_message",
"clientLockId": "前端生成的唯一ID,可选但强烈建议"
}
```
| 字段 | 类型 | 是否必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `messageId` | string | 否 | `"9a43..."` | 后端真实消息 ID。已有消息时传;前端临时锁消息首次解锁时可以不传。 |
| `lockType` | string | 临时锁消息必填 | `"voice_message"` | 锁类型。没有有效 `messageId` 时必须传。 |
| `clientLockId` | string | 否,强烈建议 | `"fb_voice_3737_001"` | 前端锁卡片唯一 ID,用于充值后重试、刷新找回和避免重复创建。 |
`lockType` 支持以下值:
| 标准值 | 兼容别名 | 积分价格 |
| --- | --- | ---: |
| `voice_message` | `voice``audio` | 20 |
| `image_paywall` | `image``photo``picture` | 40 |
| `private_message` | `private``private_topic``private_room` | 10 |
建议前端只使用标准值。
### 4.3 `clientLockId` 生成规则
前端在创建锁卡片时生成一次,之后不得改变:
```ts
const clientLockId = crypto.randomUUID();
```
同一条锁消息在以下场景中必须继续使用同一个值:
- 第一次点击解锁;
- 余额不足后进入充值;
- 充值成功后重新点击解锁;
- 网络超时后重试;
- 前端重新渲染同一条本地锁卡片。
首次响应拿到后端 `messageId` 后,前端应同时保存 `messageId``clientLockId`
### 4.4 最小请求示例
解锁已有后端消息:
```bash
curl -X POST 'https://api.banlv-ai.com/api/chat/unlock-private' \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"messageId":"<MESSAGE_ID>"}'
```
解锁前端临时语音锁消息:
```bash
curl -X POST 'https://api.banlv-ai.com/api/chat/unlock-private' \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"lockType":"voice_message",
"clientLockId":"codex_test_voice_001"
}'
```
充值后建议把三个字段都传回:
```json
{
"messageId": "<FIRST_RESPONSE_MESSAGE_ID>",
"lockType": "voice_message",
"clientLockId": "codex_test_voice_001"
}
```
## 5. 统一响应结构
```json
{
"code": 200,
"message": "success",
"success": true,
"data": {}
}
```
业务解锁失败通常也返回 HTTP 200。前端必须读取 `data.unlocked``data.reason`,不能只检查 HTTP 状态或顶层 `success`
重要字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `unlocked` | boolean | 本次操作后是否已真实解锁。 |
| `reason` | string/null | 本次操作结果原因。 |
| `messageId` / `message_id` | string | 后端真实消息 ID。首次临时锁请求后也会返回。 |
| `clientLockId` | string/null | 后端识别到的前端锁 ID。 |
| `lockType` | string/null | 规范化后的锁类型。 |
| `reply` / `response` | string | 当前消息文字。 |
| `content` | string/null | 允许展示时返回内容;私密内容未解锁时可以为 `null`。 |
| `type` / `messageType` | string | `text``voice`。 |
| `audioUrl` / `audio_url` | string | 锁定语音为空字符串,成功解锁后是完整 URL。 |
| `image.url` | string/null | 图片完整 URL;是否展示仍由 `lockDetail.locked` 决定。 |
| `creditBalance` | number/null | 操作后的当前积分。 |
| `creditsCharged` | number | 本次实际扣除积分;失败或免费额度解锁为 0。 |
| `requiredCredits` | number | 当前锁类型需要的积分。 |
| `shortfallCredits` | number | 余额不足时还差多少积分。 |
| `lockDetail.locked` | boolean | 前端锁卡片的最终展示开关。 |
| `lockDetail.showContent` | boolean | 是否允许展示文字内容。 |
| `lockDetail.showUpgrade` | boolean | 是否展示充值入口。 |
| `lockDetail.reason` | string/null | 锁定原因。 |
## 6. 余额不足
请求:
```json
{
"lockType": "voice_message",
"clientLockId": "fb_voice_37370387172559600_001"
}
```
代表性响应:
```json
{
"code": 200,
"message": "success",
"success": true,
"data": {
"unlocked": false,
"reason": "insufficient_credits",
"type": "voice",
"messageType": "voice",
"messageId": "<BACKEND_MESSAGE_ID>",
"message_id": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_voice_37370387172559600_001",
"lockType": "voice_message",
"reply": "I left a voice message for you. Unlock it to listen.",
"content": "I left a voice message for you. Unlock it to listen.",
"audioUrl": "",
"audio_url": "",
"creditBalance": 5,
"creditsCharged": 0,
"requiredCredits": 20,
"shortfallCredits": 15,
"lockDetail": {
"locked": true,
"showContent": true,
"showUpgrade": true,
"reason": "voice_message",
"detail": {
"messageId": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_voice_37370387172559600_001",
"lockType": "voice_message",
"requiredCredits": 20
}
}
}
}
```
前端处理:
1. 使用返回的 `messageId` 替换本地临时消息 ID。
2. 保留原 `clientLockId`
3. 保持锁定状态并打开充值页面。
4. 充值完成后再次调用解锁接口。
5. 请求期间禁用解锁按钮,避免用户连续点击。
## 7. 解锁成功
### 7.1 语音成功
后端先确认并扣除 20 积分,然后生成真实文字和语音,更新同一条聊天记录:
```json
{
"code": 200,
"message": "success",
"success": true,
"data": {
"unlocked": true,
"reason": "ok",
"type": "voice",
"messageType": "voice",
"messageId": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_voice_37370387172559600_001",
"lockType": "voice_message",
"reply": "I made this voice just for you.",
"content": "I made this voice just for you.",
"audioUrl": "https://api.banlv-ai.com/audio/<FILE>.mp3",
"audio_url": "https://api.banlv-ai.com/audio/<FILE>.mp3",
"creditBalance": 10,
"creditsCharged": 20,
"requiredCredits": 20,
"shortfallCredits": 0,
"lockDetail": {
"locked": false,
"showContent": true,
"showUpgrade": false
}
}
}
```
前端收到后直接用本次响应替换锁卡片,播放地址读取 `data.audioUrl`
### 7.2 图片成功
```json
{
"unlocked": true,
"reason": "ok",
"messageId": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_image_001",
"lockType": "image_paywall",
"image": {
"type": "elio_schedule",
"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/<PATH>"
},
"creditBalance": 60,
"creditsCharged": 40,
"requiredCredits": 40,
"lockDetail": {
"locked": false,
"showContent": true,
"showUpgrade": false
}
}
```
图片读取 `data.image.url`。后端已有图片锁消息可能在锁定时也返回真实 URL,因为 URL 本身不作为解锁凭证;前端仍必须根据 `lockDetail.locked` 遮挡或禁止查看。
前端临时创建、尚未确定图片内容的锁消息,在成功扣积分前可能没有图片 URL;成功后后端选择图片并返回 URL。
### 7.3 私密文本成功
前端临时私密消息按 10 积分生成并解锁:
```json
{
"unlocked": true,
"reason": "ok",
"messageId": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_private_001",
"lockType": "private_message",
"reply": "<REAL_PRIVATE_REPLY>",
"content": "<REAL_PRIVATE_REPLY>",
"creditsCharged": 10,
"requiredCredits": 10,
"lockDetail": {
"locked": false,
"showContent": true,
"showUpgrade": false
}
}
```
已有后端私密文本仍优先使用每日免费私密额度:登录用户默认每天 2 条,游客默认每天 1 条。成功消耗免费额度后也会持久解锁,但 `creditsCharged=0`
## 8. History 接口
### 8.1 请求
```http
GET <API_BASE_URL>/api/chat/history?limit=50&offset=0
Authorization: Bearer <TOKEN>
```
```bash
curl 'https://api.banlv-ai.com/api/chat/history?limit=50&offset=0' \
-H 'Authorization: Bearer <TOKEN>'
```
`limit=1&offset=0` 返回最新的一条数据库聊天记录展开后的消息;接口最终按聊天展示顺序返回 `data.messages`
### 8.2 锁定消息示例
```json
{
"role": "assistant",
"type": "voice",
"content": "I left a voice message for you. Unlock it to listen.",
"id": "<BACKEND_MESSAGE_ID>",
"created_at": "2026-07-13T08:00:00+00:00",
"audioUrl": null,
"image": {
"type": null,
"url": null
},
"lockDetail": {
"locked": true,
"showContent": true,
"showUpgrade": true,
"reason": "voice_message",
"detail": {
"messageId": "<BACKEND_MESSAGE_ID>",
"clientLockId": "fb_voice_37370387172559600_001",
"lockType": "voice_message",
"requiredCredits": 20
}
}
}
```
### 8.3 前端展示规则
```ts
const locked = message.lockDetail?.locked === true;
const showText = message.lockDetail?.showContent !== false;
if (locked) {
// 展示锁卡片、遮罩或解锁按钮
} else {
// 展示真实内容
}
```
各类型规则:
| 状态 | `lockDetail.locked` | URL/内容行为 |
| --- | ---: | --- |
| 锁定语音 | `true` | `audioUrl=null`,不能播放。 |
| 已解锁语音 | `false` | `audioUrl` 是完整可播放 URL。 |
| 锁定图片 | `true` | `image.url` 可能有值,也可能为空;必须遮挡。 |
| 已解锁图片 | `false` | 使用完整 `image.url` 展示。 |
| 锁定私密文本 | `true` | 根据 `showContent` 决定是否显示 `content`。 |
| 已解锁私密文本 | `false` | 展示完整 `content`。 |
## 9. 推荐 TypeScript 类型
```ts
type LockType = "voice_message" | "image_paywall" | "private_message";
interface UnlockPrivateRequest {
messageId?: string;
lockType?: LockType;
clientLockId?: string;
}
interface LockDetail {
locked: boolean;
showContent: boolean;
showUpgrade: boolean;
reason?: string | null;
hint?: string | null;
actionLabel?: string | null;
detail?: {
messageId?: string;
clientLockId?: string;
lockType?: LockType;
requiredCredits?: number;
refundedCredits?: number;
} | null;
}
interface UnlockPrivateData {
unlocked: boolean;
reason?: string | null;
messageId: string;
message_id: string;
clientLockId?: string | null;
lockType?: LockType | null;
type: "text" | "voice";
messageType: "text" | "voice";
reply: string;
response: string;
content?: string | null;
audioUrl: string;
audio_url: string;
image: {
type?: string | null;
url?: string | null;
};
creditBalance?: number | null;
creditsCharged: number;
requiredCredits: number;
shortfallCredits: number;
displayMessage: string;
localizedMessage: string;
lockDetail: LockDetail;
}
```
## 10. 异常与前端处理
| `data.reason` | 含义 | 前端处理 |
| --- | --- | --- |
| `ok` | 成功扣费并解锁 | 用响应替换锁卡片。 |
| `not_locked` | 消息已经不是锁定状态 | 直接按已解锁内容展示,不再扣费。 |
| `insufficient_credits` | 积分不足 | 保存返回的 `messageId`,保持锁定并进入充值。 |
| `content_generation_failed` | 临时锁内容生成失败 | 后端尝试退回本次积分;保持锁定,允许稍后重试。 |
| `voice_generation_failed` | 已有语音消息生成失败 | 未扣积分,保持锁定并允许重试。 |
| `quota_exhausted` | 已有私密文本每日免费额度用完 | 展示 `displayMessage`。 |
| `invalid_lock_type` | `lockType` 不支持 | 记录前端错误并停止请求。 |
| `not_found` | 消息不存在且没有有效 `lockType` | 禁用当前解锁按钮或重新同步 history。 |
认证失败会返回 HTTP 401。请求字段类型不合法可能返回 HTTP 422。
## 11. 前端完整流程
```text
创建本地锁卡片并生成 clientLockId
-> 用户点击解锁
-> POST /api/chat/unlock-private
-> unlocked=true:替换卡片并展示真实内容
-> insufficient_credits:保存 messageId,打开充值
-> 充值成功
-> 使用相同 messageId + lockType + clientLockId 重试
-> unlocked=true:替换卡片
-> 页面刷新
-> GET /api/chat/history
-> 只按 lockDetail.locked 恢复锁定或解锁状态
```