Explorar o código

fix(报警推送): 修复报警推送功能无效的问题

该问题原因为 com.genersoft.iot.vmp.conf.GlobalResponseAdvice 类改变了 sse 响应体的数据结构,导致前端无法正确解析 sse 数据,调试后未发现 GlobalResponseAdvice 如何修改的 sse 数据结构,故根据 sse 消息体结构自定义实现了 sse 连接
xiaoQQya %!s(int64=2) %!d(string=hai) anos
pai
achega
f78657473e

+ 7 - 4
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java

@@ -1,12 +1,12 @@
 package com.genersoft.iot.vmp.conf.security;
 
 import com.genersoft.iot.vmp.conf.UserSetting;
-import org.springframework.core.annotation.Order;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -28,6 +28,7 @@ import java.util.Arrays;
 
 /**
  * 配置Spring Security
+ *
  * @author lin
  */
 @Configuration
@@ -75,6 +76,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
             matchers.add("/js/**");
             matchers.add("/api/device/query/snap/**");
             matchers.add("/record_proxy/*/**");
+            matchers.add("/api/emit");
             matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
             // 可以直接访问的静态数据
             web.ignoring().antMatchers(matchers.toArray(new String[0]));
@@ -83,6 +85,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     /**
      * 配置认证方式
+     *
      * @param auth
      * @throws Exception
      */
@@ -111,7 +114,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .authorizeRequests()
                 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                 .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll()
-                .antMatchers("/api/user/login","/index/hook/**","/zlm_Proxy/FhTuMYqB2HeCuNOb/record/t/1/2023-03-25/16:35:07-16:35:16-9353.mp4").permitAll()
+                .antMatchers("/api/user/login", "/index/hook/**").permitAll()
                 .anyRequest().authenticated()
                 // 异常处理器
                 .and()
@@ -124,7 +127,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     }
 
-    CorsConfigurationSource configurationSource(){
+    CorsConfigurationSource configurationSource() {
         // 配置跨域
         CorsConfiguration corsConfiguration = new CorsConfiguration();
         corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
@@ -135,7 +138,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
         corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
 
         UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
-        url.registerCorsConfiguration("/**",corsConfiguration);
+        url.registerCorsConfiguration("/**", corsConfiguration);
         return url;
     }
 

+ 44 - 31
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java

@@ -1,55 +1,68 @@
 package com.genersoft.iot.vmp.gb28181.event.alarm;
 
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.context.ApplicationListener;
 import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import java.io.IOException;
-import java.util.Hashtable;
+
+import java.io.PrintWriter;
 import java.util.Iterator;
 import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * @description: 报警事件监听
- * @author: lawrencehj
- * @data: 2021-01-20
+ * 报警事件监听器.
+ *
+ * @author lawrencehj
+ * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
+ * @since 2021/01/20
  */
-
 @Component
 public class AlarmEventListener implements ApplicationListener<AlarmEvent> {
 
-    private final static Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
+    private static final Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
 
-    private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
+    private static final Map<String, PrintWriter> SSE_CACHE = new ConcurrentHashMap<>();
 
-    public void addSseEmitters(String browserId, SseEmitter sseEmitter) {
-        sseEmitters.put(browserId, sseEmitter);
+    public void addSseEmitter(String browserId, PrintWriter writer) {
+        SSE_CACHE.put(browserId, writer);
+        logger.info("SSE 在线数量: {}", SSE_CACHE.size());
+    }
+
+    public void removeSseEmitter(String browserId, PrintWriter writer) {
+        SSE_CACHE.remove(browserId, writer);
+        logger.info("SSE 在线数量: {}", SSE_CACHE.size());
     }
 
     @Override
-    public void onApplicationEvent(AlarmEvent event) {
+    public void onApplicationEvent(@NotNull AlarmEvent event) {
         if (logger.isDebugEnabled()) {
-            logger.debug("设备报警事件触发,deviceId:" + event.getAlarmInfo().getDeviceId() + ", "
-                    + event.getAlarmInfo().getAlarmDescription());
+            logger.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription());
         }
-        String msg = "<strong>设备编码:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
-                    + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
-                    + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>"
-                    + "<br><strong>报警位置:</strong> <i>" + event.getAlarmInfo().getLongitude() + "</i>"
-                    + ", <i>" + event.getAlarmInfo().getLatitude() + "</i>";
-
-        for (Iterator<Map.Entry<String, SseEmitter>> it = sseEmitters.entrySet().iterator(); it.hasNext();) {
-            Map.Entry<String, SseEmitter> emitter = it.next();
-            logger.info("推送到SSE连接,浏览器ID: " + emitter.getKey());
+
+        String msg = "<strong>设备编号:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
+                + "<br><strong>通道编号:</strong> <i>" + event.getAlarmInfo().getChannelId() + "</i>"
+                + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
+                + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>";
+
+        for (Iterator<Map.Entry<String, PrintWriter>> it = SSE_CACHE.entrySet().iterator(); it.hasNext(); ) {
+            Map.Entry<String, PrintWriter> response = it.next();
+            logger.info("推送到 SSE 连接, 浏览器 ID: {}", response.getKey());
             try {
-                emitter.getValue().send(msg);
-            } catch (IOException | IllegalStateException e) {
-                if (logger.isDebugEnabled()) {
-                    logger.debug("SSE连接已关闭");
+                PrintWriter writer = response.getValue();
+
+                if (writer.checkError()) {
+                    it.remove();
+                    continue;
                 }
-                // 移除已关闭的连接
+
+                String sseMsg = "event:message\n" +
+                        "data:" + msg + "\n" +
+                        "\n";
+                writer.write(sseMsg);
+                writer.flush();
+            } catch (Exception e) {
                 it.remove();
             }
         }

+ 0 - 37
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java

@@ -1,37 +0,0 @@
-package com.genersoft.iot.vmp.vmanager.gb28181.SseController;
-
-import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
-
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
-/**
- * @description: SSE推送
- * @author: lawrencehj
- * @data: 2021-01-20
- */
-@Tag(name  = "SSE推送")
-
-@Controller
-@RequestMapping("/api")
-public class SseController {
-    @Autowired
-    AlarmEventListener alarmEventListener;
-
-    @GetMapping("/emit")
-    public SseEmitter emit(@RequestParam String browserId) {
-        final SseEmitter sseEmitter = new SseEmitter(0L);
-        try {
-            alarmEventListener.addSseEmitters(browserId, sseEmitter);
-        }catch (Exception e){
-            sseEmitter.completeWithError(e);
-        }
-        return sseEmitter;
-    }
-}

+ 55 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/sse/SseController.java

@@ -0,0 +1,55 @@
+package com.genersoft.iot.vmp.vmanager.gb28181.sse;
+
+import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+
+/**
+ * SSE 推送.
+ *
+ * @author lawrencehj
+ * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
+ * @since 2021/01/20
+ */
+@Tag(name = "SSE 推送")
+@RestController
+@RequestMapping("/api")
+public class SseController {
+
+    @Resource
+    private AlarmEventListener alarmEventListener;
+
+    /**
+     * SSE 推送.
+     *
+     * @param response  响应
+     * @param browserId 浏览器ID
+     * @throws IOException IOEXCEPTION
+     * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
+     * @since 2023/11/06
+     */
+    @GetMapping("/emit")
+    public void emit(HttpServletResponse response, @RequestParam String browserId) throws IOException, InterruptedException {
+        response.setContentType("text/event-stream");
+        response.setCharacterEncoding("utf-8");
+
+        PrintWriter writer = response.getWriter();
+        alarmEventListener.addSseEmitter(browserId, writer);
+
+        while (!writer.checkError()) {
+            Thread.sleep(1000);
+            writer.write(":keep alive\n\n");
+            writer.flush();
+        }
+        alarmEventListener.removeSseEmitter(browserId, writer);
+    }
+}

+ 9 - 7
web_src/src/layout/UiHeader.vue

@@ -37,7 +37,6 @@
 </template>
 
 <script>
-
 import changePasswordDialog from '../components/dialog/changePassword.vue'
 import userService from '../components/service/UserService'
 import {Notification} from 'element-ui';
@@ -55,18 +54,19 @@ export default {
     };
   },
   created() {
-    console.log(4444)
     console.log(JSON.stringify(userService.getUser()))
     if (this.$route.path.startsWith("/channelList")) {
       this.activeIndex = "/deviceList"
-
     }
   },
   mounted() {
     window.addEventListener('beforeunload', e => this.beforeunloadHandler(e))
-    // window.addEventListener('unload', e => this.unloadHandler(e))
     this.alarmNotify = this.getAlarmSwitchStatus() === "true";
-    this.sseControl();
+
+    // TODO: 此处延迟连接 sse, 避免 sse 连接时 browserId 还未生成, 后续待优化
+    setTimeout(() => {
+      this.sseControl()
+    }, 3000);
   },
   methods: {
     loginout() {
@@ -107,10 +107,12 @@ export default {
         this.sseSource = new EventSource('/api/emit?browserId=' + this.$browserId);
         this.sseSource.addEventListener('message', function (evt) {
           that.$notify({
-            title: '收到报警信息',
+            title: '报警信息',
             dangerouslyUseHTMLString: true,
             message: evt.data,
-            type: 'warning'
+            type: 'warning',
+            position: 'bottom-right',
+            duration: 3000
           });
           console.log("收到信息:" + evt.data);
         });

+ 5 - 8
web_src/src/main.js

@@ -1,22 +1,20 @@
 import Vue from 'vue';
 import App from './App.vue';
-
-Vue.config.productionTip = false;
-import ElementUI from 'element-ui';
+import ElementUI, {Notification} from 'element-ui';
 import 'element-ui/lib/theme-chalk/index.css';
 import router from './router/index.js';
 import axios from 'axios';
 import VueCookies from 'vue-cookies';
-import echarts from 'echarts';
 import VCharts from 'v-charts';
 
 import VueClipboard from 'vue-clipboard2';
-import {Notification} from 'element-ui';
 import Fingerprint2 from 'fingerprintjs2';
 import VueClipboards from 'vue-clipboards';
 import Contextmenu from "vue-contextmenujs"
 import userService from "./components/service/UserService"
 
+Vue.config.productionTip = false;
+
 
 // 生成唯一ID
 Fingerprint2.get(function (components) {
@@ -29,10 +27,9 @@ Fingerprint2.get(function (components) {
   //console.log(values)  //使用的浏览器信息npm
   // 生成最终id
   let port = window.location.port;
-  console.log(port);
   const fingerPrint = Fingerprint2.x64hash128(values.join(port), 31)
   Vue.prototype.$browserId = fingerPrint;
-  console.log("唯一标识码:" + fingerPrint);
+  console.log("浏览器 ID: " + fingerPrint);
 });
 
 Vue.use(VueClipboard);
@@ -75,7 +72,7 @@ axios.interceptors.request.use(
 );
 
 Vue.prototype.$axios = axios;
-Vue.prototype.$cookies.config(60*30);
+Vue.prototype.$cookies.config(60 * 30);
 
 new Vue({
   router: router,