2025-10-7学习笔记

目录

WebSocket:

中州养老项目集成WebSocket:


WebSocket:

WebSocket 是基于 TCP 的一种新的网络协议 。它实现了浏览器与服务器的全双工通信------浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性 的连接, 并进行双向数据传输。

WebSocket的缺点:

服务器长期维护长连接需要一定的成本

各个浏览器支持程度不一

WebSocket 是长连接,受网络限制比较大,需要处理好重连

**结论:**WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

中州养老项目集成WebSocket:

在zzyl-nursing-platform中如导入以下依赖:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

定义配置类,注册WebSocket的服务端组件(在platform模块下新建config包以及配置类):

java 复制代码
package com.zzyl.nursing.config;

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

@Configuration
public class WebSocketConfig {

    /**
     * 注册基于@ServerEndpoint声明的Websocket Endpoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

定义WebSocket服务端组件:

java 复制代码
package com.zzyl.nursing.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.zzyl.common.exception.base.BaseException;
import com.zzyl.nursing.vo.AlertNotifyVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Component
@EnableWebSocket
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    private static Map<String, Session> sessionMap = new HashMap<>();

    /**
     * 连接建立时触发
     *
     * @param session
     * @param sid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        log.info("有客户端连接到了服务器 , {}", sid);
        sessionMap.put(sid, session);
    }

    /**
     * 服务端接收到消息时触发
     *
     * @param session
     * @param message
     * @param sid
     */
    @OnMessage
    public void onMessage(Session session, String message, @PathParam("sid") String sid) {
        log.info("接收到了客户端 {} 发来的消息 : {}", sid, message);
    }

    /**
     * 连接关闭时触发
     *
     * @param session
     * @param sid
     */
    @OnClose
    public void onClose(Session session, @PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 通信发生错误时触发
     *
     * @param session
     * @param sid
     * @param throwable
     */
    @OnError
    public void onError(Session session, @PathParam("sid") String sid, Throwable throwable) {
        System.out.println("出现错误:" + sid);
        throwable.printStackTrace();
    }

    /**
     * 广播消息
     *
     * @param message
     * @throws IOException
     */
    public void sendMessageToAll(String message) throws IOException {
        Collection<Session> sessions = sessionMap.values();
        if (!CollectionUtils.isEmpty(sessions)) {
            for (Session session : sessions) {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            }
        }
    }

    /**
     * 发送websocket消息给指定消费者
     *
     * @param alertNotifyVo 报警消息
     * @param userIds   报警数据map
     * @throws IOException io异常
     */
    public void sendMessageToConsumer(AlertNotifyVo alertNotifyVo, Collection<Long> userIds) {
        //如果消费者为空,程序结束
        if (CollUtil.isEmpty(userIds)) {
            return;
        }

        //如果websoket客户端为空,程序结束
        if (ObjectUtil.isEmpty(sessionMap)) {
            return;
        }

        //遍历消费者,发送消息
        //key为消息接收人id,value为报警数据id
        userIds.forEach(userId -> {
            //获取该消费者的websocket连接,如果不存在,跳出本次循环
            Session session = sessionMap.get(String.valueOf(userId));
            if (ObjectUtil.isEmpty(session)) {
                return;
            }
            //获取该消费者的websocket连接,并发送消息
            try {
                session.getBasicRemote().sendText(JSONUtil.toJsonStr(alertNotifyVo));
            } catch (IOException e) {
                throw new BaseException("websocket推送消息失败");
            }
        });
    }

}

消息通知Vo:

java 复制代码
package com.zzyl.nursing.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 报警通知消息对象
 *
 **/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AlertNotifyVo {
    /**
     * 报警数据id
     */
    private Long id;

    /**
     * 接入位置
     */
    private String accessLocation;

    /**
     * 位置类型 0:随身设备 1:固定设备
     */
    private Integer locationType;

    /**
     * 物理位置类型 0楼层 1房间 2床位
     */
    private Integer physicalLocationType;

    /**
     * 设备位置
     */
    private String deviceDescription;

    /**
     * 产品名称
     */
    private String productName;

    /**
     * 功能名称
     */
    private String functionName;

    /**
     * 数据值
     */
    private String dataValue;

    /**
     * 报警数据类型,0:老人异常数据,1:设备异常数据
     */
    private Integer alertDataType;

    /**
     * 语音通知状态,0:关闭,1:开启
     */
    private Integer voiceNotifyStatus;

    /**
     * 报警通知类型,0:解除报警,1:报警
     */
    private Integer notifyType;

    /**
     * 是否全员通知<br>
     * 智能床位的报警消息是全员通知,对于护理员和固定设备维护人员不是全员通知
     */
    private Boolean isAllConsumer;
}

AlertRuleServiceImpl:

java 复制代码
/**
     * 过滤数据是否触发报警规则
     *
     * @param rule
     * @param deviceData
     */
    private void deviceDataAlarmHandler(AlertRule rule, DeviceData deviceData) {
        // 判断上报时间是否在规则的生效时段内 00:00:00~23:59:59
        String[] split = rule.getAlertEffectivePeriod().split("~");
        LocalTime startTime = LocalTime.parse(split[0]);
        LocalTime endTime = LocalTime.parse(split[1]);
        // 获取上报时间
        LocalTime time = LocalDateTimeUtil.of(deviceData.getAlarmTime()).toLocalTime();
        // 不在上报时间内,则结束请求
        if (time.isBefore(startTime) || time.isAfter(endTime)) {
            return;
        }
        // 获取IOTID
        String iotId = deviceData.getIotId();
        // 统计次数的key
        String aggCountKey = CacheConstants.ALERT_TRIGGER_COUNT_PREFIX + iotId + ":" + deviceData.getFunctionId() + ":" + rule.getId();

        // 数据对比,上报的数据与规则中的阈值进行对比
        // 两个参数x,y(参数有顺序要求,左边是上报的数据,后边是规则的数据)  x==y 返回0  x>y 返回大于0  x<y 返回小于0的数值
        int compare = NumberUtil.compare(Double.valueOf(deviceData.getDataValue()), rule.getValue());
        if ((rule.getOperator().equals(">=") && compare >= 0) || (rule.getOperator().equals("<") && compare < 0)) {
            log.info("当前上报的数据符合规则异常");
        } else {
            // 正常的数据
            redisTemplate.delete(aggCountKey);
            return;
        }
        // 异常的数据会走到这里
        // 判断是否在沉默周期内
        String silentKey = CacheConstants.ALERT_SILENT_PREFIX + iotId + ":" + deviceData.getFunctionId() + ":" + rule.getId();
        String silentData = redisTemplate.opsForValue().get(silentKey);
        if (StringUtils.isNotEmpty(silentData)) {
            return;
        }
        // 持续周期的逻辑
        String aggData = redisTemplate.opsForValue().get(aggCountKey);
        int count = StringUtils.isEmpty(aggData) ? 1 : Integer.parseInt(aggData) + 1;
        // 如果count与持续周期的值相等,则触发报警
        if (ObjectUtil.notEqual(count, rule.getDuration())) {
            // 不相等
            redisTemplate.opsForValue().set(aggCountKey, count + "");
            return;
        }
        // 删除redis的报警数据
        redisTemplate.delete(aggCountKey);
        // 存储数据到沉默周期,设置一个过期时间,规则中的沉默周期
        redisTemplate.opsForValue().set(silentKey, "1", rule.getAlertSilentPeriod(), TimeUnit.MINUTES);

        // 报警数据,需要找到对应的人
        List<Long> userIds = new ArrayList<>();
        if (rule.getAlertDataType().equals(0)) {
            // 老人异常数据
            if (deviceData.getLocationType().equals(0)) {
                // 说明是报警手表,直接可以找到老人的id,通过老人id,找到对应的护理员
                userIds = deviceMapper.selectNursingIdsByIotIdWithElder(iotId);
            } else if (deviceData.getLocationType().equals(1) && deviceData.getPhysicalLocationType().equals(2)) {
                // 说明是床位设备,可以通过床位id找到老人,通过老人id,找到对应的护理员
                userIds = deviceMapper.selectNursingIdsByIotIdWithBed(iotId);
            }
        } else {
            // 设备异常数据,找维修工,或者是行政人员
            userIds = userRoleMapper.selectUserIdByRoleName(deviceMaintainerRole);
        }
        // 不论是哪种情况,都要通知超级管理员
        List<Long> managerIds = userRoleMapper.selectUserIdByRoleName(managerRole);
        Collection<Long> allUserIds = CollUtil.addAll(userIds, managerIds);
        // 去重
        allUserIds = CollUtil.distinct(allUserIds);

        // 批量保存异常数据
        List<AlertData> alertDataList = insertAlertData(allUserIds, rule,deviceData);

        // websocket推送消息
        webSocketNotity(alertDataList.get(0), rule, allUserIds);
    }

    /**
     * websocket推送消息
     * @param alertData
     * @param rule
     * @param allUserIds
     */
    private void webSocketNotity(AlertData alertData, AlertRule rule, Collection<Long> allUserIds) {

        //属性拷贝
        AlertNotifyVo alertNotifyVo = BeanUtil.toBean(alertData, AlertNotifyVo.class);
        alertNotifyVo.setAccessLocation(alertData.getRemark());
        alertNotifyVo.setFunctionName(rule.getFunctionName());
        alertNotifyVo.setAlertDataType(rule.getAlertDataType());
        alertNotifyVo.setNotifyType(1);
        // 向指定的人推送消息
        webSocketServer.sendMessageToConsumer(alertNotifyVo, allUserIds);

    }

    /**
     * 保存报警数据
     *
     * @param allUserIds
     * @param rule
     * @param deviceData
     */
    private List<AlertData> insertAlertData(Collection<Long> allUserIds, AlertRule rule, DeviceData deviceData) {
        // 对象拷贝
        AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class);
        alertData.setAlertRuleId(rule.getId());
        // 心率<60,持续3个周期就报警
        String alertReason = CharSequenceUtil.format("{}{}{},持续{}个周期就报警", rule.getFunctionName(), rule.getOperator(), rule.getValue(), rule.getDuration());
        alertData.setAlertReason(alertReason);
        alertData.setStatus(0);
        alertData.setType(rule.getAlertDataType());
        // 遍历allUserIds
        List<AlertData> list = allUserIds.stream().map(userId -> {
            AlertData dbAlertData = BeanUtil.toBean(alertData, AlertData.class);
            dbAlertData.setUserId(userId);
            dbAlertData.setId(null);
            return dbAlertData;
        }).collect(Collectors.toList());

        // 批量保存
        alertDataService.saveBatch(list);

        return list;
    }

前端index.vue文件:

javascript 复制代码
<template>
  <div
    :class="classObj"
    class="app-wrapper"
    :style="{ '--current-color': theme }"
  >
    <div
      v-if="device === 'mobile' && sidebar.opened"
      class="drawer-bg"
      @click="handleClickOutside"
    />
    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
    <div
      :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
      class="main-container"
    >
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
      <settings ref="settingRef" />
    </div>
  </div>
  <!-- 报警提示弹层 -->
  <Warn
    :visible="visibleWarn"
    :data="warnData"
    :time="time"
    @handleSubmit="handleSubmit"
    @handleClose="handleWarnClose"
  ></Warn>
  <!-- end -->
</template>

<script setup>
import { useWindowSize } from '@vueuse/core';
import Sidebar from './components/Sidebar/index.vue';
import Warn from '@/components/warn/index.vue';
import { AppMain, Navbar, Settings, TagsView } from './components';
import defaultSettings from '@/settings';

import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import { onMounted } from 'vue';
import useUserStore from '@/store/modules/user';
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);

const classObj = computed(() => ({
  hideSidebar: !sidebar.value.opened,
  openSidebar: sidebar.value.opened,
  withoutAnimation: sidebar.value.withoutAnimation,
  mobile: device.value === 'mobile',
}));

const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
const visibleWarn = ref(false); // 报警弹层
const operateTitle = ref(''); // 操作弹层标题
const operateText = ref(''); // 要操作的内容提示
const audioVo = ref(null);
watchEffect(() => {
  if (device.value === 'mobile' && sidebar.value.opened) {
    useAppStore().closeSideBar({ withoutAnimation: false });
  }
  if (width.value - 1 < WIDTH) {
    useAppStore().toggleDevice('mobile');
    useAppStore().closeSideBar({ withoutAnimation: true });
  } else {
    useAppStore().toggleDevice('desktop');
  }
});
onMounted(() => {
  setwebSocket();
});
function handleClickOutside() {
  useAppStore().closeSideBar({ withoutAnimation: false });
}

const settingRef = ref(null);
function setLayout() {
  settingRef.value.openSetting();
}
// 语音播报/报警异常
const socket = ref(null);
const warnData = ref({}); // 报警数据
const setwebSocket = () => {
  // TODO 一般传一个随机数字,不过本项目传的是用户id
  // const clientId = Math.random().toString(36).substr(2)
  // socket.value = new WebSocket(`ws://172.16.17.191:9000/ws/${userStore.id}`)
  console.log(`${import.meta.env.VITE_APP_SOCKET_URL}/ws/${userStore.id}`);
  socket.value = new WebSocket(
    `${import.meta.env.VITE_APP_SOCKET_URL}/ws/${userStore.id}`
  );
  socket.value.onmessage = (event) => {
    const res = JSON.parse(event.data);
    warnData.value = res;
    console.log(res);
    if (res.notifyType === 1) {
      // 报警异常
      if (res.isAllConsumer) {
        if (res.physicalLocationType === 0) {
          userStore.setUnusualFloorId(res.deviceDescription?.split(',')[0]);
        } else if (res.physicalLocationType === 2) {
          userStore.setUnusualBedId(res.deviceDescription?.split(',')[2]);
        }
      } else {
        // 添加语音播报/弹层提示
        // 报警提示弹层
        visibleWarn.value = true;
      }
    } else {
      // 解除报警异常
      if (res.physicalLocationType === 0) {
        return userStore.deleteUnusualFloorId(
          res.deviceDescription.split(',')[0]
        );
      }
      if (res.physicalLocationType === 2) {
        return userStore.deleteUnusualBedId(
          res.deviceDescription.split(',')[2]
        );
      }
    }
  };
};
// 关闭警告弹层
const handleWarnClose = () => {
  visibleWarn.value = false;
};
</script>

<style lang="scss" scoped>
@import '@/assets/styles/mixin.scss';
@import '@/assets/styles/variables.module.scss';

.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;

  &.mobile.openSidebar {
    position: fixed;
    top: 0;
  }
}

.drawer-bg {
  background: #000;
  opacity: 0.3;
  width: 100%;
  top: 0;
  height: 100%;
  position: absolute;
  z-index: 999;
}

.fixed-header {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 9;
  width: calc(100% - #{$base-sidebar-width});
  transition: width 0.28s;
}

.hideSidebar .fixed-header {
  width: calc(100% - 54px);
}

.sidebarHide .fixed-header {
  width: 100%;
}

.mobile .fixed-header {
  width: 100%;
}
</style>

然后修改前端的ws的连接地址:

由于前端要发请求到后端建立连接,咱们还需要在SpringSecurity中放行ws开头的请求:

代码推送:

相关推荐
im_AMBER2 小时前
Web 开发 21
前端·学习
又是忙碌的一天2 小时前
前端学习day01
前端·学习·html
popoxf2 小时前
spring容器启动流程(反射视角)
java·后端·spring
月白风清江有声2 小时前
安装适用于 GPU的NVIDIA显卡驱动及Linux GUI 应用
学习
2401_831501733 小时前
Python学习之day03学习(文件和异常)
开发语言·python·学习
Zwb2997923 小时前
Day 24 - 文件、目录与路径 - Python学习笔记
笔记·python·学习
AAA修煤气灶刘哥3 小时前
监控摄像头?不,我们管这个叫优雅的埋点艺术!
java·后端·spring cloud
寻星探路3 小时前
Java EE初阶启程记09---多线程案例(2)
java·开发语言·java-ee
武子康3 小时前
Java-141 深入浅出 MySQL Spring事务失效的常见场景与解决方案详解(3)
java·数据库·mysql·spring·性能优化·系统架构·事务