|
@@ -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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|