WebSocket--精准推送方案(二):实时消息推送-若依项目示例

发布于:2025-08-19 ⋅ 阅读:(12) ⋅ 点赞:(0)

1. 功能概述

在后台管理系统中,需要实时通知用户未处理订单或审批数量的变化。本例通过前端 Vue + 后端 Spring Boot WebSocket 实现:

  • 前端实时显示未处理订单数量

  • 后端实时推送消息给指定用户或所有用户

  • 支持用户角色区分(如会计、老板、库管员)推送不同数量

使用下面的方式解决了

  1.跨域问题registry.addHandler(handler, "/ws").setAllowedOrigins("*") 或指定前端地址

  2.注入问题:下面代码中在 WebSocket 处理器类中(MyWsHandler),可以使用@Autowired注入想要注入的类

2. 前端实现

2.1 HTML / Vue 模板

在若依中找到src\layout\components\Navbar.vue后可以把下面代码添加到<div class="right-menu">中

<!-- 消息图标和未处理数量 -->
<div class="message-container">
  <i class="iconfont icon-xiaoxi"></i>
  <span v-if="unreadCount > 0" class="unread-count">
    有未处理订单数量:{{ unreadCount }}
  </span>
</div>

2.2 Vue 数据与生命周期

把下面代码加src\layout\components\Navbar.vue的到export default {}中,注意,分隔符

data() {
  return {
    unreadCount: 0, // 未处理数量
    websocket: null
  };
},
created() {
  this.initWebSocket();
},
beforeDestroy() {
  // 页面关闭时断开 WebSocket
  if (this.websocket) {
    this.websocket.close();
  }
}

2.3 WebSocket 初始化

 把下面代码加src\layout\components\Navbar.vue的到methods: {}中,注意,分隔符

initWebSocket() {
  const wsUrl = `ws://localhost:8080/websocket/message?userId=1`; // userId 可动态传
  this.websocket = new WebSocket(wsUrl);

  this.websocket.onopen = () => {
    console.log('WebSocket 连接成功');
  };

  this.websocket.onmessage = (event) => {
    console.log('收到消息:', event.data);
    this.unreadCount = parseInt(event.data) || 0; // 更新未处理数量
  };

  this.websocket.onclose = () => {
    console.log('WebSocket 连接关闭,3秒后重连');
    setTimeout(this.initWebSocket, 3000); // 自动重连
  };

  this.websocket.onerror = () => {
    console.error('WebSocket 出错');
  };
}

说明:

  • WebSocket 是浏览器原生 API,可实现前端与后端长连接

  • 通过 URL 查询参数传递 userId

  • 自动重连机制保证网络中断时能恢复连接

  • 接收到消息后更新 unreadCount 显示数量

3.前端配置完成后,若依框架后端关闭token验证

  1. 在若依框架中,如果访问页面或接口没有额外配置,系统会默认检查 token 权限。因此直接访问

ws://localhost:8080/websocket/message?userId=1

        可能会被拦截,导致 WebSocket 无法建立连接。

        解决方法:在后端 com.ruoyi.framework.config 包下的 SecurityConfig 类中,找到 filterChain 方法,为该路径配置免 token 验证,即将 /websocket/**设置为不进行 token 权限检查。

注重点

关于 ws://localhost:8080/websocket/message?userId=1 的说明与注意事项

  1. 用户 userId 的获取
    本示例中 userId 是固定写死的,但在实际项目中,可以通过前端请求后台接口获取当前用户的 userId。这样每个用户就能拥有独立的 WebSocket 连接,实现精准消息推送。

  2. Token 验证问题
    WebSocket 连接无法像普通 HTTP 请求一样携带 token,因此直接访问该 URL 时无法通过若依默认的 token 权限校验。
    解决方法:在后端 com.ruoyi.framework.config 包下的 SecurityConfig 类中,找到 filterChain 方法,将 /websocket/** 路径配置为免 token 验证。这样 WebSocket 请求就不会被拦截。

  3. 为什么把 userId 拼接在 URL 中
    由于 WebSocket 连接无法直接使用 token 验证,后端在获取当前登录用户信息时(获取登录方法需要验证权限也就是Tocken)无法直接识别用户身份。因此,将 userId 作为 URL 参数传递,后台在握手阶段读取该参数,就可以确定用户身份,实现对应的消息推送。


4. 后端实现

4.1 需要的xml

  spring-boot-starter-websocket 确实是 Spring Boot 自带管理的 starter,所以不用写版本号,它会跟随 Spring Boot 的版本自动选择合适的依赖版本。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

4.2 WebSocket 配置类

@Configuration
@EnableWebSocket
public class MyWsConfig implements WebSocketConfigurer {

    @Resource
    MyWsHandler myWsHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 添加 WebSocket 处理器,并拦截 userId
        registry.addHandler(myWsHandler,"/websocket/message")
                .addInterceptors(new HttpSessionHandshakeInterceptor() {
                    @Override
                    public boolean beforeHandshake(ServerHttpRequest request,
                                                   ServerHttpResponse response,
                                                   WebSocketHandler wsHandler,
                                                   Map<String, Object> attributes) throws Exception {
                        String query = request.getURI().getQuery();
                        if (query != null) {
                            for (String param : query.split("&")) {
                                String[] kv = param.split("=");
                                if (kv.length == 2 && "userId".equals(kv[0])) {
                                    attributes.put("userId", kv[1]); // 保存 userId
                                }
                            }
                        }
                        return true;
                    }
                }).setAllowedOrigins("*"); // 允许跨域
    }
}

说明:

  • WebSocketConfigurer 用于注册 WebSocket 处理器

  • HttpSessionHandshakeInterceptor 可以在握手阶段获取 URL 参数(如 userId)

  • setAllowedOrigins("*") 允许跨域连接(开发环境用,生产可改为指定域名)


4.3 WebSocket 处理器

/**
 * WebSocket 核心处理类
 * 继承 AbstractWebSocketHandler 来处理前端与后端之间的消息交互
 * 功能点:
 *   1. 建立连接时记录用户连接(sessionMap 管理)
 *   2. 支持后端主动推送消息
 *   3. 处理异常与连接关闭,保证 sessionMap 干净
 */
@Component
public class MyWsHandler extends AbstractWebSocketHandler {

    /**
     * 保存所有在线用户的 WebSocketSession
     * key   -> userId(用户唯一标识)
     * value -> 与该用户对应的 WebSocketSession
     * 使用 ConcurrentHashMap 保证线程安全
     */
    private static Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();

    /**
     * 当客户端成功建立连接时调用
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从握手阶段存入的属性中获取 userId
        String userId = (String) session.getAttributes().get("userId");

        // 将 userId 与该用户的会话绑定到 sessionMap
        sessionMap.put(userId, session);

        // ======== 业务逻辑示例 ========
        // 模拟查询该用户未处理的订单数量(比如从数据库 select count(*))
        int count = 20;

        // 连接建立成功后立即返回该用户的待办数量
        session.sendMessage(new TextMessage(count + ""));
    }

    /**
     * 处理前端主动发送过来的消息
     * 例如:心跳检测、客户端指令
     * 本例暂时不需要,保留空实现
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 这里可以打印或处理客户端发来的消息
        // System.out.println("收到客户端消息:" + message.getPayload());
    }

    /**
     * 当传输出现异常时调用
     * 例如:网络断开、消息发送异常
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 如果 session 仍然是开启状态,先关闭
        if (session.isOpen()) {
            session.close();
        }
        // 注意:这里移除时用 session.getId(),而不是 userId
        // 说明:前面存的时候是 userId,这里用 session.getId() 可能导致删除不一致
        // 建议改成根据 userId 移除
        sessionMap.remove(session.getId());
    }

    /**
     * 当连接关闭时调用
     * 比如用户刷新页面 / 浏览器关闭
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 同样需要移除该用户的 session
        // 建议和 afterConnectionEstablished 的存储逻辑保持一致
        sessionMap.remove(session.getId());
    }

    /**
     * 后端主动推送消息给所有在线用户
     * 场景:当有新订单 / 审批状态变化时,需要实时通知所有人
     */
    //这里如果不想推送给所有人,可以给sendMse()添加参数作为推送条件
    public void sendMse() {
        // 遍历所有在线用户
        for (String userId : sessionMap.keySet()) {
            try {
                // ======== 业务逻辑示例 ========
                // 模拟动态查询该用户对应的未处理数量
                // 实际开发中应根据 userId 查询不同角色的待办数量
                int count = 30;

                // 拿到该用户的 WebSocket 会话
                WebSocketSession session = sessionMap.get(userId);

                // 判断 session 是否有效(防止空指针或连接已关闭)
                if (session != null && session.isOpen()) {
                    // 主动推送消息给前端
                    session.sendMessage(new TextMessage(count + ""));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

        本样例推送时推送所有人,如果不想推送给所有人,可以给sendMse()添加参数作为推送条件

说明:

  • sessionMap 保存所有在线用户的 WebSocketSession,便于推送消息

  • afterConnectionEstablished 建立连接时发送初始未处理数量

  • sendMessage 方法可在后台数据更新时调用,实现实时推送

  • handleTransportErrorafterConnectionClosed 确保断开连接及时清理


5. 工作流程

  1. 前端 Vue 页面加载时调用 initWebSocket() 建立连接

  2. 后端 MyWsHandler 保存用户 session 并返回初始未处理数量

  3. 后端业务逻辑变化(如审批通过/新订单)调用 sendMessage(),推送新数量

  4. 前端 onmessage 接收并更新页面显示

  5. 网络断开后,前端自动重连


6. 特点与注意事项

  • 支持实时消息推送,避免轮询数据库

  • 根据 userId 精准推送

  • 可以扩展支持角色、权限等业务逻辑

  • 前端自动重连机制保证稳定性

  • 开发环境可允许跨域,生产环境应限制 origin

  • session.isOpen() 用于判断 WebSocket 是否仍有效


网站公告

今日签到

点亮在社区的每一天
去签到