springboot 接入websocket实现定时推送消息到客户端

该示例展示了如何在SpringBoot应用中配置WebSocket,以便在用户登录后,后端能根据用户ID定时发送消息到前端进行展示。通过使用ConcurrentHashMap和CopyOnWriteArraySet管理用户连接,确保线程安全,并通过@Scheduled注解实现定时任务来广播消息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

说明

如标题,举例需求场景:
前端与后端websocket连接上后,多用户登录,后端根据不同用户定时发消息给前端用于展示

代码实现

1、

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

2、

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

}

3、

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;



// 交给IOC容器
@Component
// 如果去掉/{userId} 那就是不分用户 给连接上的用户统一发送消息
@ServerEndpoint("/websocket/{userId}")
@Slf4j
public class WebSocketService {


    // 这里用ConcurrentHashMap 因为他是一个线程安全的Map
    private static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketService>> userwebSocketMap = new ConcurrentHashMap<>();

    private static ConcurrentHashMap<String, Integer> count = new ConcurrentHashMap<>();

    private String userId;


    /*
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 连接建立成功调用的方法
     *
     * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") final String userId) {
        this.session = session;
        this.userId = userId;
        System.out.println("session:" + session);
        System.out.println("userId:" + userId);
        if (!exitUser(userId)) {
            initUserInfo(userId);
        } else {
            CopyOnWriteArraySet<WebSocketService> webSocketServiceSet = getUserSocketSet(userId);
            webSocketServiceSet.add(this);
            userCountIncrease(userId);
        }
        System.out.println("有" + userId + "新连接加入!当前在线人数为" + getCurrUserCount(userId));
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        CopyOnWriteArraySet<WebSocketService> webSocketServiceSet = userwebSocketMap.get(userId);
        //从set中删除
        webSocketServiceSet.remove(this);
        //在线数减1
        userCountDecrement(userId);
        System.out.println("有一连接关闭!当前在线人数为" + getCurrUserCount(userId));
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        CopyOnWriteArraySet<WebSocketService> webSocketSet = userwebSocketMap.get(userId);
        System.out.println("来自客户端" + userId + "的消息:" + message);
        //群发消息
        for (WebSocketService item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }

    }


    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }


    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     *
     * @param message
     * @throws IOException
     */

    public void sendMessage(String message) throws IOException {
        System.out.println("服务端推送" + userId + "的消息:" + message);
        this.session.getAsyncRemote().sendText(message);
    }


    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。   我是在有代办消息时 调用此接口 向指定用户发送消息
     *
     * @param message
     * @throws IOException
     */

    public void sendMessage(String userId, String message) throws IOException {
        System.out.println("服务端推送" + userId + "的消息:" + message);
        CopyOnWriteArraySet<WebSocketService> webSocketSet = userwebSocketMap.get(userId);
        //群发消息
        for (WebSocketService item : webSocketSet) {
            try {
                item.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    public void sendOpenAllUserMessage(List<String> userIds, String message) {
        for (String userId : userIds) {
            CopyOnWriteArraySet<WebSocketService> webSocketSet = userwebSocketMap.get(userId);
            //群发消息
            for (WebSocketService item : webSocketSet) {
                try {
                    item.session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }


    public boolean exitUser(String userId) {
        return userwebSocketMap.containsKey(userId);
    }

    public CopyOnWriteArraySet<WebSocketService> getUserSocketSet(String userId) {
        return userwebSocketMap.get(userId);
    }

    public void userCountIncrease(String userId) {
        if (count.containsKey(userId)) {
            count.put(userId, count.get(userId) + 1);
        }
    }


    public void userCountDecrement(String userId) {
        if (count.containsKey(userId)) {
            count.put(userId, count.get(userId) - 1);
        }
    }

    public void removeUserConunt(String userId) {
        count.remove(userId);
    }

    public Integer getCurrUserCount(String userId) {
        return count.get(userId);
    }

    private void initUserInfo(String userId) {
        CopyOnWriteArraySet<WebSocketService> webSocketServiceSet = new CopyOnWriteArraySet<WebSocketService>();
        webSocketServiceSet.add(this);
        userwebSocketMap.put(userId, webSocketServiceSet);
        count.put(userId, 1);
    }

    public List<String> getAllUser() {
        List<String> allUser = new LinkedList<>();
        Enumeration<String> keys = userwebSocketMap.keys();
        while (keys.hasMoreElements()) {
            String key = keys.nextElement();
            allUser.add(key);
        }
        return allUser;
    }

}

4、

import com.lq.demo1.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.List;

@EnableScheduling
@Configuration
@Slf4j
public class TaskTimer {
    @Autowired
    private WebSocketService webSocketService;

    @Scheduled(cron = "0/10 * * * * ?")
    public void cleanToken() {
        //10s推送一次
        List<String> allUser = webSocketService.getAllUser();
        //自己可以定义不同用户发送的信息,这里不做演示了
        webSocketService.sendOpenAllUserMessage(allUser, "告警!");
    }

}

@EnableScheduling
在这里插入图片描述

然后把项目启动,打开在线调试websocket连接

路径格式为:ws://localhost:8081/websocket/1
在这里插入图片描述

在这里插入图片描述

成功展示,也可以搞多个窗口,发送内容一致

就先说到这 \color{#008B8B}{ 就先说到这} 就先说到这
在下 A p o l l o \color{#008B8B}{在下Apollo} 在下Apollo
一个爱分享 J a v a 、生活的小人物, \color{#008B8B}{一个爱分享Java、生活的小人物,} 一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞! \color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!} 咱们来日方长,有缘江湖再见,告辞!

在这里插入图片描述

<think>好的,我现在需要帮助用户设计一个基于UniApp的图书馆座位预约系统。首先,我得理清用户的需求。用户提到了功能需求、技术架构和实现方案,所以我要覆盖这三个方面。 首先,功能需求。根据引用[1]、[2]、[4],系统通常分为学生端和管理端。学生端需要注册登录、座位查询、预约、暂离、签到等功能。管理端需要管理学生信息、座位信息、预约记录等。可能还需要结合门禁系统,引用[4]提到进出刷卡数据确定是否在馆,所以签到可能涉及位置或门禁验证。 接下来是技术架构。用户指定使用UniApp,所以前端用Vue.js和UniApp框架,支持多端。后端方面,引用[1]用Java,引用[3]用Node.js,用户可能需要选择合适的技术。考虑到UniApp的前后端分离,后端可以用Spring Boot或Node.js,数据库用MySQL。引用[3]和[4]都提到MySQL,所以数据库选型明确。此外,引用[1]用了Mybatis,引用[3]用Express,可能需要根据团队熟悉度选择。 实现方案需要分步骤。首先环境搭建,安装HBuilder X和依赖。然后设计数据库,包括学生表、座位表、预约记录等。引用[1]和[4]提到座位信息管理、预约记录,所以数据库设计要涵盖这些。接着前后端开发,前端使用UniApp组件,后端提供API。接口文档需要明确,比如预约座位、暂离、签到等接口。测试和部署阶段要确保多端兼容,引用[4]提到与门禁系统结合,可能需要硬件对接。 注意事项方面,引用[2]提到高峰期并发,需要Redis缓存或队列处理。引用[4]提到用户体验,界面要美观易用。权限管理方面,管理员和学生权限不同,需做好控制。 可能的问题包括座位状态实时更新,如何防止重复预约,签到机制如何设计。引用[2]中的门禁系统集成可能需要蓝牙或定位验证用户是否到馆。此外,引用[1]用了token认证,需要在后端实现JWT来管理用户登录状态。 总结下来,我需要将功能模块细化,技术选型明确,并分步骤说明实现过程,同时考虑扩展性和性能优化。确保引用中的关键点都被涵盖,比如前后端分离、数据库设计、权限管理和并发处理。</think>### 基于UniApp的图书馆座位预约系统设计与实现方案 #### 一、功能需求分析 1. **学生端功能**: - 用户认证:微信授权登录 + 学号绑定(需对接学校认证系统)[^2] - 座位查询:可视化展示楼层平面图,支持筛选空闲/占用状态(颜色区分)[^4] - 预约管理:支持即时预约(30分钟内到馆签到)、定时预约(提前24小时) - 暂离功能:15分钟临时锁座(需扫码离座/归座)[^2] - 签到机制:蓝牙信标定位 + 二维码双重验证[^4] - 违约记录:超时未签到扣信用分,累计3次禁用预约权限[^1] 2. **管理端功能**: - 动态座位管理:支持区域划分(普通区/静音区/研讨间)[^4] - 预约规则配置:开放时间、最大预约时长(建议4-8小时可调) - 数据看板:实时监控入馆人数、高峰时段统计[^2] - 异常处理:强制释放占座(超时未签入自动释放)[^1] - 消息推送:系统公告、预约提醒(微信服务通知)[^3] #### 二、技术架构设计 $$系统架构图$$ ```mermaid graph TD A[UniApp前端] --> B[Vue.js组件化开发] B --> C{多端适配} C -->|微信小程序| D[微信API] C -->|H5| E[浏览器API] F[SpringBoot后端] --> G[Restful API] G --> H[MySQL集群] H --> I[Redis缓存] I --> J[定时任务] J --> K[座位状态同步] ``` 1. **前端技术栈**: - 核心框架:UniApp + Vue3 + Vite - 可视化组件:uCharts实现座位热力图[^4] - 地图引擎:集成腾讯地图SDK实现楼层导航 - 状态管理:Pinia管理全局预约状态 2. **后端技术栈**: - 微服务架构:SpringCloud Alibaba + Nacos注册中心 - 持久层:MyBatis-Plus + Sharding-JDBC分库分表 - 实时通信:WebSocket推送座位状态变更[^3] - 安全机制:JWT令牌 + 接口签名验证 3. **数据库设计**: ```sql CREATE TABLE seat ( seat_id VARCHAR(20) PRIMARY KEY, -- 座位编号A1-001 zone ENUM('普通区','静音区','研讨间'), status ENUM('空闲','已预约','使用中','维护中'), last_used DATETIME ); CREATE TABLE reservation ( order_id BIGINT AUTO_INCREMENT, user_id VARCHAR(12) NOT NULL, seat_id VARCHAR(20) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME, checkin_time DATETIME, status TINYINT(1) DEFAULT 0 -- 0待签到 1使用中 2已完成 3已取消 ); ``` #### 三、关键实现方案 1. **实时座位状态同步**: - 采用Redis发布订阅模式,当座位状态变更时触发事件 - 前端通过WebSocket建立长连接接收更新数据 - 心跳检测机制保持连接活性(间隔30秒) 2. **预约冲突解决算法**: $$冲突检测公式:\exists t \in [t_{start}, t_{end}] \cap [t'_{start}, t'_{end}]$$ ```python def check_availability(seat_id, new_start, new_end): existing = Reservation.objects.filter( seat_id=seat_id, status__in=[0, 1], end_time__gt=new_start, start_time__lt=new_end ) return not existing.exists() ``` 3. **高并发优化**: - 使用Redisson分布式锁处理预约请求 - 数据库读写分离 + 垂直分库(用户库、座位库、订单库) - 热点数据预加载:每日凌晨生成座位状态快照 #### 四、实施步骤建议 1. **环境搭建**: - 安装HBuilderX 3.6+,配置微信开发者工具 - 创建UniApp项目选择vue3模板 - 后端使用IDEA创建SpringBoot 2.7+项目 2. **核心功能开发顺序**: 1. 用户认证模块(微信登录+学号绑定) 2. 座位地图渲染组件(使用svg动态绘制) 3. 预约状态机实现(包括超时自动释放) 4. 消息通知系统(模板消息+短信双通道) 3. **测试重点**: - 压力测试:JMeter模拟500并发预约 - 兼容性测试:iOS/Android微信客户端版本覆盖 - 异常测试:断网重连后的状态同步 #### 五、扩展性设计 1. **智能推荐模块**: - 基于用户历史偏好推荐座位(靠窗/插座位置) - 使用协同过滤算法:$$similarity(u,v) = \frac{\sum_{i}(r_{ui} - \bar{r}_u)(r_{vi} - \bar{r}_v)}{\sqrt{\sum_{i}(r_{ui} - \bar{r}_u)^2} \sqrt{\sum_{i}(r_{vi} - \bar{r}_v)^2}}$$ 2. **物联网集成**: - 通过ESP32开发板连接座位传感器 - 使用MQTT协议上报实时占用状态 - 硬件状态与系统数据双重校验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值