前端对接文档
本文面向后续生成或重构前端项目使用,目标是让前端按同一套接口、类型和实时协议接入 flow-talk-server。
完整 HTTP 接口以 openapi.json 为准;本文重点补充前端实现时需要的页面数据流、状态管理和 WebSocket 接入规则。
基础配置
服务地址
本地开发默认后端地址:
http://127.0.0.1:8080WebSocket 地址由 HTTP 地址转换得到:
ws://127.0.0.1:8080/api/ws?token={jwt}&device_id={device_id}推荐前端使用环境变量:
VITE_API_BASE_URL=http://127.0.0.1:8080如果使用其它框架,变量名可按框架约定调整,但建议统一封装为 API_BASE_URL。
通用响应格式
HTTP 接口统一返回:
{
"code": 200,
"data": {},
"message": "操作成功"
}前端封装请求时建议:
code === 200:返回datacode !== 200:抛出带message的业务错误- HTTP
401:清理本地登录态并跳转登录页 - HTTP
500:展示通用错误文案,并查看后端日志定位原因
鉴权
登录或注册成功后,后端返回:
{
"user": {
"id": 1,
"username": "alice",
"nickname": "Alice",
"auth_source": "local",
"status": 1
},
"token": "<jwt-token>"
}前端需要保存:
token:HTTPAuthorization和 WebSocket 建连使用user:当前用户基础信息device_id:浏览器本地生成并持久化,例如web-{uuid}
HTTP 鉴权头:
Authorization: Bearer <jwt-token>WebSocket 鉴权参数:
/api/ws?token=<jwt-token>&device_id=<device_id>推荐前端模块
建议生成前端项目时拆成以下模块:
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
interface User {
id: number;
username: string;
nickname?: string;
avatar_url?: string;
auth_source?: string;
status: number;
}Conversation
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
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;
}本地临时消息可以额外使用前端字段:
interface LocalMessage extends Message {
local_status?: "sending" | "failed";
}不要把前端临时字段提交给后端。
Presence
interface Presence {
user_id: number;
online: boolean;
connection_count: number;
last_active_at?: string;
last_seen_at?: string;
revision: number;
}revision 只用于前端判断 presence.changed 是否过期,不需要回传服务端。
HTTP 接口接入
认证
POST /api/auth/register
POST /api/auth/login
POST /api/auth/external
GET /api/me前端启动时建议:
- 从本地读取
token和user。 - 有 token 时调用
GET /api/me校验登录态。 - 校验失败时清理登录态并跳转登录页。
会话
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单聊创建请求:
{
"target_user_id": 2
}群聊创建请求:
{
"title": "项目群",
"avatar_url": "",
"member_ids": [2, 3]
}会话详情请求:
{
"conversation_id": 1
}修改群资料请求:
{
"conversation_id": 1,
"title": "新群名",
"avatar_url": ""
}添加群成员请求:
{
"conversation_id": 1,
"user_ids": [2, 3]
}移除群成员请求:
{
"conversation_id": 1,
"user_id": 2
}退出群聊请求:
{
"conversation_id": 1
}修改成员角色请求:
{
"conversation_id": 1,
"user_id": 2,
"role": "admin"
}前端点击用户发起单聊时:
- 调用
POST /api/conversations/direct。 - 使用返回的
conversation.id进入会话页。 - 刷新会话列表。
历史消息
POST /api/conversations/messages/list请求体:
{
"conversation_id": 1,
"before_id": 1001,
"limit": 20
}首次进入会话:
- 调用会话详情接口。
- 调用消息列表接口,
limit建议 20 或 50。 - 按
id升序渲染消息。 - 服务端历史消息接口只返回
status = "normal"的消息,已撤回消息不会出现在列表中。
上拉加载更多:
- 取当前最早消息
id作为before_id。 - 拉取更早消息。
- 合并时按
id去重。
HTTP 发送消息
POST /api/conversations/messages该接口只作为调试、脚本发送、WebSocket 不可用时的降级路径。聊天页面发送消息首选 WebSocket message.send。
降级发送时必须复用同一个 client_msg_id:
{
"conversation_id": 1,
"client_msg_id": "web-uuid-001",
"message_type": "text",
"content": {
"text": "hello"
}
}撤回消息
POST /api/messages/recall撤回是软撤回:服务端不会删除 messages.content,只把消息状态更新为 recalled。历史消息、消息搜索、未读统计和会话最后消息都只展示正常消息,因此撤回后该消息会从接口和页面中隐藏。
请求体:
{
"message_id": 1001
}响应:
{
"code": 200,
"data": null,
"message": "消息已撤回"
}前端建议:
- 只允许当前用户对自己发送且
status = "normal"的消息展示撤回入口。 - PC 端通过右键消息气泡弹出确认框。
- 移动端通过长按消息气泡弹出确认框。
- 用户确认后再调用接口;成功后从当前消息列表和搜索结果中移除该消息,并刷新会话列表。
消息搜索
POST /api/conversations/messages/search
POST /api/messages/search会话内搜索请求:
{
"conversation_id": 1,
"keyword": "hello",
"limit": 20
}当前用户全部消息搜索请求:
{
"keyword": "hello",
"limit": 20
}消息搜索只返回 status = "normal" 的文本消息,已撤回消息不会出现在搜索结果中。
WebSocket 接入
建连
GET /api/ws?token={jwt}&device_id={device_id}前端建议在登录态确认后立即建立 WebSocket。断线后可以重连,但要避免多个页面重复创建多条连接;如果允许多标签页同时在线,也需要确保每个标签页有独立连接或统一由 BroadcastChannel 协调。
事件信封
所有 WebSocket 消息使用统一 JSON:
{
"type": "event.name",
"request_id": "req-001",
"payload": {}
}字段说明:
type:事件类型request_id:客户端生成,请求类事件建议必填payload:事件数据
心跳
客户端定时发送:
{
"type": "ping",
"request_id": "ping-001"
}服务端返回:
{
"type": "pong",
"request_id": "ping-001",
"payload": {
"server_time": "2026-07-02T12:00:00+08:00"
}
}建议前端每 25 到 30 秒发送一次 ping。服务端 75 秒没有收到任何事件会主动回收连接;如果连续多次没有收到 pong 或连接进入 closed/error,进入重连流程。
发送消息
聊天页面发送消息必须优先使用 WebSocket:
{
"type": "message.send",
"request_id": "req-001",
"payload": {
"conversation_id": 1,
"client_msg_id": "web-uuid-001",
"message_type": "text",
"content": {
"text": "hello"
}
}
}前端发送流程:
- 生成
client_msg_id。 - 在当前会话插入本地
sending消息。 - WebSocket 已连接时发送
message.send。 - 不要同时调用 HTTP 发送接口。
- 等待
message.ack。 - 超时或 WebSocket 不可用时,使用同一个
client_msg_id调用 HTTP 发送接口降级。
message.ack
服务端收到 message.send 并成功入库后,向发送连接返回:
{
"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"
}
}前端处理:
- 用
payload.client_msg_id找到本地sending消息。 - 替换为服务端返回的正式消息。
- 如果找不到本地消息,按
id去重后追加。 - 刷新会话列表中的最后消息。
message.deliver
服务端向会话 active 成员的在线连接广播:
{
"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"
}
}前端处理:
- 按
payload.id去重。 - 如果
payload.conversation_id是当前会话,追加到消息列表。 - 如果不是当前会话,只更新会话列表的最后消息;未读数等待
conversation.unread.changed覆盖。 - 如果收到的是自己发送的消息,也按
id/client_msg_id去重,避免 ack 和 deliver 重复展示。
presence.changed
用户的 WebSocket 连接数量发生变化时,服务端向所有在线连接广播:
{
"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
新消息入库或当前用户标记已读后,服务端向该用户的所有在线连接推送权威未读状态:
{
"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
创建群聊、添加或移除成员、成员退出、角色变化、群资料变化及消息撤回后,服务端向受影响用户推送会话失效通知:
{
"type": "conversation.changed",
"payload": {
"conversation_id": 16,
"change_type": "members"
}
}change_type 当前为 members、profile 或 message。客户端收到事件后重新获取会话列表;如果目标会话当前已打开,再重新获取会话详情。被移除或退出的用户也会收到一次通知,用于及时从本地列表移除无权访问的会话。message 表示会话内消息可见性变化,例如消息被软撤回;客户端应重新拉取历史消息或移除本地对应消息,避免继续展示已撤回内容。
error
发送失败时,服务端返回:
{
"type": "error",
"request_id": "req-001",
"payload": {
"message": "参数校验失败"
}
}前端处理:
- 找到对应
request_id或client_msg_id的本地消息。 - 标记为
failed。 - 提供重试入口。
聊天页面数据流
初始化
- 校验登录态。
- 获取当前用户
GET /api/me。 - 建立唯一 WebSocket,开始暂存实时事件。
- 获取全部用户及在线状态
POST /api/users/presence/batch,请求体传{"type":"all"}。 - 获取会话列表
GET /api/conversations。 - 写入 HTTP 快照后,再按 revision 应用暂存的实时事件。
- 如果前端路由状态中带
conversation_id,通过 body 传参加载会话详情和历史消息。
用户列表查询参数:
| 参数 | 说明 |
|---|---|
all=true | 读取全部用户,适合通讯录初始化 |
limit | 分页条数,不传时默认 50,最大 500 |
offset | 分页偏移 |
keyword | 搜索用户名、昵称或外部 ID |
status | 用户状态,1 正常,0 禁用 |
auth_source | 用户来源,local 或 external |
选择会话
- 设置当前
conversation_id。 - 拉取会话详情。
- 拉取历史消息。
- 调用已读接口更新会话已读游标。
已读接口:
POST /api/conversations/read请求体:
{
"conversation_id": 1,
"last_read_message_id": 1001
}响应会同时返回 unread_count、last_message_id 和 revision;服务端还会向当前用户其它标签页或设备推送 conversation.unread.changed。
发送文本消息
- 校验当前会话存在。
- 校验输入文本去空格后非空。
- 构造
client_msg_id。 - 本地追加
sending消息。 - WebSocket 发送
message.send。 - 收到
message.ack后替换本地消息。 - 收到
message.deliver后按id去重。
离线补偿
用户重新进入页面或 WebSocket 重连后:
- 刷新会话列表和批量在线状态,HTTP 快照直接覆盖旧状态。
- 对未读会话展示
unread_count,并保存unread_revision。 - 用户进入会话时拉取历史消息。
- 历史消息和实时消息按
id去重合并。 - 页面重新可见时执行同样校准;需要更快识别服务实例崩溃时,可以每 60 秒低频刷新一次在线状态。
设备上报
接口:
POST /api/devices
GET /api/devices
DELETE /api/devices上报请求:
{
"user_id": 1,
"data": {
"platform": "web",
"device_id": "web-001",
"app_version": "1.0.0"
}
}data 只要求是合法 JSON,内部字段由客户端自行决定。user_id 必须是当前登录用户 ID,不能替其它用户上报。
在线状态
接口:
POST /api/users/presence
POST /api/users/presence/batch单个用户查询请求:
{
"user_id": 2
}在线人员查询通过 type 选择范围:
{
"type": "all"
}type 枚举:
all:全部人员online:在线人员offline:离线人员specified:指定人员,此时需要同时传user_ids
查询指定人员:
{
"type": "specified",
"user_ids": [1, 2, 3]
}为兼容旧客户端,只传 user_ids 时也按 specified 处理。
批量接口的每一项同时包含用户资料和在线状态,前端无需再根据 user_id 二次查询用户。例如:
{
"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 接口负责初始化和重连校准
消息回执
接口:
POST /api/messages/receipts
POST /api/messages/read
POST /api/messages/unread请求体:
{
"message_id": 1001
}前端建议:
- 用户进入会话并读到消息后,调用
POST /api/messages/read标记单条消息已读。 - 用户需要把消息重新标为未读时,调用
POST /api/messages/unread。 - 展示某条消息已读/未读详情时,调用
POST /api/messages/receipts。
资源消息
上传接口:
POST /api/resources/upload上传成功后返回资源 URL。资源上传接口不会自动发送消息;前端需要再通过 WebSocket message.send 发送图片、视频或文件消息。
图片消息示例:
{
"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 降级发送。