Skip to content

前端对接文档 ​

本文面向后续生成或重构前端项目使用,目标是让前端按同一套接口、类型和实时协议接入 flow-talk-server。

完整 HTTP 接口以 openapi.json 为准;本文重点补充前端实现时需要的页面数据流、状态管理和 WebSocket 接入规则。

基础配置 ​

服务地址 ​

本地开发默认后端地址:

text
http://127.0.0.1:8080

WebSocket 地址由 HTTP 地址转换得到:

text
ws://127.0.0.1:8080/api/ws?token={jwt}&device_id={device_id}

推荐前端使用环境变量:

text
VITE_API_BASE_URL=http://127.0.0.1:8080

如果使用其它框架,变量名可按框架约定调整,但建议统一封装为 API_BASE_URL。

通用响应格式 ​

HTTP 接口统一返回:

json
{
  "code": 200,
  "data": {},
  "message": "操作成功"
}

前端封装请求时建议:

  • code === 200:返回 data
  • code !== 200:抛出带 message 的业务错误
  • HTTP 401:清理本地登录态并跳转登录页
  • HTTP 500:展示通用错误文案,并查看后端日志定位原因

鉴权 ​

登录或注册成功后,后端返回:

json
{
  "user": {
    "id": 1,
    "username": "alice",
    "nickname": "Alice",
    "auth_source": "local",
    "status": 1
  },
  "token": "<jwt-token>"
}

前端需要保存:

  • token:HTTP Authorization 和 WebSocket 建连使用
  • user:当前用户基础信息
  • device_id:浏览器本地生成并持久化,例如 web-{uuid}

HTTP 鉴权头:

text
Authorization: Bearer <jwt-token>

WebSocket 鉴权参数:

text
/api/ws?token=<jwt-token>&device_id=<device_id>

推荐前端模块 ​

建议生成前端项目时拆成以下模块:

text
src/
  api/                 HTTP API 封装
  hooks/
    useWebSocket.ts    WebSocket 建连、心跳、发送事件
  pages/
    LoginPage.tsx
    ChatPage.tsx
  stores/              登录态、会话、消息、在线状态
  types/               接口 DTO 和 WebSocket 事件类型
  utils/
    session.ts
    device.ts
    clientMessageId.ts

如果使用 React,可以用 useState/useReducer 或 Zustand 管理状态;如果使用 Vue,可以用 Pinia。

核心类型 ​

User ​

ts
interface User {
  id: number;
  username: string;
  nickname?: string;
  avatar_url?: string;
  auth_source?: string;
  status: number;
}

Conversation ​

ts
interface Conversation {
  id: number;
  type: "direct" | "group" | string;
  direct_key?: string;
  title?: string;
  avatar_url?: string;
  owner_id?: number;
  member_count?: number;
  last_message_id?: number;
  last_message_at?: string;
  last_message?: Message;
  last_read_message_id?: number;
  unread_count?: number;
  unread_revision?: number;
  members?: ConversationMember[];
}

unread_revision 只用于前端判断 conversation.unread.changed 是否过期,不需要回传服务端。

Message ​

ts
interface Message {
  id: number;
  conversation_id: number;
  sender_id: number;
  client_msg_id?: string;
  message_type?: "text" | "image" | "video" | "file" | string;
  content?: {
    text?: string;
    url?: string;
    name?: string;
    size?: number;
    [key: string]: unknown;
  };
  status?: "sending" | "normal" | "recalled" | string;
  sent_at?: string;
}

本地临时消息可以额外使用前端字段:

ts
interface LocalMessage extends Message {
  local_status?: "sending" | "failed";
}

不要把前端临时字段提交给后端。

Presence ​

ts
interface Presence {
  user_id: number;
  online: boolean;
  connection_count: number;
  last_active_at?: string;
  last_seen_at?: string;
  revision: number;
}

revision 只用于前端判断 presence.changed 是否过期,不需要回传服务端。

HTTP 接口接入 ​

认证 ​

text
POST /api/auth/register
POST /api/auth/login
POST /api/auth/external
GET  /api/me

前端启动时建议:

  1. 从本地读取 token 和 user。
  2. 有 token 时调用 GET /api/me 校验登录态。
  3. 校验失败时清理登录态并跳转登录页。

会话 ​

text
GET    /api/conversations
POST   /api/conversations/detail
POST   /api/conversations/direct
POST   /api/conversations/groups
PATCH  /api/conversations/profile
POST   /api/conversations/members
DELETE /api/conversations/members
POST   /api/conversations/leave
PATCH  /api/conversations/members/role

单聊创建请求:

json
{
  "target_user_id": 2
}

群聊创建请求:

json
{
  "title": "项目群",
  "avatar_url": "",
  "member_ids": [2, 3]
}

会话详情请求:

json
{
  "conversation_id": 1
}

修改群资料请求:

json
{
  "conversation_id": 1,
  "title": "新群名",
  "avatar_url": ""
}

添加群成员请求:

json
{
  "conversation_id": 1,
  "user_ids": [2, 3]
}

移除群成员请求:

json
{
  "conversation_id": 1,
  "user_id": 2
}

退出群聊请求:

json
{
  "conversation_id": 1
}

修改成员角色请求:

json
{
  "conversation_id": 1,
  "user_id": 2,
  "role": "admin"
}

前端点击用户发起单聊时:

  1. 调用 POST /api/conversations/direct。
  2. 使用返回的 conversation.id 进入会话页。
  3. 刷新会话列表。

历史消息 ​

text
POST /api/conversations/messages/list

请求体:

json
{
  "conversation_id": 1,
  "before_id": 1001,
  "limit": 20
}

首次进入会话:

  1. 调用会话详情接口。
  2. 调用消息列表接口,limit 建议 20 或 50。
  3. 按 id 升序渲染消息。
  4. 服务端历史消息接口只返回 status = "normal" 的消息,已撤回消息不会出现在列表中。

上拉加载更多:

  1. 取当前最早消息 id 作为 before_id。
  2. 拉取更早消息。
  3. 合并时按 id 去重。

HTTP 发送消息 ​

text
POST /api/conversations/messages

该接口只作为调试、脚本发送、WebSocket 不可用时的降级路径。聊天页面发送消息首选 WebSocket message.send。

降级发送时必须复用同一个 client_msg_id:

json
{
  "conversation_id": 1,
  "client_msg_id": "web-uuid-001",
  "message_type": "text",
  "content": {
    "text": "hello"
  }
}

撤回消息 ​

text
POST /api/messages/recall

撤回是软撤回:服务端不会删除 messages.content,只把消息状态更新为 recalled。历史消息、消息搜索、未读统计和会话最后消息都只展示正常消息,因此撤回后该消息会从接口和页面中隐藏。

请求体:

json
{
  "message_id": 1001
}

响应:

json
{
  "code": 200,
  "data": null,
  "message": "消息已撤回"
}

前端建议:

  • 只允许当前用户对自己发送且 status = "normal" 的消息展示撤回入口。
  • PC 端通过右键消息气泡弹出确认框。
  • 移动端通过长按消息气泡弹出确认框。
  • 用户确认后再调用接口;成功后从当前消息列表和搜索结果中移除该消息,并刷新会话列表。

消息搜索 ​

text
POST /api/conversations/messages/search
POST /api/messages/search

会话内搜索请求:

json
{
  "conversation_id": 1,
  "keyword": "hello",
  "limit": 20
}

当前用户全部消息搜索请求:

json
{
  "keyword": "hello",
  "limit": 20
}

消息搜索只返回 status = "normal" 的文本消息,已撤回消息不会出现在搜索结果中。

WebSocket 接入 ​

建连 ​

text
GET /api/ws?token={jwt}&device_id={device_id}

前端建议在登录态确认后立即建立 WebSocket。断线后可以重连,但要避免多个页面重复创建多条连接;如果允许多标签页同时在线,也需要确保每个标签页有独立连接或统一由 BroadcastChannel 协调。

事件信封 ​

所有 WebSocket 消息使用统一 JSON:

json
{
  "type": "event.name",
  "request_id": "req-001",
  "payload": {}
}

字段说明:

  • type:事件类型
  • request_id:客户端生成,请求类事件建议必填
  • payload:事件数据

心跳 ​

客户端定时发送:

json
{
  "type": "ping",
  "request_id": "ping-001"
}

服务端返回:

json
{
  "type": "pong",
  "request_id": "ping-001",
  "payload": {
    "server_time": "2026-07-02T12:00:00+08:00"
  }
}

建议前端每 25 到 30 秒发送一次 ping。服务端 75 秒没有收到任何事件会主动回收连接;如果连续多次没有收到 pong 或连接进入 closed/error,进入重连流程。

发送消息 ​

聊天页面发送消息必须优先使用 WebSocket:

json
{
  "type": "message.send",
  "request_id": "req-001",
  "payload": {
    "conversation_id": 1,
    "client_msg_id": "web-uuid-001",
    "message_type": "text",
    "content": {
      "text": "hello"
    }
  }
}

前端发送流程:

  1. 生成 client_msg_id。
  2. 在当前会话插入本地 sending 消息。
  3. WebSocket 已连接时发送 message.send。
  4. 不要同时调用 HTTP 发送接口。
  5. 等待 message.ack。
  6. 超时或 WebSocket 不可用时,使用同一个 client_msg_id 调用 HTTP 发送接口降级。

message.ack ​

服务端收到 message.send 并成功入库后,向发送连接返回:

json
{
  "type": "message.ack",
  "request_id": "req-001",
  "payload": {
    "id": 1001,
    "conversation_id": 1,
    "sender_id": 2,
    "client_msg_id": "web-uuid-001",
    "message_type": "text",
    "content": {
      "text": "hello"
    },
    "status": "normal",
    "sent_at": "2026-07-02T12:00:00+08:00"
  }
}

前端处理:

  1. 用 payload.client_msg_id 找到本地 sending 消息。
  2. 替换为服务端返回的正式消息。
  3. 如果找不到本地消息,按 id 去重后追加。
  4. 刷新会话列表中的最后消息。

message.deliver ​

服务端向会话 active 成员的在线连接广播:

json
{
  "type": "message.deliver",
  "payload": {
    "id": 1001,
    "conversation_id": 1,
    "sender_id": 2,
    "client_msg_id": "web-uuid-001",
    "message_type": "text",
    "content": {
      "text": "hello"
    },
    "status": "normal",
    "sent_at": "2026-07-02T12:00:00+08:00"
  }
}

前端处理:

  1. 按 payload.id 去重。
  2. 如果 payload.conversation_id 是当前会话,追加到消息列表。
  3. 如果不是当前会话,只更新会话列表的最后消息;未读数等待 conversation.unread.changed 覆盖。
  4. 如果收到的是自己发送的消息,也按 id/client_msg_id 去重,避免 ack 和 deliver 重复展示。

presence.changed ​

用户的 WebSocket 连接数量发生变化时,服务端向所有在线连接广播:

json
{
  "type": "presence.changed",
  "payload": {
    "user_id": 2,
    "online": true,
    "connection_count": 2,
    "last_active_at": "2026-07-26T12:00:00+08:00",
    "last_seen_at": "2026-07-26T12:00:00+08:00",
    "revision": 15
  }
}

同一个用户可以有多条连接。关闭其中一条连接只会减少 connection_count,最后一条连接关闭后 online 才变为 false。前端按 user_id 保存状态,并只应用 revision 不小于本地值的增量事件。

conversation.unread.changed ​

新消息入库或当前用户标记已读后,服务端向该用户的所有在线连接推送权威未读状态:

json
{
  "type": "conversation.unread.changed",
  "payload": {
    "conversation_id": 1,
    "unread_count": 3,
    "last_message_id": 1004,
    "last_read_message_id": 1001,
    "last_read_at": "2026-07-26T12:00:00+08:00",
    "revision": 2005
  }
}

前端必须用 payload.unread_count 替换本地会话数量,不要自行执行 +1/-1。顶部总未读数由会话 Store 中所有 unread_count 求和。只应用 revision 不小于当前会话 unread_revision 的事件。

注意:message.deliver 与 conversation.unread.changed 是两个独立事件。Redis 多实例部署时它们位于不同 Pub/Sub channel,客户端不能依赖二者严格同序到达;正确做法是 message.deliver 只负责消息和最后消息展示,未读数始终以后端下发的 conversation.unread.changed 为准。

conversation.changed ​

创建群聊、添加或移除成员、成员退出、角色变化、群资料变化及消息撤回后,服务端向受影响用户推送会话失效通知:

json
{
  "type": "conversation.changed",
  "payload": {
    "conversation_id": 16,
    "change_type": "members"
  }
}

change_type 当前为 members、profile 或 message。客户端收到事件后重新获取会话列表;如果目标会话当前已打开,再重新获取会话详情。被移除或退出的用户也会收到一次通知,用于及时从本地列表移除无权访问的会话。message 表示会话内消息可见性变化,例如消息被软撤回;客户端应重新拉取历史消息或移除本地对应消息,避免继续展示已撤回内容。

error ​

发送失败时,服务端返回:

json
{
  "type": "error",
  "request_id": "req-001",
  "payload": {
    "message": "参数校验失败"
  }
}

前端处理:

  1. 找到对应 request_id 或 client_msg_id 的本地消息。
  2. 标记为 failed。
  3. 提供重试入口。

聊天页面数据流 ​

初始化 ​

  1. 校验登录态。
  2. 获取当前用户 GET /api/me。
  3. 建立唯一 WebSocket,开始暂存实时事件。
  4. 获取全部用户及在线状态 POST /api/users/presence/batch,请求体传 {"type":"all"}。
  5. 获取会话列表 GET /api/conversations。
  6. 写入 HTTP 快照后,再按 revision 应用暂存的实时事件。
  7. 如果前端路由状态中带 conversation_id,通过 body 传参加载会话详情和历史消息。

用户列表查询参数:

参数说明
all=true读取全部用户,适合通讯录初始化
limit分页条数,不传时默认 50,最大 500
offset分页偏移
keyword搜索用户名、昵称或外部 ID
status用户状态,1 正常,0 禁用
auth_source用户来源,local 或 external

选择会话 ​

  1. 设置当前 conversation_id。
  2. 拉取会话详情。
  3. 拉取历史消息。
  4. 调用已读接口更新会话已读游标。

已读接口:

text
POST /api/conversations/read

请求体:

json
{
  "conversation_id": 1,
  "last_read_message_id": 1001
}

响应会同时返回 unread_count、last_message_id 和 revision;服务端还会向当前用户其它标签页或设备推送 conversation.unread.changed。

发送文本消息 ​

  1. 校验当前会话存在。
  2. 校验输入文本去空格后非空。
  3. 构造 client_msg_id。
  4. 本地追加 sending 消息。
  5. WebSocket 发送 message.send。
  6. 收到 message.ack 后替换本地消息。
  7. 收到 message.deliver 后按 id 去重。

离线补偿 ​

用户重新进入页面或 WebSocket 重连后:

  1. 刷新会话列表和批量在线状态,HTTP 快照直接覆盖旧状态。
  2. 对未读会话展示 unread_count,并保存 unread_revision。
  3. 用户进入会话时拉取历史消息。
  4. 历史消息和实时消息按 id 去重合并。
  5. 页面重新可见时执行同样校准;需要更快识别服务实例崩溃时,可以每 60 秒低频刷新一次在线状态。

设备上报 ​

接口:

text
POST   /api/devices
GET    /api/devices
DELETE /api/devices

上报请求:

json
{
  "user_id": 1,
  "data": {
    "platform": "web",
    "device_id": "web-001",
    "app_version": "1.0.0"
  }
}

data 只要求是合法 JSON,内部字段由客户端自行决定。user_id 必须是当前登录用户 ID,不能替其它用户上报。

在线状态 ​

接口:

text
POST /api/users/presence
POST /api/users/presence/batch

单个用户查询请求:

json
{
  "user_id": 2
}

在线人员查询通过 type 选择范围:

json
{
  "type": "all"
}

type 枚举:

  • all:全部人员
  • online:在线人员
  • offline:离线人员
  • specified:指定人员,此时需要同时传 user_ids

查询指定人员:

json
{
  "type": "specified",
  "user_ids": [1, 2, 3]
}

为兼容旧客户端,只传 user_ids 时也按 specified 处理。

批量接口的每一项同时包含用户资料和在线状态,前端无需再根据 user_id 二次查询用户。例如:

json
{
  "id": 1,
  "user_id": 1,
  "username": "alice",
  "nickname": "Alice",
  "avatar_url": "https://example.com/avatar.png",
  "auth_source": "local",
  "status": 1,
  "online": true,
  "connection_count": 1,
  "revision": 10
}

前端建议:

  • 人员列表初始化时传 type=all,一次读取所有用户及在线状态
  • 会话列表或成员列表加载后可传 type=specified 批量查询指定成员
  • WebSocket 连接状态只表示“当前用户是否连上服务端”
  • presence 接口表示“目标用户是否在线”
  • presence.changed 负责正常上下线时的实时增量更新,HTTP presence 接口负责初始化和重连校准

消息回执 ​

接口:

text
POST /api/messages/receipts
POST /api/messages/read
POST /api/messages/unread

请求体:

json
{
  "message_id": 1001
}

前端建议:

  • 用户进入会话并读到消息后,调用 POST /api/messages/read 标记单条消息已读。
  • 用户需要把消息重新标为未读时,调用 POST /api/messages/unread。
  • 展示某条消息已读/未读详情时,调用 POST /api/messages/receipts。

资源消息 ​

上传接口:

text
POST /api/resources/upload

上传成功后返回资源 URL。资源上传接口不会自动发送消息;前端需要再通过 WebSocket message.send 发送图片、视频或文件消息。

图片消息示例:

json
{
  "type": "message.send",
  "request_id": "req-image-001",
  "payload": {
    "conversation_id": 1,
    "client_msg_id": "web-image-001",
    "message_type": "image",
    "content": {
      "url": "/api/static/uploads/image.png"
    }
  }
}

前端生成检查清单 ​

生成前端项目时,至少实现:

  • 登录、注册、退出
  • token 持久化和 GET /api/me 校验
  • HTTP client 统一处理 code/data/message
  • WebSocket 建连、心跳、重连和关闭
  • WebSocket message.send
  • message.ack 替换本地发送中消息
  • message.deliver 实时追加和去重
  • POST /api/messages/recall 消息软撤回确认弹框和本地隐藏
  • HTTP 发送消息降级逻辑
  • 会话列表、单聊创建、群聊创建
  • 会话详情、历史消息分页
  • 当前会话已读上报
  • 在线状态批量查询
  • 上传资源后通过 WebSocket 发送资源消息

常见问题 ​

为什么发送方看到了消息,接收方没及时看到? ​

优先检查前端是否仍在只调用 HTTP 发送接口。聊天页面应该使用 WebSocket message.send。HTTP 发送接口只作为降级路径。

为什么消息重复出现? ​

通常是同时调用了 WebSocket message.send 和 HTTP 发送接口,或者没有按 id/client_msg_id 去重。一次用户发送操作只应走一条发送路径。

WebSocket 断开后怎么办? ​

前端应展示连接状态,并启动重连。重连期间发送消息可以先保留在本地 sending 状态;超时后再使用同一个 client_msg_id 走 HTTP 降级发送。

基于 MIT 许可发布