AnyTalk IM · 后端文档

服务版本:server · Go 1.27 · LiveKit | PostgreSQL | Redis

访问 {{ baseURL }} 同样可打开本页(等价 / 与 /docs)。

功能总览

账号注册 / 登录 / 登出(JWT)、当前用户、设备管理、搜索用户
好友搜索、申请 / 接受 / 拒绝、好友列表、备注、删除、拉黑 / 解除
会话1v1 幂等创建、群创建、群资料修改、成员增删、转让群主、置顶 / 免打扰、未读
消息文本 / 文件 / 图片、送达与已读回执、正在输入、撤回(2 分钟内)
文件multipart 上传(≤20MB)、原图 / 缩略图下载、MIME 识别
通话1v1 音视频(LiveKit)、呼叫 / 振铃 / 接听 / 拒接 / 取消 / 挂断 / 记录
房间群组语音房(LiveKit)、成员进出实时同步、房间结束通知
实时基于 Redis 总线的跨节点推送、在线状态(presence)查询

整体架构

单二进制、前后端分离。HTTP 对外暴露两类接入:REST(请求-响应型:账号、联系人、会话管理、上传、通话控制)与 WebSocket(/ws,实时消息收发与在场/回执/输入中信令)。

存储:PostgreSQL 用户 / 关系 / 会话 / 消息 / 资产元数据;Redis 在线状态、1v1 通话状态机锁、跨节点消息总线与漏斗(fanout)镜像;对象存储后端(S3)自 M4 起接入,当前开发期使用本地磁盘。

媒体面由 LiveKit 承载:call-<callID> 为 1v1 通话房间、room-<convID> 为群语音房。服务端仅签发 token(JWT,LiveKit 私钥签名),音频 / 视频流转发由 LiveKit 完成;房间进出事件通过 LiveKit webhook 回注。

认证

登录 / 注册返回 JWT(HS256,应用密钥),后续请求置于 Authorization: Bearer <token>。首期为无状态 JWT,登出仅由客户端删除本地 token(POST /v1/auth/logout 为预留占位)。

1v1 会话或群均要求请求者属于对应会话成员(40301 不是该会话成员)。分组操作(改资料 / 拉人 / 踢人)需 owner 或 admin;转让仅 owner。

WebSocket 实时协议

连接

GET /ws?token=<JWT>(token 走 query,不用 Header)。升级成功后服务端先推一条 ready 信封。底层心跳由 gorilla/websocket 负责(服务端 25s ping / 60s pong 超时)。单条入站消息上限 64KB。

统一信封(Outbound / Inbound)

{
  "rid":  "req-1",          // 请求 ID(入站请求带,出站服务器推送不带)
  "type": "msg.send",       // 业务类型
  "data": { ... },          // 业务负载
  "ts":   1720000000000     // 可选时间戳
}

应答(Ack)

对带 rid 的入站请求,服务端回 ack 信封,ok 标识成败:

字段说明
type固定 ack
rid回显请求 rid
oktrue / false
code/msg失败时返回业务错误码与说明(成功可省略)
data成功时可选负载

入站请求(客户端 → 服务端)

所有入站请求建议携带 rid 以接收 ack;不理会匹配类型的信封数据会被静默忽略。

发送
msg.send需登录
发送消息到会话,返回 ack.data 含 id/seq/conversation_id;消息落库后推 msg.new 给会话内全员。
data: { "conversation_id":1001, "type":"text", "body":"你好",
       "content":{...}, "client_msg_id":"c-1", "reply_to":0 }
  • type 默认 text;文件 / 图片用 image,content 携带 asset_id。
  • client_msg_id 用于幂等去重(去重场景必填,缺失报 40001)。
  • 推送事件见 服务端推送。
回执
msg.delivered需登录
上报“已送达”游标,通知发送者(msg.receipt · kind=delivered)。
data: { "conversation_id":1001, "message_id":500 }
回执
msg.read需登录
上报“已读”游标,通知同会话其他成员(msg.receipt · kind=read)。
data: { "conversation_id":1001, "message_id":500 }
撤回
msg.recall需登录
撤回自己 2 分钟内的消息,广播 msg.recalled。超时可撤回者或非本人报 40302。
data: { "message_id":500 }
输入态
conv.typing需登录
广播“正在输入”,给同会话其他成员推 msg.typing(不持久化)。
data: { "conversation_id":1001, "is_typing":true }
在线
presence.query需登录
批量查询在线状态,ack.data 返回 {"status":{uid:bool}}。
data: { "user_ids":[ 1, 2, 3 ] }

服务端推送事件(Outbound)

连接建立时先收 ready。以下事件由服务端主动推送,客户端按 type 分发。

type负载要点
ready连接就绪
msg.new 广播新消息:id, conversation_id, sender_id, seq, type, body, content, client_msg_id, reply_to, status, created_at
msg.receipt 广播回执:conversation_id, user_id, kind(delivered|read), message_id
msg.recalled 广播撤回:conversation_id, message_id
msg.typing 广播输入态:conversation_id, user_id, is_typing
conv.new 定向新会话(1v1 对端 / 群初始成员 / 拉入成员):conversation_id, type, title?, peer_id / actor_id
conv.update 群群资料变更:conversation_id, title?, notice?, actor_id
conv.member_update 群成员变化:conversation_id, kind(member_added|member_kicked|member_quitted|owner_transferred), user_ids, actor_id
friend.request 定向收到好友申请:request_id, from_user_id, message
friend.accepted申请被接受:request_id, user_id
friend.rejected申请被拒绝:request_id, user_id
call.invite 被叫收到来电:call_id, caller{id,name}, media, room, expire
call.ringing 主叫被叫振铃回告:call_id
call.accept 主叫被叫接听(带主叫 LiveKit 凭证):call_id, token, url, room
call.end对方挂断:call_id, reason, by
call.reject / call.cancel拒接 / 取消:call_id, by
call.timeout振铃超时:call_id
room.participant_update房间成员进出:conversation_id, room, user_ids, count
room.ended房间结束:conversation_id, room

REST 接口

统一响应:成功 { "data": ... };失败 { "error": { "code": 40000, "message": "..." } }。POST/PUT/PATCH 请求体为 JSON。除标注外,接口均需鉴权。

认证与账号

/v1/auth/register
POST /v1/auth/register
注册并返回 JWT。用户名≥3 位、密码≥6 位。重复用户名报 40901。
请求 / 响应
// 请求
{ "username":"alice","password":"123456","nickname":"爱丽丝",
  "device":{ "install_id":"i-1","platform":"android","model":"pixel","os_version":"14","app_version":"1.0.0","locale":"zh" } }
// 响应 data
{ "token":"<jwt>", "user":{ "id":1,"username":"alice","nickname":"爱丽丝","bio":"" }, "device_id": 11 }
/v1/auth/login
POST /v1/auth/login
登录返回 JWT。错误凭据报 40102。device 可选(缺省跳过设备登记)。
请求 / 响应
{ "username":"alice","password":"123456","device":{ ... } }
// data = { "token","user","device_id" }
/v1/auth/logout
POST /v1/auth/logout需登录
预留;无状态 JWT 无法吊销,客户端删除本地 token。返回 { "ok": true }。
/v1/users/me
GET /v1/users/me需登录
当前用户资料:data = { id,username,nickname,bio }。
/v1/users
GET /v1/users?keyword=&page=需登录
搜索用户(每页 20)。返回 data = { items:[{id,username,nickname,bio}], total, page }。
/v1/devices
GET /v1/devices需登录
当前账号登录设备列表。
/v1/devices/{id}
DELETE /v1/devices/{id}需登录
移除一台设备,返回 { "ok": true }。

好友与黑名单

/v1/friend/requests
GET /v1/friend/requests?dir=需登录
dir 缺省为 incoming;outgoing 查发出的。返回待处理申请:items:[{id,from_user_id,to_user_id,message,status}]。
/v1/friend/requests
POST /v1/friend/requests需登录
发送好友申请。err 冲突提示见错误码。
请求 / 响应
{ "to_user_id":2, "message":"认识一下" }
// data = { "request_id": 31, "status": "pending" }
/v1/friend/requests/{id}/accept
POST /v1/friend/requests/{id}/accept需登录
接受申请(仅接收方),建立双向好友,返回 { "ok": true }。
/v1/friend/requests/{id}/reject
POST /v1/friend/requests/{id}/reject需登录
拒绝申请(仅接收方),返回 { "ok": true }。
/v1/friends
GET /v1/friends需登录
好友列表:items:[{id,username,nickname,bio,remark}]。
/v1/friends/{id}
DELETE /v1/friends/{id}需登录
删除好友(双向),返回 { "ok": true }。
/v1/friends/{id}/remark
PUT /v1/friends/{id}/remark需登录
设置好友备注。请求体 { "remark": "" }。
/v1/blocks
POST /v1/blocks需登录
拉黑用户。请求体 { "blocked_id": 2 }。已互拉黑报 40310。
/v1/blocks
GET /v1/blocks需登录
黑名单列表:items:[{id,username,nickname}]。
/v1/blocks/{id}
DELETE /v1/blocks/{id}需登录
解除拉黑,返回 { "ok": true }。

会话

/v1/conversations
GET /v1/conversations需登录
当前用户的会话列表(含未读 / 置顶 / 免打扰 / 角色)。
响应 data 元素
{ "id":1,"type":"single","title":"","notice":"","owner_id":1,"member_count":2,
  "last_message_id":500,"last_message_at":1720000000000,
  "role":"owner","last_read_message_id":480,"unread_count":2,"muted":false,"pinned":false }
/v1/conversations
POST /v1/conversations需登录
幂等创建 1v1 会话。请求体 { "user_id": 2 },返回会话对象。对方不存在报 40404。
/v1/conversations/{id}
GET /v1/conversations/{id}需登录
会话详情(同上面的对象,不含用户态字段)。
/v1/groups
POST /v1/groups需登录
创建群聊。请求体 { "title":"xx群", "member_ids":[2,3] };创建者即 owner,初始成员收 conv.new。
/v1/conversations/{id}
PATCH /v1/conversations/{id}需登录
改群资料(owner/admin)。请求体 { "title":?,"notice":? },广播 conv.update。
/v1/conversations/{id}/messages
GET /v1/conversations/{id}/messages?before=&after=&limit=需登录
拉历史消息,limit 默认 30、上限 100。before / after 按消息 ID 游标。返回 items:[messageDTO]。
messageDTO
{ "id":500,"conversation_id":1,"sender_id":1,"seq":12,"type":"text",
    "body":"你好","content":{...},"client_msg_id":"c-1","reply_to":0,
    "status":"normal","created_at":1720000000000 }
/v1/conversations/{id}/read
POST /v1/conversations/{id}/read需登录
上报已读游标。请求体 { "message_id": 500 }。等价 WS msg.read(REST 侧不推送回执)。
/v1/messages/{id}/recall
POST /v1/messages/{id}/recall需登录
撤回消息(2 分钟内、仅本人);超时 / 非本人报 40302。等价 WS msg.recall。

群成员 / 设置

/v1/conversations/{id}/members
GET /v1/conversations/{id}/members需登录
成员列表(含基础资料)。客户端据此解析 1v1 对端与群成员昵称。返回 items:[{user_id,role,username,nickname,bio}]。
/v1/conversations/{id}/members
POST /v1/conversations/{id}/members需登录
拉人进群(owner/admin)。请求体 { "member_ids":[3,4] }。新成员收 conv.new,现有成员收 conv.member_update。
/v1/conversations/{id}/members/{uid}
DELETE /v1/conversations/{id}/members/{uid}需登录
踢人(owner/admin,不能踢自己——请用退群)。
/v1/conversations/{id}/members
DELETE /v1/conversations/{id}/members需登录
自己退群。群主退群前必须先转让(否则 40930)。
/v1/conversations/{id}/settings
PATCH /v1/conversations/{id}/settings需登录
置顶 / 免打扰。请求体 { "muted":?, "pinned":? }(均为可选 bool)。
/v1/conversations/{id}/transfer
POST /v1/conversations/{id}/transfer需登录
转让群主(仅 owner)。请求体 { "user_id": 3 },广播 conv.member_update。

文件 / 图片

/v1/assets/upload
POST /v1/assets/upload需登录
multipart 上传,字段名 file,上限 20MB(超限 41300)。图片自动生成缩略图。返回资产对象。
响应 data
{ "id":90001,"category":"image","mime":"image/jpeg","size":12345,
  "width":1080,"height":1920,"has_thumb":true,"status":"ready" }
/v1/assets/{id}
GET /v1/assets/{id}需登录
下载原图 / 原文件(按存储 Content-Type 输出二进制)。
/v1/assets/{id}/thumb
GET /v1/assets/{id}/thumb需登录
下载缩略图(图片类)。

1v1 音视频通话

/v1/calls
POST /v1/calls需登录
发起呼叫。请求体 { "callee_id":2, "media_type":"audio"|"video" }。对方离线 40920、忙线 40921。被叫收 call.invite。
响应 data
{ "call_id":"<uuid>","room":"call-<callId>","conversation_id":1 }
/v1/calls/{id}/ringing
POST /v1/calls/{id}/ringing需登录
被叫回告振铃(收到 invite 后),主叫收 call.ringing。
/v1/calls/{id}/accept
POST /v1/calls/{id}/accept需登录
被叫接听。返回本人 LiveKit 凭证;主叫经 call.accept 推送收凭证。
响应 data
{ "token":"<lk-jwt>","url":"ws://localhost:7880","room":"call-<callId>" }
/v1/calls/{id}/reject
POST /v1/calls/{id}/reject需登录
被叫拒接,主叫收 call.reject。
/v1/calls/{id}/cancel
POST /v1/calls/{id}/cancel需登录
主叫取消,被叫收 call.cancel。
/v1/calls/{id}/end
POST /v1/calls/{id}/end需登录
通话中任一方挂断,对方收 call.end。
/v1/calls
GET /v1/calls?page=需登录
通话记录(每页 20)。返回 items:[{call_id,conversation_id,room,caller_id,callee_id,media_type,status,answered}];status ∈ ringing/accepted/rejected/canceled/timeout/ended。

群语音房

/v1/conversations/{id}/room/join
POST /v1/conversations/{id}/room/join需登录
加入群语音房(须为成员),返回 LiveKit 凭证。房间名 room-<convID>。
响应 data
{ "token":"<lk-jwt>","url":"ws://localhost:7880","room":"room-123" }
/v1/conversations/{id}/room
GET /v1/conversations/{id}/room需登录
当前房间在线参与者:data = { user_ids:[1,2], count:2 }。

系统 / 文档

/v1/health
GET /v1/health
健康检查,含 Postgres / Redis 连通性。依赖故障时返回 503 与 degraded。
/healthz
GET /healthz
同上。
/docs
GET /docs (及 /)
本页面。

错误码

HTTP 状态码与业务错误码 code 对照如下;错误体统一 {"error":{code,message}}。

codeHTTP含义
40000400请求格式错误 / 缺参(通用)
40001400参数不合法(如用户名位数、media_type)
40010400不能添加自己为好友
40020/40021400请用退群接口 / 不能转让给自己
40100/40101401缺少登录凭证 / 登录已失效
40102401用户名或密码错误
40301403不是该会话 / 群成员
40302403不能撤回(超时或非本人)
40303403已被对方拉黑(发消息)
40310403已被对方拉黑(好友申请)
40320403通话非本方成员操作
40330/40331403需要管理员权限 / 仅群主可转让
40404404用户 / 会话 / 设备 / 文件不存在
40410404目标用户不存在(好友申请)
40901409用户名已存在
40910/40911409已是好友 / 申请已存在
40920/40921409对方离线 / 忙线
40922/40923409通话未在振铃 / 通话已结束
40930409请先转让群主再退群
41300413文件过大(上限 20MB)
40404 / 未知400-404WS 未知消息类型(ack)
50000500系统错误(通用)

运行与配置

单二进制 cmd/anytalk,环境变量驱动(默认以 development 启动,生产须覆盖默认密钥,见 config.Validate):

变量默认说明
LISTEN_ADDR:8080HTTP 监听地址
APP_ENVdevelopmentdevelopment / production
DATABASE_URLlocal pgPostgreSQL 连接串
REDIS_ADDRlocalhost:6379Redis 地址
JWT_SECRETdev 默认应用登录签名密钥(生产必改)
LIVEKIT_URL / API_KEY / API_SECRETdev 默认LiveKit 媒体面与 webhook 验签
STORAGE_LOCAL_DIR./data开发期本地文件存储目录
SNOWFLAKE_NODE1雪花节点号(0-1023,生产必填)

注:所有 ID 为 64 位雪花 ID(最长 18 位),超出浮点安全范围,客户端务必以 Int64 / Long 解析。