lipenghui hai 2 semanas
pai
achega
648464b0c4

+ 105 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/config/AlarmMessage.java

@@ -0,0 +1,105 @@
+package cn.iocoder.yudao.module.pms.config;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+@Data
+public class AlarmMessage {
+    /**
+     * 告警ID
+     */
+    @JsonProperty("alarmId")
+    private String alarmId;
+    
+    /**
+     * 告警类型
+     * 1:移动侦测 2:视频丢失 3:视频遮挡 4:区域入侵 5:越界侦测
+     */
+    @JsonProperty("alarmType")
+    private Integer alarmType;
+    
+    /**
+     * 告警类型名称
+     */
+    @JsonProperty("alarmTypeName")
+    private String alarmTypeName;
+    
+    /**
+     * 设备ID
+     */
+    @JsonProperty("deviceId")
+    private String deviceId;
+    
+    /**
+     * 设备名称
+     */
+    @JsonProperty("deviceName")
+    private String deviceName;
+    
+    /**
+     * 通道号
+     */
+    @JsonProperty("channelNo")
+    private Integer channelNo;
+    
+    /**
+     * 通道名称
+     */
+    @JsonProperty("channelName")
+    private String channelName;
+    
+    /**
+     * 告警时间
+     */
+    @JsonProperty("alarmTime")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date alarmTime;
+    
+    /**
+     * 告警描述
+     */
+    @JsonProperty("alarmDescription")
+    private String alarmDescription;
+    
+    /**
+     * 告警级别
+     * 1:低 2:中 3:高
+     */
+    @JsonProperty("alarmLevel")
+    private Integer alarmLevel;
+    
+    /**
+     * 告警状态
+     * 0:开始 1:停止
+     */
+    @JsonProperty("alarmStatus")
+    private Integer alarmStatus;
+    
+    /**
+     * 图片URL(如果有)
+     */
+    @JsonProperty("pictureUrl")
+    private String pictureUrl;
+    
+    /**
+     * 录像URL(如果有)
+     */
+    @JsonProperty("videoUrl")
+    private String videoUrl;
+    
+    /**
+     * 扩展字段
+     */
+    @JsonProperty("extra")
+    private Map<String, Object> extra;
+    
+    /**
+     * 接收时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime = new Date();
+}

+ 146 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/config/HikvisionAuthTest.java

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.pms.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+@Slf4j
+public class HikvisionAuthTest {
+    
+    /**
+     * 测试海康设备认证
+     */
+    public static void testHikvisionAuth(String ip, String username, String password) {
+        String baseUrl = "http://" + ip;
+        
+        log.info("测试海康设备认证...");
+        log.info("设备IP: {}", ip);
+        log.info("用户名: {}", username);
+        
+        try (CloseableHttpClient httpClient = createHikvisionHttpClient(ip, username, password)) {
+            
+            // 测试1: 获取设备信息
+            testDeviceInfo(httpClient, baseUrl);
+            
+            // 测试2: 测试订阅能力
+            testNotificationCapabilities(httpClient, baseUrl);
+            
+            // 测试3: 尝试订阅
+            testSubscribe(httpClient, baseUrl, "192.168.1.100", 8080);
+            
+        } catch (Exception e) {
+            log.error("测试失败", e);
+        }
+    }
+    
+    private static CloseableHttpClient createHikvisionHttpClient(String ip, String username, String password) {
+        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope(ip, 80),
+                new UsernamePasswordCredentials(username, password)
+        );
+        
+        return HttpClients.custom()
+                .setDefaultCredentialsProvider(credentialsProvider)
+                .build();
+    }
+    
+    private static void testDeviceInfo(CloseableHttpClient client, String baseUrl) throws Exception {
+        String url = baseUrl + "/ISAPI/System/deviceInfo";
+        HttpGet request = new HttpGet(url);
+        request.setHeader("Accept", "application/xml");
+        
+        log.info("测试设备信息接口: {}", url);
+        HttpResponse response = client.execute(request);
+        
+        int statusCode = response.getStatusLine().getStatusCode();
+        log.info("响应状态码: {}", statusCode);
+        
+        if (statusCode == 200) {
+            HttpEntity entity = response.getEntity();
+            String responseBody = EntityUtils.toString(entity);
+            log.info("设备信息响应:");
+            log.info("{}", responseBody.substring(0, Math.min(responseBody.length(), 500)));
+        } else if (statusCode == 401) {
+            log.error("认证失败 (401) - 可能原因:");
+            log.error("1. 用户名密码错误");
+            log.error("2. 需要摘要认证(Digest Auth)");
+            log.error("3. 用户权限不足");
+            
+            // 尝试Basic Auth
+            testBasicAuth(baseUrl);
+        }
+    }
+    
+    private static void testBasicAuth(String baseUrl) throws Exception {
+        log.info("尝试Basic Auth...");
+        String url = baseUrl + "/ISAPI/System/deviceInfo";
+        
+        try (CloseableHttpClient client = HttpClients.createDefault()) {
+            HttpGet request = new HttpGet(url);
+            request.setHeader("Accept", "application/xml");
+            // Basic Auth头
+            String auth = "admin:admin123";
+            String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes());
+            request.setHeader("Authorization", "Basic " + encodedAuth);
+            
+            HttpResponse response = client.execute(request);
+            log.info("Basic Auth响应状态码: {}", response.getStatusLine().getStatusCode());
+        }
+    }
+    
+    private static void testNotificationCapabilities(CloseableHttpClient client, String baseUrl) throws Exception {
+        String url = baseUrl + "/ISAPI/Event/notification/capabilities";
+        HttpGet request = new HttpGet(url);
+        request.setHeader("Accept", "application/xml");
+        
+        HttpResponse response = client.execute(request);
+        log.info("事件通知能力测试 - 状态码: {}", response.getStatusLine().getStatusCode());
+    }
+    
+    private static void testSubscribe(CloseableHttpClient client, String baseUrl, String localIp, int localPort) throws Exception {
+        String url = baseUrl + "/ISAPI/Event/notification/subscribe";
+        HttpPost request = new HttpPost(url);
+        request.setHeader("Content-Type", "application/xml");
+        request.setHeader("Accept", "application/xml");
+        
+        String xmlBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                "<SubscribeRequest version=\"1.0\" xmlns=\"http://www.isapi.org/ver20/XMLSchema\">\n" +
+                "  <SubscribeDetail>\n" +
+                "    <Address>" + localIp + "</Address>\n" +
+                "    <Port>" + localPort + "</Port>\n" +
+                "    <Protocol>HTTP</Protocol>\n" +
+                "    <SubscribeDuration>3600</SubscribeDuration>\n" +
+                "  </SubscribeDetail>\n" +
+                "</SubscribeRequest>";
+        
+        request.setEntity(new StringEntity(xmlBody));
+        
+        HttpResponse response = client.execute(request);
+        log.info("订阅测试 - 状态码: {}", response.getStatusLine().getStatusCode());
+        
+        if (response.getStatusLine().getStatusCode() == 200) {
+            String responseBody = EntityUtils.toString(response.getEntity());
+            log.info("订阅响应:\n{}", responseBody);
+        }
+    }
+    
+    public static void main(String[] args) {
+        // 修改为你的设备信息
+        String ip = "172.26.0.52";
+        String username = "admin";
+        String password = "admin123"; // 请使用实际密码
+        
+        testHikvisionAuth(ip, username, password);
+    }
+}

+ 62 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/config/HikvisionHttpClient.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.pms.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+public class HikvisionHttpClient {
+    
+    /**
+     * 创建支持海康设备认证的RestTemplate
+     */
+    public RestTemplate createHikvisionRestTemplate(String ip, int port, String username, String password) {
+        try {
+            // 创建凭证提供者
+            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+            credentialsProvider.setCredentials(
+                    new AuthScope(ip, port),
+                    new UsernamePasswordCredentials(username, password)
+            );
+            
+            // 配置超时
+            RequestConfig requestConfig = RequestConfig.custom()
+                    .setConnectTimeout(10000)
+                    .setSocketTimeout(30000)
+                    .setConnectionRequestTimeout(5000)
+                    .build();
+            
+            // 创建HTTP客户端
+            CloseableHttpClient httpClient = HttpClients.custom()
+                    .setDefaultCredentialsProvider(credentialsProvider)
+                    .setDefaultRequestConfig(requestConfig)
+                    .setRedirectStrategy(new LaxRedirectStrategy()) // 允许重定向
+                    .disableCookieManagement() // 海康设备可能有cookie问题
+                    .build();
+            
+            // 创建RestTemplate
+            HttpComponentsClientHttpRequestFactory factory = 
+                    new HttpComponentsClientHttpRequestFactory(httpClient);
+            
+            return new RestTemplate(factory);
+            
+        } catch (Exception e) {
+            log.error("创建海康HTTP客户端失败", e);
+            // 返回普通RestTemplate作为fallback
+            return new RestTemplate();
+        }
+    }
+}

+ 101 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/alarm/AlarmController.java

@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.pms.controller.admin.alarm;
+
+import cn.iocoder.yudao.module.pms.config.AlarmMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/alarm")
+public class AlarmController {
+    
+    @Autowired
+    private AlarmService alarmService;
+    
+    /**
+     * 接收海康超脑告警推送(HTTP POST方式)
+     * 海康设备会向此接口推送告警信息
+     */
+    @PostMapping("/receive")
+    public ResponseEntity<Map<String, Object>> receiveAlarm(
+            @RequestBody AlarmMessage alarmMessage,
+            HttpServletRequest request) {
+
+        System.out.println("收到海康超脑告警推送,来源IP: "+request.getRemoteAddr());
+        System.out.println("告警消息内容: "+ alarmMessage);
+        
+        try {
+            // 处理告警消息
+            alarmService.processAlarm(alarmMessage);
+            
+            // 返回成功响应
+            Map<String, Object> response = new HashMap<>();
+            response.put("code", 0);
+            response.put("message", "告警接收成功");
+            response.put("timestamp", System.currentTimeMillis());
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (Exception e) {
+            log.error("处理告警消息失败", e);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("code", -1);
+            response.put("message", "处理告警失败: " + e.getMessage());
+            response.put("timestamp", System.currentTimeMillis());
+            
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+    
+    /**
+     * 接收海康超脑告警推送(兼容GET方式,部分设备可能使用GET)
+     */
+    @GetMapping("/receive")
+    public ResponseEntity<Map<String, Object>> receiveAlarmGet(
+            @RequestParam Map<String, String> params,
+            HttpServletRequest request) {
+
+        System.out.println("收到海康超脑GET告警推送,参数: "+ params);
+        
+        try {
+            // 将GET参数转换为告警消息
+            AlarmMessage alarmMessage = alarmService.convertParamsToAlarm(params);
+            alarmService.processAlarm(alarmMessage);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("code", 0);
+            response.put("message", "告警接收成功");
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (Exception e) {
+            log.error("处理GET告警消息失败", e);
+            
+            Map<String, Object> response = new HashMap<>();
+            response.put("code", -1);
+            response.put("message", "处理告警失败");
+            
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+        }
+    }
+    
+    /**
+     * 心跳检测接口
+     */
+    @GetMapping("/health")
+    public ResponseEntity<Map<String, Object>> healthCheck() {
+        Map<String, Object> response = new HashMap<>();
+        response.put("status", "UP");
+        response.put("service", "Hikvision Alarm Receiver");
+        response.put("timestamp", System.currentTimeMillis());
+        return ResponseEntity.ok(response);
+    }
+}

+ 29 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/alarm/AlarmService.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.pms.controller.admin.alarm;
+
+
+import cn.iocoder.yudao.module.pms.config.AlarmMessage;
+
+import java.util.Map;
+
+public interface AlarmService {
+    
+    /**
+     * 处理告警消息
+     */
+    void processAlarm(AlarmMessage alarmMessage);
+    
+    /**
+     * 将GET参数转换为告警消息
+     */
+    AlarmMessage convertParamsToAlarm(Map<String, String> params);
+    
+    /**
+     * 向海康超脑订阅告警
+     */
+    boolean subscribeAlarm();
+    
+    /**
+     * 取消告警订阅
+     */
+    boolean unsubscribeAlarm();
+}

+ 410 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/alarm/AlarmServiceImpl.java

@@ -0,0 +1,410 @@
+package cn.iocoder.yudao.module.pms.controller.admin.alarm;
+
+import cn.iocoder.yudao.module.pms.config.AlarmMessage;
+import cn.iocoder.yudao.module.pms.config.HikvisionConfig;
+import cn.iocoder.yudao.module.pms.config.HikvisionHttpClient;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.*;
+
+@Slf4j
+@Service
+@EnableScheduling
+public class AlarmServiceImpl implements AlarmService {
+
+    @Autowired
+    private HikvisionConfig hikvisionConfig;
+
+    @Autowired
+    private HikvisionHttpClient hikvisionHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private RestTemplate hikRestTemplate;
+    private String subscribeId;
+
+    @PostConstruct
+    public void init() {
+        // 创建专用的HTTP客户端
+        hikRestTemplate = hikvisionHttpClient.createHikvisionRestTemplate(
+                hikvisionConfig.getIp(),
+                hikvisionConfig.getPort(),
+                hikvisionConfig.getUsername(),
+                hikvisionConfig.getPassword()
+        );
+
+        if (hikvisionConfig.getAlarm().getSubscribe().getEnabled()) {
+            System.out.println("开始订阅海康超脑告警...");
+            System.out.println("超脑IP: 端口: "+ hikvisionConfig.getIp()+"11111"+ hikvisionConfig.getPort());
+
+            // 1. 先测试连接
+            if (testDeviceConnection()) {
+                // 2. 订阅告警
+                boolean success = subscribeAlarm();
+                if (success) {
+                    System.out.println("海康超脑告警订阅成功,订阅ID: "+ subscribeId);
+                } else {
+                    log.error("海康超脑告警订阅失败");
+                }
+            } else {
+                log.error("设备连接失败,无法进行订阅");
+            }
+        } else {
+            System.out.println("告警订阅功能已禁用");
+        }
+    }
+
+    /**
+     * 测试设备连接(正确的ISAPI调用)
+     */
+    private boolean testDeviceConnection() {
+        try {
+            // 海康ISAPI设备信息接口
+            String url = String.format("http://%s:%d/ISAPI/System/deviceInfo",
+                    hikvisionConfig.getIp(),
+                    hikvisionConfig.getPort());
+
+            System.out.println("测试设备连接,URL: "+ url);
+
+            // 尝试多种内容类型
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Accept", "application/xml, application/json");
+
+            HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+
+            ResponseEntity<String> response = hikRestTemplate.exchange(
+                    url, HttpMethod.GET, requestEntity, String.class);
+
+            System.out.println("连接测试响应状态码: "+ response.getStatusCode());
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                String responseBody = response.getBody();
+                log.info("设备连接成功");
+
+                // 解析响应
+                if (responseBody != null) {
+                    parseDeviceResponse(responseBody);
+                }
+                return true;
+            } else if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
+                log.error("认证失败 (401) - 请检查用户名密码");
+                // 尝试其他认证方式
+                return tryAlternativeAuthentication();
+            } else {
+                log.error("连接失败,状态码: {}", response.getStatusCode());
+                return false;
+            }
+
+        } catch (Exception e) {
+            log.error("设备连接测试异常", e);
+
+            // 检查具体错误
+            if (e.getMessage().contains("401")) {
+                log.error("认证失败,可能原因:");
+                log.error("1. 用户名密码错误");
+                log.error("2. 设备使用摘要认证(Digest Auth)");
+                log.error("3. 用户权限不足");
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * 尝试其他认证方式
+     */
+    private boolean tryAlternativeAuthentication() {
+        log.info("尝试其他认证方式...");
+
+        try {
+            // 方法1: 尝试直接使用Basic Auth
+            String url = String.format("http://%s:%d/ISAPI/System/deviceInfo",
+                    hikvisionConfig.getIp(),
+                    hikvisionConfig.getPort());
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setBasicAuth(hikvisionConfig.getUsername(), hikvisionConfig.getPassword());
+            headers.set("Accept", "application/xml");
+
+            HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+
+            RestTemplate basicAuthTemplate = new RestTemplate();
+            ResponseEntity<String> response = basicAuthTemplate.exchange(
+                    url, HttpMethod.GET, requestEntity, String.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                log.info("Basic Auth认证成功");
+                return true;
+            }
+
+        } catch (Exception e) {
+            log.warn("Basic Auth尝试失败: {}", e.getMessage());
+        }
+
+        return false;
+    }
+
+    /**
+     * 解析设备响应
+     */
+    private void parseDeviceResponse(String responseBody) {
+        try {
+            // 判断是XML还是JSON
+            if (responseBody.trim().startsWith("<?xml") || responseBody.trim().startsWith("<")) {
+                // XML格式
+                log.info("设备使用XML格式响应");
+                parseXmlDeviceInfo(responseBody);
+            } else if (responseBody.trim().startsWith("{") || responseBody.trim().startsWith("[")) {
+                // JSON格式
+                log.info("设备使用JSON格式响应");
+                parseJsonDeviceInfo(responseBody);
+            } else {
+                log.info("设备响应格式未知,内容前100字符: {}",
+                        responseBody.substring(0, Math.min(responseBody.length(), 100)));
+            }
+        } catch (Exception e) {
+            log.warn("解析设备响应失败", e);
+        }
+    }
+
+    private void parseXmlDeviceInfo(String xml) {
+        try {
+            // 简化解析,实际应使用XML解析器
+            if (xml.contains("<deviceName>")) {
+                int start = xml.indexOf("<deviceName>") + 12;
+                int end = xml.indexOf("</deviceName>");
+                if (end > start) {
+                    String deviceName = xml.substring(start, end);
+                    log.info("设备名称: {}", deviceName);
+                }
+            }
+            if (xml.contains("<model>")) {
+                int start = xml.indexOf("<model>") + 7;
+                int end = xml.indexOf("</model>");
+                if (end > start) {
+                    String model = xml.substring(start, end);
+                    log.info("设备型号: {}", model);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("解析XML设备信息失败", e);
+        }
+    }
+
+    private void parseJsonDeviceInfo(String json) {
+        try {
+            Map<?, ?> data = objectMapper.readValue(json, Map.class);
+            Map<?, ?> deviceInfo = (Map<?, ?>) data.get("DeviceInfo");
+            if (deviceInfo != null) {
+                log.info("设备名称: {}", deviceInfo.get("deviceName"));
+                log.info("设备型号: {}", deviceInfo.get("model"));
+                log.info("序列号: {}", deviceInfo.get("serialNumber"));
+            }
+        } catch (Exception e) {
+            log.warn("解析JSON设备信息失败", e);
+        }
+    }
+
+    @Override
+    public boolean subscribeAlarm() {
+        try {
+            // 先获取设备能力,确认支持订阅
+            if (!checkEventNotificationSupport()) {
+                log.error("设备不支持事件通知功能");
+                return false;
+            }
+
+            // 构建订阅请求XML(海康ISAPI标准格式)
+            String requestXml = buildISAPISubscribeRequest();
+            log.debug("订阅请求XML:\n{}", requestXml);
+
+            String url = String.format("http://%s:%d/ISAPI/Event/notification/subscribe",
+                    hikvisionConfig.getIp(),
+                    hikvisionConfig.getPort());
+
+            log.info("订阅URL: {}", url);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_XML);
+            headers.set("Accept", "application/xml");
+
+            HttpEntity<String> requestEntity = new HttpEntity<>(requestXml, headers);
+
+            log.info("发送订阅请求...");
+            ResponseEntity<String> response = hikRestTemplate.exchange(
+                    url, HttpMethod.POST, requestEntity, String.class);
+
+            log.info("订阅响应状态码: {}", response.getStatusCode());
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                String responseBody = response.getBody();
+                log.info("订阅成功");
+                log.debug("订阅响应:\n{}", responseBody);
+
+                // 解析订阅ID
+                subscribeId = parseSubscriptionId(responseBody);
+                if (subscribeId != null) {
+                    log.info("获取到订阅ID: {}", subscribeId);
+                } else {
+                    // 如果没有订阅ID,生成一个本地ID
+                    subscribeId = "local-sub-" + System.currentTimeMillis();
+                    log.info("使用本地订阅ID: {}", subscribeId);
+                }
+                return true;
+            } else {
+                log.error("订阅失败,状态码: {}", response.getStatusCode());
+                if (response.getBody() != null) {
+                    log.error("错误响应: {}", response.getBody());
+                }
+                return false;
+            }
+
+        } catch (Exception e) {
+            log.error("订阅告警失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 检查设备是否支持事件通知
+     */
+    private boolean checkEventNotificationSupport() {
+        try {
+            String url = String.format("http://%s:%d/ISAPI/Event/notification/capabilities",
+                    hikvisionConfig.getIp(),
+                    hikvisionConfig.getPort());
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Accept", "application/xml");
+
+            HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+
+            ResponseEntity<String> response = hikRestTemplate.exchange(
+                    url, HttpMethod.GET, requestEntity, String.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                log.info("设备支持事件通知功能");
+                return true;
+            }
+
+        } catch (Exception e) {
+            log.warn("检查事件通知能力失败: {}", e.getMessage());
+        }
+
+        return false;
+    }
+
+    /**
+     * 构建ISAPI标准订阅请求XML
+     */
+    private String buildISAPISubscribeRequest() {
+        String localIp = hikvisionConfig.getAlarm().getSubscribe().getLocalIp();
+        int localPort = hikvisionConfig.getAlarm().getSubscribe().getLocalPort();
+        int duration = hikvisionConfig.getAlarm().getSubscribe().getSubscribeDuration();
+
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                "<SubscribeRequest version=\"1.0\" xmlns=\"http://www.isapi.org/ver20/XMLSchema\">\n" +
+                "  <SubscribeDetail version=\"2.0\">\n" +
+                "    <Address>" + localIp + "</Address>\n" +
+                "    <Port>" + localPort + "</Port>\n" +
+                "    <Protocol>HTTP</Protocol>\n" +
+                "    <SubscribeDuration>" + duration + "</SubscribeDuration>\n" +
+                "    <EventTypes>\n" +
+                "      <EventType version=\"2.0\">\n" +
+                "        <Type>VideoLoss</Type>\n" +
+                "        <Active>true</Active>\n" +
+                "      </EventType>\n" +
+                "      <EventType version=\"2.0\">\n" +
+                "        <Type>VMD</Type>\n" +
+                "        <Active>true</Active>\n" +
+                "      </EventType>\n" +
+                "      <EventType version=\"2.0\">\n" +
+                "        <Type>VideoTamper</Type>\n" +
+                "        <Active>true</Active>\n" +
+                "      </EventType>\n" +
+                "    </EventTypes>\n" +
+                "  </SubscribeDetail>\n" +
+                "</SubscribeRequest>";
+    }
+
+    /**
+     * 解析订阅ID
+     */
+    private String parseSubscriptionId(String responseXml) {
+        try {
+            if (responseXml.contains("<subscribeId>")) {
+                int start = responseXml.indexOf("<subscribeId>") + 13;
+                int end = responseXml.indexOf("</subscribeId>");
+                if (end > start) {
+                    return responseXml.substring(start, end);
+                }
+            } else if (responseXml.contains("subscriptionId")) {
+                // 可能是其他格式
+                int start = responseXml.indexOf("subscriptionId") + 15;
+                int end = responseXml.indexOf("\"", start);
+                if (end > start) {
+                    return responseXml.substring(start, end);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("解析订阅ID失败", e);
+        }
+        return null;
+    }
+
+    @Override
+    public boolean unsubscribeAlarm() {
+        if (subscribeId == null) {
+            return true;
+        }
+
+        try {
+            String url = String.format("http://%s:%d/ISAPI/Event/notification/unsubscribe/%s",
+                    hikvisionConfig.getIp(),
+                    hikvisionConfig.getPort(),
+                    subscribeId);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Accept", "application/xml");
+
+            HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+
+            ResponseEntity<String> response = hikRestTemplate.exchange(
+                    url, HttpMethod.DELETE, requestEntity, String.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                log.info("成功取消订阅: {}", subscribeId);
+                subscribeId = null;
+                return true;
+            }
+
+        } catch (Exception e) {
+            log.error("取消订阅失败", e);
+        }
+
+        return false;
+    }
+
+    // 其他方法保持不变...
+    @Override
+    public void processAlarm(AlarmMessage alarmMessage) {
+        log.info("处理告警: {}", alarmMessage.getAlarmDescription());
+        // 处理逻辑...
+    }
+
+    @Override
+    public AlarmMessage convertParamsToAlarm(Map<String, String> params) {
+        AlarmMessage alarmMessage = new AlarmMessage();
+        // 转换逻辑...
+        return alarmMessage;
+    }
+}

+ 22 - 0
yudao-server/src/main/resources/application-dev.yaml

@@ -295,6 +295,28 @@ oa:
 file:
 file:
   upload-path: D:\wenjian
   upload-path: D:\wenjian
 
 
+
+hikvision:
+  superbrain:
+    ip: 172.26.0.52
+    port: 80
+    username: admin
+    password: rqny@szh2026
+    alarm:
+      subscribe:
+        enabled: true
+        local-ip: 192.168.188.149
+        local-port: 8080
+        protocol: HTTP
+        format: JSON  # 新增:指定格式为JSON
+        subscribe-duration: 3600
+        alarm-types:
+          - "VideoLoss"
+          - "MotionDetect"
+          - "VideoTamper"
+          - "IntrusionDetect"
+          - "LineCrossDetect"
+
 # sip 配置
 # sip 配置
 sip:
 sip:
   enabled: true                            # 是否启用视频监控SIP,true为启用
   enabled: true                            # 是否启用视频监控SIP,true为启用