13 KiB
单角色应用迁移为多角色聊天架构
1. 文档目标
本文档定义 CozSweet 从单角色 Elio 迁移到多角色架构时,后端需要完成的接口、数据模型和兼容策略调整。
迁移目标:
- 前端维护统一的本地角色目录,后端只识别稳定角色 ID。
- 同一用户可以分别与多个角色聊天。
- 不同角色的聊天历史、媒体、锁内容、私密相册和打赏归属必须隔离。
- 钱包、VIP、支付套餐和每日免费额度继续按用户全局共享。
- 现有客户端和历史数据继续默认归属于 Elio。
本文档中的标准角色标识:
| 类型 | Elio 示例 | 用途 |
|---|---|---|
id |
character_elio |
数据库关联、API 请求和缓存隔离 |
slug |
elio |
前端 URL 和外部入口 |
后端业务接口必须使用 id,不能使用可能变化的展示名称。
环境地址:
| 环境 | API Base URL |
|---|---|
| test 测试环境 | https://testapi.banlv-ai.com |
| pro 预发环境 | https://proapi.banlv-ai.com |
| production 生产环境 | https://api.banlv-ai.com |
2. 前端本地角色目录
角色列表不通过网络接口加载。前端在本地维护只读目录,并使用 slug 解析角色路径、使用 id 调用后端业务接口。
当前角色:
id |
slug |
displayName |
头像 | 页面封面 | 私密空间 Banner |
|---|---|---|---|---|---|
character_elio |
elio |
Elio Silvestri | /images/avatar/elio.png |
/images/cover/elio.png |
/images/private-room/banner/elio.png |
character_maya |
maya |
Maya Tan | /images/avatar/maya.png |
/images/cover/maya.png |
/images/private-room/banner/maya.png |
character_nayeli |
nayeli |
Nayeli Cervantes | /images/avatar/nayeli.png |
/images/cover/nayeli.png |
/images/private-room/banner/nayeli.png |
本地记录同时保存短名称及 Splash、Private Room、Tip 使用的角色文案。后端不提供头像、封面、展示名称或角色目录接口,但后端配置中的角色 ID 必须与前端目录一致。
3. 聊天接口调整
3.1 通用规则
以下接口新增标准字段 characterId:
POST /api/chat/send
GET /api/chat/history
POST /api/chat/unlock-private
POST /api/chat/unlock-history
characterId 使用前端本地角色目录中的 id,例如 character_elio,不能传 slug 或显示名称。
后端必须使用以下维度定位聊天数据:
正式用户:userId + characterId
游客:guest identity/device identity + characterId
任何历史查询、消息发送、解锁和配额判断都不能只按用户查找。
3.2 发送消息
POST /api/chat/send
Content-Type: application/json
Authorization: Bearer <TOKEN>
现有请求体增加 characterId,其他字段保持原协议:
{
"characterId": "character_elio",
"message": "Hello Elio",
"image": "",
"imageId": "",
"imageThumbUrl": "",
"imageMediumUrl": "",
"imageOriginalUrl": "",
"imageWidth": 0,
"imageHeight": 0,
"useWebSocket": false
}
后端处理要求:
- 校验角色存在且已启用。
- 使用
characterId选择对应角色 Prompt、模型配置和聊天上下文。 - 只加载该用户与该角色的历史。
- 新消息和生成结果必须写入相同
characterId。 - 响应继续沿用现有
ChatSendResponse,不额外返回前端不使用的角色字段。
如果后端启用 WebSocket,连接握手或每条发送帧也必须携带 characterId,不能依赖服务端进程内的“当前角色”状态。
3.3 获取聊天历史
GET /api/chat/history?characterId=character_elio&limit=50&offset=0
Authorization: Bearer <TOKEN>
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
characterId |
string | 是 | 角色业务 ID。 |
limit |
integer | 否 | 保持现有分页规则。 |
offset |
integer | 否 | 保持现有分页规则。 |
响应结构和分页字段保持不变。messages、total 和 offset 必须只统计当前角色。
禁止在当前角色历史中返回其他角色的消息,即使消息属于同一用户。
3.4 解锁单条消息
POST /api/chat/unlock-private
Content-Type: application/json
Authorization: Bearer <TOKEN>
请求体增加 characterId:
{
"characterId": "character_elio",
"messageId": "message-id",
"lockType": "voice_message",
"clientLockId": "client-generated-id"
}
现有 messageId、lockType 和 clientLockId 规则保持不变。
后端必须验证:
- 已存在消息的
character_id与请求characterId一致。 - 临时锁消息不存在
messageId时,新建消息必须写入请求角色。 clientLockId的幂等范围至少包含身份和角色,推荐唯一键:
identity + character_id + client_lock_id
跨角色解锁返回 CHARACTER_MISMATCH,不能扣积分,也不能修改消息。
3.5 一键解锁历史
POST /api/chat/unlock-history
Content-Type: application/json
Authorization: Bearer <TOKEN>
请求体由空请求调整为:
{
"characterId": "character_elio"
}
后端只能统计和解锁当前角色的锁消息。requiredCredits、解锁数量和返回 messages 都必须按当前角色计算。
4. 角色最新消息接口
角色列表需要一次性获取各角色的最新消息,避免对每个角色分别请求 history。
4.1 请求
GET /api/chat/previews
Authorization: Bearer <TOKEN>
4.2 响应
{
"success": true,
"message": "success",
"data": {
"items": [
{
"characterId": "character_elio",
"message": {
"id": "message-id",
"role": "assistant",
"type": "text",
"content": "How was your day?",
"createdAt": "2026-07-17T10:00:00Z"
}
},
{
"characterId": "character_aria",
"message": null
}
]
}
}
items按前端本地角色目录顺序返回当前可用角色。message使用现有 ChatMessage wire 结构;没有历史时返回null。- 锁消息继续遵循现有内容隐藏规则,不能通过预览接口泄露 URL 或锁定内容。
- 查询应批量完成,避免后端内部出现按角色逐条查询的 N+1 问题。
5. 私密空间接口调整
5.1 获取相册
标准请求改为:
GET /api/private-room/albums?characterId=character_elio&limit=20
Authorization: Bearer <TOKEN>
兼容期继续接受旧参数:
GET /api/private-room/albums?character=elio&limit=20
参数优先级:
- 存在
characterId时使用标准 ID。 - 兼容期仅存在
character时,将旧 slug 解析为角色 ID。 - 两者同时存在但指向不同角色时返回
CHARACTER_MISMATCH。 - 两者都缺失时,兼容期默认 Elio。
响应结构保持不变,不增加前端未使用的角色字段。
5.2 解锁相册
接口路径保持不变:
POST /api/private-room/albums/{albumId}/unlock
albumId 已能唯一确定角色,因此请求体不增加 characterId。后端必须根据 album 关联的角色进行权限、积分和解锁记录校验。
6. Tip 支付接口调整
6.1 创建订单
现有接口:
POST /api/payment/create-order
请求体增加可选字段 recipientCharacterId:
{
"planId": "tip_coffee_medium",
"payChannel": "stripe",
"autoRenew": false,
"recipientCharacterId": "character_elio"
}
后端必须先根据 planId 判断订单类型:
| 订单类型 | recipientCharacterId 规则 |
|---|---|
| Tip | 必填,并校验角色存在且启用 |
| VIP | 忽略,不保存 |
| Top-up | 忽略,不保存 |
Tip 订单和最终支付记录需要保存 recipient_character_id,用于归属、统计和后续展示。
支付套餐、创建订单响应和订单状态响应不增加角色字段。
7. 数据模型调整
7.1 Characters 表
后台建议至少保存:
| 字段 | 说明 |
|---|---|
id |
主键,稳定角色 ID。 |
slug |
唯一 URL 标识。 |
display_name |
展示名称。 |
avatar_url |
头像地址。 |
enabled |
是否对用户启用,仅后台使用。 |
sort_order |
运营排序,仅后台使用。 |
created_at |
创建时间。 |
updated_at |
更新时间。 |
enabled 和 sort_order 是后端管理字段,不返回前端。
7.2 业务表
以下数据增加 character_id 外键:
- conversations 或等价会话表;
- chat messages;
- 锁消息、解锁记录和
clientLockId幂等记录; - private albums 及相册解锁记录;
- Tip 订单或 Tip 业务记录。
推荐索引:
(user_id, character_id, created_at)
(guest_id, character_id, created_at)
(character_id, enabled)
(identity, character_id, client_lock_id) UNIQUE
如果当前系统没有 conversations 表,可以继续使用 messages,但所有历史查询必须显式包含 character_id。
8. 历史数据迁移
上线新接口前执行:
- 创建 Elio 角色,固定
id=character_elio、slug=elio。 - 为相关业务表增加可空
character_id。 - 将现有消息、锁记录、相册和 Tip 记录全部回填为
character_elio。 - 检查不存在空角色或无法关联的数据。
- 创建联合索引和外键。
- 将必要业务表的
character_id调整为非空。
回填前后需要核对每张表的记录数量,迁移不能删除历史聊天或解锁状态。
9. 兼容发布策略
第一阶段:后端兼容
- 上线角色 ID 配置和新字段。
- Chat 请求缺少
characterId时暂时默认character_elio。 - Private Room 缺少角色时暂时默认 Elio。
- Tip 旧客户端缺少 recipient 时,旧 Tip 订单暂时默认 Elio。
- 记录缺少角色参数的调用次数和客户端版本。
第二阶段:前端迁移
- 新前端所有请求显式发送角色 ID。
- 动态路由和支付回跳携带角色上下文。
- 观察错误率、跨角色校验和旧请求占比。
第三阶段:收紧协议
- Chat 接口缺少
characterId时返回CHARACTER_REQUIRED。 - Tip 订单缺少 recipient 时返回
CHARACTER_REQUIRED。 - 删除 Private Room 旧
character参数。 - 删除默认 Elio 的接口兼容逻辑。
由于 PWA 和浏览器可能长期缓存旧前端,第三阶段只能在旧请求流量降至可接受水平后执行。
只有“完全缺少角色字段”的旧请求可以在兼容期默认 Elio。显式传入不存在、禁用或不匹配角色时不得回退 Elio。
10. 错误响应
统一失败 envelope:
{
"success": false,
"message": "Character is not available",
"error": "CHARACTER_DISABLED"
}
新增错误:
| HTTP 状态 | error | 场景 |
|---|---|---|
400 |
CHARACTER_REQUIRED |
收紧协议后缺少角色字段。 |
404 |
CHARACTER_NOT_FOUND |
id 或兼容 slug 不存在。 |
403 |
CHARACTER_DISABLED |
角色存在但未启用。 |
409 |
CHARACTER_MISMATCH |
消息、相册或重复参数属于不同角色。 |
角色错误不能扣积分、创建消息、创建订单或修改任何解锁状态。
11. 安全和一致性要求
- 角色 ID 必须由后端查询验证,不能信任客户端展示信息。
- 用户只能读取自己的某个角色会话,不能通过替换 characterId 读取其他用户数据。
- messageId、albumId 和 clientLockId 必须同时校验身份与角色归属。
- 角色禁用后不能继续发送、解锁、创建相册订单或 Tip 订单。
- 角色禁用不删除历史数据,恢复启用后历史仍可读取。
- 日志可以记录
characterId,但不能记录聊天正文、Token 或私密媒体 URL。 - Analytics 和业务统计统一使用角色 ID,不使用 displayName。
12. 联调验收
后端完成后至少验证:
- 前端本地目录中的每个角色 ID 均可调用 Chat、Private Room 和 Tip 业务接口。
- Login Token 和 Guest Token 均可按角色 ID 聊天。
- 同一用户在 Elio 和其他角色中的历史完全隔离。
- 发送消息、历史分页、单条解锁和历史解锁都限定当前角色。
- 使用 Elio 的 messageId 配合其他角色 ID 解锁时返回
CHARACTER_MISMATCH,且不扣积分。 - Private Room 的标准 characterId 与旧 character slug 在兼容期结果一致。
- Tip 订单正确保存
recipient_character_id,VIP 和 Top-up 不保存。 - 角色预览接口一次返回各启用角色的最新消息,没有历史时返回
null。 - 后端禁用角色后,直接访问业务接口返回
CHARACTER_DISABLED;前端发布时同步移除本地记录。 - 缺少角色字段的旧请求在兼容期仍进入 Elio。
- 历史数据回填前后记录数量、积分和解锁状态一致。
13. OpenAPI 参考
paths:
/api/chat/history:
get:
parameters:
- in: query
name: characterId
required: true
schema:
type: string
- in: query
name: limit
required: false
schema:
type: integer
- in: query
name: offset
required: false
schema:
type: integer
/api/chat/unlock-history:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [characterId]
properties:
characterId:
type: string