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

13 KiB
Raw Blame History

前端锁消息解锁 API 接入文档

2. 功能目标

前端可以先展示一条本地锁消息。用户点击解锁时,即使该消息还没有后端 messageId,后端也会创建对应的聊天记录,并根据余额决定是否生成真实内容。

核心规则:

  1. 余额不足时不生成真实语音、图片或私密内容,不扣积分,但会保存锁消息并返回真实 messageId
  2. 用户充值后,前端使用同一个 clientLockId 再次解锁,后端更新原消息,不新增另一条聊天记录。
  3. 成功扣积分并持久化解锁后,history 才返回解锁状态。
  4. 刷新页面不会自动解锁。未扣积分、未消耗免费私密额度时,history 仍返回 locked=true
  5. 前端只能使用 unlockedlockDetail.locked 判断锁状态,不能根据 URL 是否为空判断。

4. 解锁接口

4.1 请求地址

POST <API_BASE_URL>/api/chat/unlock-private

生产环境完整地址:

POST https://api.banlv-ai.com/api/chat/unlock-private

4.2 请求参数

请求体为 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 voiceaudio 20
image_paywall imagephotopicture 40
private_message privateprivate_topicprivate_room 10

建议前端只使用标准值。

4.3 clientLockId 生成规则

前端在创建锁卡片时生成一次,之后不得改变:

const clientLockId = crypto.randomUUID();

同一条锁消息在以下场景中必须继续使用同一个值:

  • 第一次点击解锁;
  • 余额不足后进入充值;
  • 充值成功后重新点击解锁;
  • 网络超时后重试;
  • 前端重新渲染同一条本地锁卡片。

首次响应拿到后端 messageId 后,前端应同时保存 messageIdclientLockId

4.4 最小请求示例

解锁已有后端消息:

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>"}'

解锁前端临时语音锁消息:

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"
  }'

充值后建议把三个字段都传回:

{
  "messageId": "<FIRST_RESPONSE_MESSAGE_ID>",
  "lockType": "voice_message",
  "clientLockId": "codex_test_voice_001"
}

5. 统一响应结构

{
  "code": 200,
  "message": "success",
  "success": true,
  "data": {}
}

业务解锁失败通常也返回 HTTP 200。前端必须读取 data.unlockeddata.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 textvoice
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. 余额不足

请求:

{
  "lockType": "voice_message",
  "clientLockId": "fb_voice_37370387172559600_001"
}

代表性响应:

{
  "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 积分,然后生成真实文字和语音,更新同一条聊天记录:

{
  "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 图片成功

{
  "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 积分生成并解锁:

{
  "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 请求

GET <API_BASE_URL>/api/chat/history?limit=50&offset=0
Authorization: Bearer <TOKEN>
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 锁定消息示例

{
  "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 前端展示规则

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 类型

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. 前端完整流程

创建本地锁卡片并生成 clientLockId
  -> 用户点击解锁
  -> POST /api/chat/unlock-private
     -> unlocked=true:替换卡片并展示真实内容
     -> insufficient_credits:保存 messageId,打开充值
        -> 充值成功
        -> 使用相同 messageId + lockType + clientLockId 重试
        -> unlocked=true:替换卡片
  -> 页面刷新
  -> GET /api/chat/history
  -> 只按 lockDetail.locked 恢复锁定或解锁状态