资讯动态

一个的即时通讯项目是怎么长出来的:(Spring Boot 3 + Vue 3 微前端 + 悟空 IM + LiveKit)

发布时间:2026/10/22 13:09:59 来源:尧图企业网站定制
项目飞鸽即时通讯一套包含单聊、群聊、音视频通话、AI 助手、管理后台、桌面客户端的完整 IM 系统。 后端 Spring Boot 3 多模块前端 Vue 3 qiankun 微前端媒体面自建 LiveKit SFU客户端除 Web 外还有 Tauri 桌面端。这篇文章不复述功能清单写的是为什么这么选、哪里差点走错、上线这一段又教会了我什么。线上地址https://www.bwyxy.cn/chat/0. 项目全貌一句话概括用一套系统把微前端 IM 实时音视频 AI 桌面端这几件事串起来而不是做五个 Demo。维度规模后端模块8 个 Maven 模块common / framework / system / admin / ai / chat / app / blog前端3 个子应用主应用基座 聊天 管理后台 2 个公共包pnpm workspace客户端Webqiankun 微前端 Tauri 桌面端服务端口9070 AI、9080 聊天、9090 管理后台、9100 Todo前端 5000 / 9001 / 9002数据库MySQL 8 Redis缓存、分布式锁、限流后端技术栈Spring Boot 3.2.4 / JDK 17 / Spring Security 6 JWT / MyBatis-Plus / MySQL 8 / Redis / Bucket4j / Spring AI。 前端技术栈Vue 3 TypeScript / Vite / qiankun / Pinia / Element Plus /easyjssdkIM/livekit-client音视频。xy-service (parent) ├── xy-common # 公共实体、枚举、Feign Client、异常 ├── xy-framework # 安全、JWT、全局异常、限流、Web 配置 ├── xy-system # 用户、权限、菜单、七牛云、悟空 IM Client ├── xy-admin # 管理后台 API :9090 ├── common-api/xy-ai # AI 能力层对话/RAG/Agent :9070 ├── chat-api/fg-chat # 聊天主 API含音视频通话 :9080 └── customer-api # xy-app :9100 / xy-blog1. 四个关键决策项目里真正花时间想清楚的不是怎么写代码而是下面四个选 A 还是选 B。1.1 IM 底座自研还是用现成的最后选了悟空 IMWuKongIM只做几件事频道channel模型、WebSocket 长连接、离线消息、多端同步。判断依据很简单IM 的最后一公里多端同步、离线补偿、消息序号、已读位点是脏活累活但它不是这个项目的差异化价值。把时间花在这上面就没有时间做音视频、AI 和微前端了。不过这只是阶段性的务实取舍不是终点。用现成 IM 的代价很明确协议怎么走、序号怎么长、离线消息怎么补、多端位点怎么维护这些都在别人的代码里——想改只能等上游想扩展只能绕过去。所以长连接的自主实现是我接下来最想投入的方向见第 5 节。1.2 发消息为什么绕一层自己的后端前端拿到 IM 的 SDK 后最省事的做法是直接sdk.send()。我没有这么做而是统一走POST /message/send由后端转发给悟空 IM。多一跳的代价换来的是消息出口在后端。敏感词、图片审核、频率控制、消息审计、灰度策略这些能力将来只需要在后端加前端一行不改。如果让前端直连 IM 发消息这个位置就永远失去了。1.3 音视频从 P2P 到 SFU 的一次真实演进第一版是1v1 P2P 直连RTCPeerConnection 自建信令当时觉得很优雅不花服务器带宽。坑都记录在《给即时通讯做 1v1 音视频通话我踩的 7 个坑》里。但需求走到群聊音视频时P2P 直接出局方案N 人上行路数我的结论Mesh两两互联N-13 人还行5 人浏览器就开始发热9 人不可能MCU服务端混流1服务端要做解码/混流CPU 成本高不适合单机SFU只转发不混流1选它服务端只转发弱网可分层simulcast人数上限可控最终选定自建 LiveKit SFU并把 1v1 也一起迁到同一套媒体层——统一一套媒体面比同时维护两套要省心得多。房间人数上限设为 9。1.4 呼叫信令开一条新长连接还是复用 IM复用。呼叫控制邀请/接听/拒绝/挂断/取消/结束用悟空 IM 个人频道的200–205 消息类型码传输不新增长连接、不新增一个信令服务器。代价是复用了消息通道就必须回到消息分发器确认语义假设。这一点我在 1v1 那版上栽过——前端有一行type 100 一律当系统通知而通话信令刚好落在 200–208结果一次通话弹几十条系统通知。教训写在那篇博客里新加类型码时都会先回去看一圈分发顺序。最终音视频被拆成四个平面平面职责实现控制面发起/接听/拒绝/挂断、超时、通话记录REST /rtc/*权威落库、可幂等信令面呼叫事件传递复用悟空 IM 200–205媒体面音视频流自建 LiveKit SFU状态面房间与参与者审计t_rtc_room / t_rtc_participant 定时兜底为什么控制面坚持走 REST因为控制指令需要权威和幂等这通电话到底接没接不能有两个答案而信令只是管道里的数据。2. 做得最扎实的三个细节2.1 「仅本机生效」的删除聊天记录微信里删聊天记录对方是不知道的。所以删除这个动作只能作用于当前用户不能去改悟空服务端的消息。如果直接删服务端消息会同时影响对端可见性——这是数据模型层面的错误。我采用的是私有会话标记-- t_conversation_setting cleared_seq -- 清空聊天记录时的最新消息序号 deleted_seq -- 删除会话时的最新消息序号拉历史消息时按 seq 过滤掉 cleared_seq 之前的消息会话列表查询时过滤掉 deleted_seq 的会话收到新消息seq 更大后会话自动重新出现。“删除变成了记一个我看不到的边界”对端数据完好无损而且新消息还能把会话救回来——行为和微信一致实现上只是两个bigint。2.2 群聊通话状态机 三层兜底一次群聊通话会经历发起 → 振铃 → 多人陆续加入 → 有人退出 → 结束。中间任何一环都可能异常前端崩溃、直接关标签页、断网、媒体建连失败。三层兜底幂等结束结束房间时用受影响行数判断UPDATE … WHERE status 通话中只有真正改到的那一次才发通知、写通话记录重复调用返回 0 行直接忽略超长通话单次通话上限 180 分钟定时任务兜底结束异常残留本次补的每 10 秒扫一次通话中房间只要 LiveKit 里已经没人在场、且开始时间已过 60 秒宽限就结束房间并落记录。第 3 条是踩出来的前端异常退出时只清了本地状态服务端房间还停在通话中导致该用户此后发起通话一直被你已在通话中挡住。修法是两头一起做——前端异常分支补发一次挂断上报后端再用媒体面还有没有人做权威判定。LiveKit 不可达时直接跳过扫描避免网络抖动误杀进行中的通话。2.3 消息去重id 靠不住去重最初只看消息id结果还是重复。原因在于同一条消息有三个来源IM 推送的消息真实 id历史拉取的消息服务端 id本地乐观插入的消息临时 idclientMsgNo同一句话在三个来源里的id可能都不一样用id去重必然漏。改成语义身份判定优先按 clientMsgNo 匹配发送方生成的三个来源都一致其次按频道 消息序号匹配兼容服务端把序号序列化成字符串的情况。去重键应该来自消息的业务身份而不是存储层的主键。3. 上线这一段才叫项目本地跑通只证明代码能跑。部署这一天我遇到的每一个问题都不是代码问题。3.1 镜像拉不下来Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled国内服务器直连 Docker Hub 超时。配了腾讯云内网镜像源mirror.ccs.tencentyun.com后正常。3.2 443 端口被占用不要抢要复用我给 LiveKit 配了 Caddy 自动签证书结果容器反复重启listening on :443: listen tcp :443: bind: address already in use排查发现 443 早被服务器上的宝塔 Nginx占着而且上面还跑着别的站点不可能让出来。最后方案是删掉 Caddy复用已有 Nginx 按域名反向代理到 LiveKit 的 7880。两个容易踩的细节location ^~ /livekit/ { proxy_pass http://127.0.0.1:7880/; # 结尾斜杠 剥掉前缀 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # WebSocket 必须缺了会握手失败 proxy_set_header Connection upgrade; }教训服务器上已有入口往往比新起一个入口更划算。抢端口之前先看看那个端口上跑的是什么。3.3 LiveKit 广播了内网 IP最隐蔽的一个坑信令通了、浏览器连上了但怎么都出不了画面控制台只给一句ConnectionError: could not establish pc connection看 LiveKit 日志才找到根因INFO using external IPs {ips: [10.x.x.x/10.x.x.x, 172.17.0.1/172.17.0.1, ...], advertiseInternalIP: false} WARN could not validate external IP {ip: 公网IP, error: context canceled}LiveKit 用 STUN 正确探测到了公网 IP但要再做一次反向校验从本机往自己的公网 IP 发探测包确认可达。云厂商的弹性 IP 通常不支持回环hairpin NAT校验失败 → LiveKit 把公网 IP 从候选列表里剔掉 → 对外只广播10.x.x.x这类内网地址 → 外部客户端的 ICE 永远连不上。修法是显式指定对外公布的 IP跳过探测与校验rtc: tcp_port: 7881 udp_port: 7882 use_external_ip: false node_ip: 你的公网 IP教训NAT 环境下别让服务自己猜自己的公网地址。猜错了它不会报错只是安静地广播一个别人永远到不了的地址。3.4 TCP 通了不等于媒体通了还有个更基础的放行要放两层——云安全组 系统防火墙firewalld。我一开始只放行了 TCPfirewall-cmd --list-ports 20/tcp 21/tcp 22/tcp 80/tcp 443/tcp ... # 一条 UDP 都没有而 WebRTC 媒体优先走 UDPUDP 被静默丢包时 ICE 会长时间卡住最后同样报could not establish pc connection。另外一个实践建议媒体端口别开整段50000-50100/udp用单端口更省事——udp_port: 7882只需要管7881/tcp7882/udp两条规则出问题时排查面小得多。3.5 浏览器能跑 ≠ 桌面端能跑Web 端一切正常同一份前端打包成 Tauri 桌面端后所有接口都访问不了。查出三个独立问题① 打包塞进去的是旧产物。构建输出目录是chat/而 Tauri 的frontendDist指向../dist——新构建写在 A 目录打包读的是 B 目录桌面端里跑的是好几天前的代码。修法就是让两者对齐// vite.config.ts build: { outDir: isTauri ? dist : chat, }② 桌面端天然是跨域。桌面端页面自身的源是tauri://localhostWindows 上是http://tauri.localhost访问后端接口必然跨域。因此后端必须显式放行用户才会遇到下面这个经典的意外——预检请求 OPTIONS 被认证拦截了OPTIONS /chat-api/chat/login Origin: tauri://localhost → 403 Invalid CORS request预检请求不携带 token如果它排在 Spring Security 后面会被判未登录直接 401/403跨域还没开始就结束了。所以跨域处理必须早于安全过滤器链FilterRegistrationBeanCorsFilter registration new FilterRegistrationBean(new CorsFilter(source)); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); // 关键早于 Spring Security 的 -100③ 相对路径会打到自己身上。如果打包时 API 地址为空baseURL为空字符串/chat/login就被解析成tauri://localhost/chat/login——请求打给了应用自己连后端都没碰到。教训Web 端的同源假设在桌面端、小程序、Electron 里全部失效。只要客户端不是浏览器同源加载的CORS、Cookie、混合内容都要重新过一遍。4. 复盘能迁移走的几条先想清楚哪些不做再决定做什么。 IM 最后一公里交给悟空 IM是因为它不构成差异化省下的时间做了音视频、AI 和微前端。状态存在哪里决定了能不能做对。 删除聊天记录做成私有边界而非删数据是因为数据是共享的、而边界是私有的——选错位置功能永远做不对。异步 多来源的场景去重键必须是业务身份。 存储主键在跨来源时不可靠。控制面要幂等兜底要看事实而不是看上报。 前端说我挂了不可信媒体面还有没有人才可信。本地跑通不是项目上线才是。 云主机上的问题是叠加的NAT、三层防火墙、共享 443、镜像源、证书……每一层单独都不难难的是不知道问题在哪一层——先把信令面通不通和媒体面通不通分开验证能省掉大量盲猜。同一个前端不同宿主就是不同环境。 桌面端教我重新过了一遍跨域、产物目录和资源路径。5. 后续规划最想投入的方向是把聊天长连接握在自己手里现在的 IM 底座够稳够快但协议、存储、扩展点都不是自己的——能力长在自己身上才谈得上往里加东西。计划分三步不跳级阶段目标要啃的点1. 看懂能用抓包和日志把一条消息的一生讲清楚连接鉴权、心跳与保活、收发 ACK、上行/下行序号、离线补偿边界2. 自研连接层独立的 WebSocket 网关多端在线 断线重连长连接框架、连接注册表与会话路由、心跳超时踢线、优雅下线与灰度重启3. 补齐可靠语义做到不丢、不重、可补、可读会话级递增序号、离线消息与拉取补洞、多端已读位点、幂等落库围绕这条主线音视频与 AI 侧也在列录制与回放LiveKit 的 Egress 服务落对象存储群聊会议支持录制服务端混流多人通话时的合流输出便于直播旁听场景AI 助手进入会话现在 AI 是独立助手下一步希望它能在群里被 就上车。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑