服务版本:server · Go 1.27 · LiveKit | PostgreSQL | Redis
访问 {{ baseURL }} 同样可打开本页(等价 / 与 /docs)。
单二进制、前后端分离。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。
GET /ws?token=<JWT>(token 走 query,不用 Header)。升级成功后服务端先推一条 ready 信封。底层心跳由 gorilla/websocket 负责(服务端 25s ping / 60s pong 超时)。单条入站消息上限 64KB。
{
"rid": "req-1", // 请求 ID(入站请求带,出站服务器推送不带)
"type": "msg.send", // 业务类型
"data": { ... }, // 业务负载
"ts": 1720000000000 // 可选时间戳
}
对带 rid 的入站请求,服务端回 ack 信封,ok 标识成败:
| 字段 | 说明 |
|---|---|
type | 固定 ack |
rid | 回显请求 rid |
ok | true / false |
code/msg | 失败时返回业务错误码与说明(成功可省略) |
data | 成功时可选负载 |
所有入站请求建议携带 rid 以接收 ack;不理会匹配类型的信封数据会被静默忽略。
id/seq/conversation_id;消息落库后推 msg.new 给会话内全员。data: { "conversation_id":1001, "type":"text", "body":"你好",
"content":{...}, "client_msg_id":"c-1", "reply_to":0 }
text;文件 / 图片用 image,content 携带 asset_id。client_msg_id 用于幂等去重(去重场景必填,缺失报 40001)。data: { "conversation_id":1001, "message_id":500 }
data: { "conversation_id":1001, "message_id":500 }
40302。data: { "message_id":500 }
data: { "conversation_id":1001, "is_typing":true }
{"status":{uid:bool}}。data: { "user_ids":[ 1, 2, 3 ] }
连接建立时先收 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 |
统一响应:成功 { "data": ... };失败 { "error": { "code": 40000, "message": "..." } }。POST/PUT/PATCH 请求体为 JSON。除标注外,接口均需鉴权。
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 }
40102。device 可选(缺省跳过设备登记)。{ "username":"alice","password":"123456","device":{ ... } }
// data = { "token","user","device_id" }{ "ok": true }。data = { id,username,nickname,bio }。data = { items:[{id,username,nickname,bio}], total, page }。{ "ok": true }。dir 缺省为 incoming;outgoing 查发出的。返回待处理申请:items:[{id,from_user_id,to_user_id,message,status}]。err 冲突提示见错误码。{ "to_user_id":2, "message":"认识一下" }
// data = { "request_id": 31, "status": "pending" }{ "ok": true }。{ "ok": true }。items:[{id,username,nickname,bio,remark}]。{ "ok": true }。{ "remark": "" }。{ "blocked_id": 2 }。已互拉黑报 40310。items:[{id,username,nickname}]。{ "ok": true }。{ "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 }{ "user_id": 2 },返回会话对象。对方不存在报 40404。{ "title":"xx群", "member_ids":[2,3] };创建者即 owner,初始成员收 conv.new。{ "title":?,"notice":? },广播 conv.update。before / after 按消息 ID 游标。返回 items:[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 }{ "message_id": 500 }。等价 WS msg.read(REST 侧不推送回执)。40302。等价 WS msg.recall。items:[{user_id,role,username,nickname,bio}]。{ "member_ids":[3,4] }。新成员收 conv.new,现有成员收 conv.member_update。40930)。{ "muted":?, "pinned":? }(均为可选 bool)。{ "user_id": 3 },广播 conv.member_update。file,上限 20MB(超限 41300)。图片自动生成缩略图。返回资产对象。{ "id":90001,"category":"image","mime":"image/jpeg","size":12345,
"width":1080,"height":1920,"has_thumb":true,"status":"ready" }{ "callee_id":2, "media_type":"audio"|"video" }。对方离线 40920、忙线 40921。被叫收 call.invite。{ "call_id":"<uuid>","room":"call-<callId>","conversation_id":1 }{ "token":"<lk-jwt>","url":"ws://localhost:7880","room":"call-<callId>" }items:[{call_id,conversation_id,room,caller_id,callee_id,media_type,status,answered}];status ∈ ringing/accepted/rejected/canceled/timeout/ended。room-<convID>。{ "token":"<lk-jwt>","url":"ws://localhost:7880","room":"room-123" }data = { user_ids:[1,2], count:2 }。degraded。HTTP 状态码与业务错误码 code 对照如下;错误体统一 {"error":{code,message}}。
| code | HTTP | 含义 |
|---|---|---|
| 40000 | 400 | 请求格式错误 / 缺参(通用) |
| 40001 | 400 | 参数不合法(如用户名位数、media_type) |
| 40010 | 400 | 不能添加自己为好友 |
| 40020/40021 | 400 | 请用退群接口 / 不能转让给自己 |
| 40100/40101 | 401 | 缺少登录凭证 / 登录已失效 |
| 40102 | 401 | 用户名或密码错误 |
| 40301 | 403 | 不是该会话 / 群成员 |
| 40302 | 403 | 不能撤回(超时或非本人) |
| 40303 | 403 | 已被对方拉黑(发消息) |
| 40310 | 403 | 已被对方拉黑(好友申请) |
| 40320 | 403 | 通话非本方成员操作 |
| 40330/40331 | 403 | 需要管理员权限 / 仅群主可转让 |
| 40404 | 404 | 用户 / 会话 / 设备 / 文件不存在 |
| 40410 | 404 | 目标用户不存在(好友申请) |
| 40901 | 409 | 用户名已存在 |
| 40910/40911 | 409 | 已是好友 / 申请已存在 |
| 40920/40921 | 409 | 对方离线 / 忙线 |
| 40922/40923 | 409 | 通话未在振铃 / 通话已结束 |
| 40930 | 409 | 请先转让群主再退群 |
| 41300 | 413 | 文件过大(上限 20MB) |
| 40404 / 未知 | 400-404 | WS 未知消息类型(ack) |
| 50000 | 500 | 系统错误(通用) |
单二进制 cmd/anytalk,环境变量驱动(默认以 development 启动,生产须覆盖默认密钥,见 config.Validate):
| 变量 | 默认 | 说明 |
|---|---|---|
| LISTEN_ADDR | :8080 | HTTP 监听地址 |
| APP_ENV | development | development / production |
| DATABASE_URL | local pg | PostgreSQL 连接串 |
| REDIS_ADDR | localhost:6379 | Redis 地址 |
| JWT_SECRET | dev 默认 | 应用登录签名密钥(生产必改) |
| LIVEKIT_URL / API_KEY / API_SECRET | dev 默认 | LiveKit 媒体面与 webhook 验签 |
| STORAGE_LOCAL_DIR | ./data | 开发期本地文件存储目录 |
| SNOWFLAKE_NODE | 1 | 雪花节点号(0-1023,生产必填) |
注:所有 ID 为 64 位雪花 ID(最长 18 位),超出浮点安全范围,客户端务必以 Int64 / Long 解析。