十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebSocket+Spring Boot+Vue3:从零构建团队协作沟通工具

WebSocket+Spring Boot+Vue3:从零构建团队协作沟通工具 产品经理、设计师、研发、测试几乎每一次业务迭代都依赖跨职能沟通。可一旦项目变多、人员变多消息被淹没在群聊里、决策散落在各种工具中就成了常态。团队需要的往往不是再多一个聊天软件而是一种能围绕项目上下文、减少无关打扰、让异步沟通更顺畅的工作方式。本文会从技术实现的角度拆解一套“产品团队协作沟通工具”的搭建过程。内容包括需求分析、技术选型、后端 WebSocket 实时通信、前端消息收发、数据库设计以及从“可用”到“高效”的体验优化和工程落地建议。无论你是想自己搭一个内部沟通工具还是想理解团队协作类产品的核心技术链路这篇文章都值得读下去。1. 产品团队沟通为什么需要专用工具1.1 传统沟通模式的困境互联网团队最初的协作方式非常朴素一个群聊拉上所有人消息一条接一条。刚开始项目小这种模式确实够用。但当团队扩大到几十人、几百人问题就会集中爆发。第一是信息淹没。一条需求讨论里可能夹杂着打招呼、表情、无关回复真正有效的信息被快速冲走。第二是上下文缺失。新同学加入群聊翻几个小时历史消息也未必能拼凑出完整的决策脉络。第三是通知轰炸。所有消息一视同仁地推给你专注时间被不断切断。还有一个容易被忽略的问题沟通工具和项目工具分离。需求在项目管理平台里讨论代码在代码托管平台里评论日常沟通又在群里进行。信息被割裂成多个孤岛想复盘一个需求的完整过程非常困难。1.2 高效沟通工具的四个能力支柱把这些问题拆开看一个适合产品团队的沟通工具至少要具备四个能力。频道化组织消息。不同的项目、模块、专项应该拥有独立的讨论空间。用户可以按需加入或退出信息天然被分区而不是在一个大群里滚动。线程化讨论。一条消息可以展开成子线程针对这条消息的讨论不会污染主频道。这使得异步沟通变得可行即使有人晚几个小时参与讨论也能立刻理解上下文。精准的通知策略。用户可以设置只接收“我”的消息、某些频道的消息或者关键词提醒把无关打扰降到最低。高效的搜索与关联。历史和关键词可以被快速检索消息能够与需求、缺陷、任务建立关联让沟通真正围绕业务对象展开。1.3 从沟通工具到协作工作台当一个沟通工具同时具备以上能力它就不仅仅是“聊天工具”而是变成了团队的协作工作台。产品、设计、研发可以在同一个上下文里完成讨论和决策最后的结论还能沉淀成可追溯的记录。这也是为什么很多团队会考虑自建轻量协作工具而不是依赖通用 IM。通用 IM 擅长社交和群聊但在“围绕业务对象组织沟通”这件事上往往做得不够深入。从技术实现来看这类工具的基础链路并不复杂前端通过 WebSocket 与后端保持长连接后端负责消息的接收、存储和广播。本文将围绕这条链路逐步展开。2. 系统总体设计与技术选型2.1 整体架构为了便于理解我们把一个最小可运行的团队沟通工具拆成三部分。前端页面Vue 3 ↓ STOMP over WebSocket 后端服务Spring Boot ↓ JPA/JDBC 数据库MySQL 在线状态Redis在单机演示阶段Spring Boot 内置的 SimpleBroker 就可以完成消息转发。如果后续要支撑多实例部署则需要引入 Redis Pub/Sub 或消息队列来做广播具体我们在最佳实践章节展开。2.2 为什么选择 WebSocket STOMP团队沟通工具的核心诉求是“低延迟实时通信”。常见的方案有轮询、SSEServer-Sent Events和 WebSocket。轮询实现简单但存在消息延迟和无效请求过多的问题。SSE 适合服务端单向推送但不适合客户端频繁发送即时消息。WebSocket 则支持全双工通信客户端和服务端可以互相推送消息是聊天类场景的主流选择。在 WebSocket 之上我们通常还会选择 STOMP 协议。STOMP 是一个简单的文本消息协议它在 WebSocket 之上定义了“目的地destination”的概念。服务端可以往某个目的地推送消息客户端可以订阅某个目的地消息路由逻辑变得非常清晰。例如一个消息发送到/app/chat.send服务端处理后会推送到/topic/channel/1。所有订阅了这个目的地的在线客户端都能实时收到。这个“发布订阅”模型非常适合聊天室和协作工具。2.3 数据模型设计虽然消息是实时推送的但历史消息、频道信息、成员关系仍然需要持久化存储。这里给出一个最简版表结构覆盖核心场景。-- 用户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- 频道表 CREATE TABLE t_channel ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, owner_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- 频道成员关系表 CREATE TABLE t_channel_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, channel_id BIGINT NOT NULL, user_id BIGINT NOT NULL, joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_channel_user (channel_id, user_id) ); -- 消息表 CREATE TABLE t_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, channel_id BIGINT NOT NULL, sender_id BIGINT NOT NULL, content TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_channel_created (channel_id, created_at) );这里有一个值得注意的设计点消息表索引idx_channel_created按channel_id和created_at建立联合索引。因为历史消息最常见的查询场景是“进入某个频道后按时间倒序拉取最近 N 条”这个索引可以显著提升查询效率。2.4 消息模型约定消息在前后端传输时需要约定一个统一结构。为了减少学习成本我们可以直接使用和后端实体一致的 DTO。{ id: 1001, channelId: 1, senderId: 42, content: 邀请大家今天下午三点评审新版原型, createdAt: 2024-01-15 14:30:00 }senderId在前端发送消息时不需要传后端从 WebSocket 会话的认证信息中获取。这样可以避免伪造发送者身份。3. 后端核心实现Spring Boot WebSocket3.1 创建项目与依赖后端我们使用 Spring Boot并在pom.xml中加入 WebSocket、JPA、MySQL 驱动和 Lombok 依赖。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies需要注意的是不同 Spring Boot 版本的 MySQL 驱动坐标可能不同。Spring Boot 3.x 推荐使用com.mysql:mysql-connector-j。本文示例以 Spring Boot 2.7 为主如果你使用的是 3.x请做相应替换。数据库连接配置放在application.yml中。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/team_chat?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true这里的ddl-auto: update在演示阶段很方便Hibernate 会根据实体自动建表。生产环境建议改为validate由 DBA 统一管理表结构变更。3.2 配置 STOMP 端点与消息代理Spring Boot 通过实现WebSocketMessageBrokerConfigurer来配置 WebSocket 和 STOMP。package com.example.teamchat.config; import com.example.teamchat.interceptor.AuthHandshakeInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws-chat) .addInterceptors(new AuthHandshakeInterceptor()) .setAllowedOriginPatterns(*) .withSockJS(); } Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic, /queue); registry.setApplicationDestinationPrefixes(/app); } }这里有两个关键配置。setApplicationDestinationPrefixes(/app)表示客户端发送消息时统一使用/app前缀比如发送到/app/chat.send。enableSimpleBroker(/topic, /queue)则表示服务端可以向/topic和/queue前缀的目的地推送消息。/topic是广播模式适合频道消息/queue是点对点模式适合用户私信和未读提醒。这样路由规则就很清晰了。3.3 握手认证拦截器WebSocket 连接握手阶段我们需要从请求中获取用户身份。在最小演示版本中前端会在连接地址上携带userId参数后端手工校验。package com.example.teamchat.interceptor; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; public class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { if (request instanceof ServletServerHttpRequest servletRequest) { String userId servletRequest.getServletRequest().getParameter(userId); if (userId null || userId.isBlank()) { return false; } // 生产环境应解析并校验 JWT而不是直接信任参数 attributes.put(userId, Long.parseLong(userId)); return true; } return false; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手完成后的回调可以用于日志记录 } }attributes是握手阶段保存会话属性的一种方式后续在MessageMapping方法中可以通过SimpMessageHeaderAccessor取回。这里要特别强调演示代码直接信任userId参数仅用于本地联调绝对不能在生产环境这样用。生产环境必须使用 JWT 或其它认证机制在后端解析并校验签名。3.4 消息发送服务首先创建消息实体。package com.example.teamchat.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; Data Entity Table(name t_message) public class Message { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name channel_id, nullable false) private Long channelId; Column(name sender_id, nullable false) private Long senderId; Column(name content, nullable false) private String content; Column(name created_at, nullable false) private LocalDateTime createdAt LocalDateTime.now(); }如果你使用 Spring Boot 3.x需要把javax.persistence替换为jakarta.persistence这是 Java EE 向 Jakarta EE 迁移带来的变化。再定义客户端发送消息的 DTO。package com.example.teamchat.dto; import lombok.Data; Data public class ChatMessage { private Long channelId; private String content; }接着创建消息仓库。package com.example.teamchat.repository; import com.example.teamchat.entity.Message; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface MessageRepository extends JpaRepositoryMessage, Long { ListMessage findTop50ByChannelIdOrderByCreatedAtDesc(Long channelId); }然后是核心的聊天控制器。package com.example.teamchat.controller; import com.example.teamchat.dto.ChatMessage; import com.example.teamchat.entity.Message; import com.example.teamchat.repository.MessageRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import java.time.LocalDateTime; Controller public class ChatController { Autowired private SimpMessagingTemplate messagingTemplate; Autowired private MessageRepository messageRepository; MessageMapping(/chat.send) public void sendMessage(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { Long userId (Long) headerAccessor.getSessionAttributes().get(userId); // 生产环境需要在这里校验当前用户是否属于该频道 // boolean allowed memberRepository.existsByChannelIdAndUserId(...); // if (!allowed) { throw new IllegalAccessException(); } // 1. 保存消息 Message message new Message(); message.setChannelId(chatMessage.getChannelId()); message.setSenderId(userId); message.setContent(chatMessage.getContent()); message.setCreatedAt(LocalDateTime.now()); Message saved messageRepository.save(message); // 2. 推送消息到频道订阅者 messagingTemplate.convertAndSend( /topic/channel/ chatMessage.getChannelId(), saved ); } }SimpMessagingTemplate.convertAndSend是 Spring 提供的最核心推送方法内部会把对象序列化为 JSON并发送到指定目的地。所有订阅了/topic/channel/{channelId}的在线客户端都能收到这条消息。这里把“存储”和“推送”放在同一个同步事务流程里优点是逻辑简单消息不会漏存缺点是推送耗时会影响接口响应。在高并发场景下可以先把消息写入数据库再通过异步任务或消息队列推送。后续可以在最佳实践章节扩展。3.5 消息历史查询提供 RESTful 接口让前端进入频道后先加载最近历史消息。package com.example.teamchat.controller; import com.example.teamchat.entity.Message; import com.example.teamchat.repository.MessageRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/messages) public class MessageApiController { Autowired private MessageRepository messageRepository; GetMapping public ListMessage history(RequestParam Long channelId) { // 按时间倒序取最近 50 条前端再反转展示 return messageRepository.findTop50ByChannelIdOrderByCreatedAtDesc(channelId); } }如果你需要做分页可以把返回值改成PageMessage每次请求指定page和size。实际项目中通常还要 join 用户表返回发送者昵称、头像等信息这里为了保持示例简洁只用senderId表示。4. 前端核心实现Vue 3 实时聊天4.1 初始化前端项目前端使用 Vue 3 Vite配合stomp/stompjs和sockjs-client两个库。stompjs负责 STOMP 协议的编解码sockjs-client负责在浏览器中模拟 WebSocket 兼容层。npm create vitelatest team-chat-web -- --template vue cd team-chat-web npm install stomp/stompjs sockjs-client4.2 建立 StompJS 连接在 Vue 组件中建立连接时因为后端使用 SockJS 端点所以前端也要通过 SockJS 方式创建连接。script setup import { ref, onMounted, onUnmounted } from vue; import { Client } from stomp/stompjs; import SockJS from sockjs-client; const userId 1; // 演示固定实际从登录态获取 const channelId 1; const messages ref([]); const input ref(); let client null; onMounted(async () { // 1. 拉取历史消息 const res await fetch(http://localhost:8080/api/messages?channelId${channelId}); const history await res.json(); // 接口返回倒序这里反转成正序展示 messages.value history.reverse(); // 2. 建立 WebSocket 连接 client new Client({ webSocketFactory: () new SockJS(http://localhost:8080/ws-chat?userId${userId}), onConnect: () { // 订阅频道主题 client.subscribe(/topic/channel/${channelId}, (frame) { const msg JSON.parse(frame.body); messages.value.push(msg); }); }, onStompError: (frame) { console.error(STOMP 错误, frame); } }); client.activate(); }); onUnmounted(() { if (client) { client.deactivate(); } }); /scriptclient.subscribe中的回调会在每次收到服务端推送时触发我们把新消息推进数组Vue 的响应式系统会自动更新页面。4.3 发送消息发送消息时客户端通过client.publish把消息发送到后端的目的地/app/chat.send。script setup function send() { if (!input.value.trim()) return; client.publish({ destination: /app/chat.send, body: JSON.stringify({ channelId: channelId, content: input.value }) }); input.value ; } /script这里不需要传senderId后端会从 WebSocket 会话中获取。4.4 渲染消息列表模板部分保持简单先让核心流程跑通。template div classchat-page div classmessage-list div v-formsg in messages :keymsg.id classmessage-item span classsender用户 {{ msg.senderId }}/span span classcontent{{ msg.content }}/span span classtime{{ msg.createdAt }}/span /div /div div classinput-area input v-modelinput placeholder输入消息回车发送 keyup.entersend / button clicksend发送/button /div /div /template在实际项目中消息列表还需要处理时间分组、滚动到底部、图片预览、链接识别、表情支持等交互细节。这里先聚焦实时通信链路。5. 启动运行与验证5.1 启动后端服务在application.yml中配置好数据库连接后直接运行主类。如果使用的是 Spring Boot 2.7主类可以这样写。package com.example.teamchat; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class TeamChatApplication { public static void main(String[] args) { SpringApplication.run(TeamChatApplication.class, args); } }启动成功后控制台会看到 Tomcat 和 WebSocket 初始化日志。5.2 启动前端页面在team-chat-web目录下执行npm run dev默认访问http://localhost:5173。由于前端页面运行在 5173 端口后端是 8080 端口存在跨域问题。后端已经在握手配置中设置了setAllowedOriginPatterns(*)可以放开 WebSocket 连接的限制。5.3 双用户验证实时通信为了验证实时性可以打开两个浏览器窗口一个把userId改成 1另一个改成 2。两个页面都订阅同一个频道后在其中一个页面发送消息另一个页面应该能几乎无延迟地收到消息。后端控制台也会打印对应的 SQL 插入日志说明消息已经持久化。5.4 预期结果如果一切顺利你会看到一个最简单的实时频道聊天工具进入页面自动加载最近历史消息发送后所有订阅该频道的用户实时收到新消息刷新页面后历史消息仍然存在。这套最小链路已经覆盖了团队沟通工具最核心的实时通信能力。6. 常见问题与排查思路在联调 WebSocket 应用时很容易遇到各种“连不上、收不到、发不出”的问题。下面整理一份高频问题排查清单。问题现象常见原因解决思路浏览器控制台报 403WebSocket 握手失败后端握手拦截器拒绝或跨域配置不正确检查拦截器是否从 request 取到了合法的 userId 参数STOMP 连接成功但收不到频道消息订阅的 destination 与服务端推送的 destination 不一致统一使用/topic/channel/{channelId}约定发送消息后页面无反应客户端发送的 destination 前缀不对检查setApplicationDestinationPrefixes是否配置为/app历史消息能加载但刷新后消失消息没有成功写入数据库检查 JPA 配置和数据库表是否创建成功两个用户连接互相收不到消息两个用户订阅的 channelId 不一致在浏览器 Network 面板中核对订阅地址消息偶尔丢一条WebSocket 网络闪断客户端没有自动重连在 onWebSocketClose 中实现重连机制消息顺序错乱多实例部署时没有全局有序机制单频道内使用同一个消息队列或用时间戳序号排序排查 WebSocket 问题时优先在浏览器开发者工具的 Network 面板查看帧数据。WebSocket 的帧内容可以直接看到客户端和服务端互发的原始报文这是定位问题的最直接手段。另一个常用技巧是在后端全局异常处理器中增加对MissingRequestUserException、IllegalArgumentException等异常的处理把握手阶段和消息处理阶段的异常以明文方式返回给前端便于快速定位。7. 让沟通更“Enjoyable”体验优化方向做到这里我们拥有的是一个能用的实时聊天工具。但“能用”和“好用”之间还有很长的距离。要让产品团队愿意长期使用必须在体验上做大量细节优化。7.1 通知分级与免打扰无差别的通知是团队沟通效率最大的杀手。实现上可以给每条消息增加通知策略判断只有消息中了当前用户、或者关键词命中当前用户的订阅规则才实时推送否则只更新未读角标。在技术实现上可以在消息入库后异步触发通知中心通知中心根据成员偏好决定“立即推送、静默未读、彻底忽略”。这样既保证重要消息不遗漏也保护了成员的专注时间。7.2 消息线程与提及线程化讨论需要消息表新增parent_id字段。初始消息的parent_id为空回复消息的parent_id指向被回复的消息。前端渲染时把同一parent_id的消息聚合到同一个线程组件中。提及则需要对消息内容做解析识别用户名的特殊语法。解析结果需要保存成结构化的 mentions 数据方便消息索引和通知中心判断是否要提醒对应成员。7.3 全文搜索随着历史消息变多MySQL 的LIKE %keyword%查询会越来越慢。如果团队规模较大推荐引入专门的搜索服务在消息写入时同步建立索引搜索时直接查索引。对于中小团队也可以先使用 MySQL 全文索引或多个关键词组合查询。搜索功能的好坏直接影响“沟通沉淀”的价值这一点值得投入。7.4 与项目管理工具联动让消息和需求、任务、缺陷建立关联是团队沟通工具从“聊天工具”升级为“协作工作台”的关键一步。实现上可以约定一种特殊消息语法比如输入#需求ID或者粘贴需求链接后端解析后保存关联关系。页面上展示关联消息时可以渲染出对应的需求标题和状态标签点击即可跳转到项目管理平台。这种联动能让开发同学在不需要切换到另一个系统的情况下完成大部分沟通产品经理也能在需求详情页里直接看到相关的讨论记录。8. 生产落地最佳实践从本地演示到生产环境还有大量工程问题需要解决。下面按重要程度给出建议。8.1 安全加固WebSocket 后端必须使用真实认证机制比如 Spring Security JWT。握手阶段解析 JWT校验签名和有效期从 token 中获取 userId 和权限列表。频道权限校验不能省略。每次发消息和订阅频道时都要检查当前用户是否为频道成员。否则可以伪造订阅地址收听自己不该访问的频道内容。消息内容展示时要做 XSS 过滤。用户发送的文本可能包含 HTML 或脚本前端渲染时不能直接v-html需要对特殊字符进行转义。服务端也应该做内容安全过滤拒绝危险内容。8.2 消息可靠性WebSocket 本质上是基于 TCP 的实时通道网络抖动会造成连接断开。客户端需要实现断线重连机制并在重连后主动拉取断开期间未接收的消息。一种常用策略是消息入库时带上全局自增序号客户端记录本地已收到的最大序号。重连后通过请求/api/messages/since?lastSeq...拉取缺失消息。同时发送端可以在本地维护待确认队列服务端确认后再移除避免消息在客户端本地“以为自己发出去了实际没有入库”。8.3 容量与性能单机版的SimpleBroker只适用于内部小团队。生产环境一旦有多个后端实例每个实例的内存 Broker 是相互隔离的。用户 A 连接在实例 1用户 B 连接在实例 2如果实例 1 推送消息实例 2 上的用户就收不到。解决方案是把消息广播层改为 Redis Pub/Sub 或 RabbitMQ。发布者把消息发送到 Redis Channel所有后端实例订阅同一个 Channel收到后推送给各自连接的客户端。这一步是团队协作工具从单机走向集群的关键改造。消息表的数据增长很快建议按月或按频道进行分区。历史消息定期归档到冷存储保留在线热数据的查询性能。8.4 可观测性生产环境的 WebSocket 服务必须做好监控。至少要记录以下指标当前在线连接数每秒消息发送量单频道订阅人数消息入库耗时和推送耗时WebSocket 握手成功率当在线连接数异常波动或消息推送耗时上升时监控系统能第一时间告警。结合日志追踪可以快速定位是数据库瓶颈、网络问题还是消息队列积压。9. 总结与动手建议构建一个适合产品团队的沟通工具核心链路并不复杂频道组织消息、WebSocket 实时推送、数据库持久化、通知与搜索增强体验。本文给出的最小版本已经覆盖了前三个环节你可以在此基础上继续叠加线程、提及、未读角标和项目管理联动。如果你是第一次接触 WebSocket 开发建议先不要急着加复杂功能。把本文的单机版本跑通理解/app和/topic这两个前缀的含义再逐步加入认证、权限校验和历史消息分页。通信链路没有问题后再考虑多实例广播和消息可靠性。如果你所在团队正好有内部工具需求这套方案可以作为第一版原型。先解决“消息不淹没、讨论有上下文、历史可搜索”这三个核心痛点剩下的体验细节可以边用边迭代。
返回列表