xiaoQQya
2023-11-06 f78657473e85487c09cd3c10112db48a0c4a7c4e
fix(报警推送): 修复报警推送功能无效的问题

该问题原因为 com.genersoft.iot.vmp.conf.GlobalResponseAdvice 类改变了 sse 响应体的数据结构,导致前端无法正确解析 sse 数据,调试后未发现 GlobalResponseAdvice 如何修改的 sse 数据结构,故根据 sse 消息体结构自定义实现了 sse 连接
4个文件已修改
1个文件已添加
1个文件已删除
209 ■■■■■ 已修改文件
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/sse/SseController.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/layout/UiHeader.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/main.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 @@
/**
 * 配置Spring Security
 *
 * @author lin
 */
@Configuration
@@ -75,6 +76,7 @@
            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 @@
    /**
     * 配置认证方式
     *
     * @param auth
     * @throws Exception
     */
@@ -111,7 +114,7 @@
                .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 @@
    }
    CorsConfigurationSource configurationSource(){
    CorsConfigurationSource configurationSource() {
        // 配置跨域
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
@@ -135,7 +138,7 @@
        corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
        UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
        url.registerCorsConfiguration("/**",corsConfiguration);
        url.registerCorsConfiguration("/**", corsConfiguration);
        return url;
    }
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.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.util.Iterator;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;
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();
            }
        }
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java
File was deleted
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/sse/SseController.java
New file
@@ -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);
    }
}
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 @@
    };
  },
  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 @@
        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);
        });
web_src/src/main.js
@@ -1,21 +1,19 @@
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
@@ -29,10 +27,9 @@
  //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 @@
);
Vue.prototype.$axios = axios;
Vue.prototype.$cookies.config(60*30);
Vue.prototype.$cookies.config(60 * 30);
new Vue({
  router: router,