Zimo 1 dzień temu
rodzic
commit
961c0aa521

+ 25 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/AlarmEventDTO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class AlarmEventDTO {
+    private String eventId;
+    private String deviceId;
+    private String deviceName;
+    private String channelId;
+    private String channelName;
+    private String eventType;
+    private String eventDescription;
+    private LocalDateTime eventTime;
+    private String eventState;
+    private Double confidenceLevel;
+    private String regionCoordinates;
+    private String targetCoordinates;
+    private String imageUrl;
+    private String videoUrl;
+    private LocalDateTime receiveTime = LocalDateTime.now();
+    private Boolean processed = false;
+}

+ 120 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/AlarmEventService.java

@@ -0,0 +1,120 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlarmEventService {
+    
+    // 内存存储示例(实际应该使用数据库)
+    private Map<String, AlarmEventDTO> alarmEventStore = new ConcurrentHashMap<>();
+    
+    /**
+     * 保存告警事件
+     */
+    public void saveAlarmEvent(AlarmEventDTO dto) {
+        dto.setProcessed(false);
+        alarmEventStore.put(dto.getEventId(), dto);
+        log.info("告警事件已保存: eventId={}, type={}", 
+            dto.getEventId(), dto.getEventType());
+    }
+    
+    /**
+     * 异步处理告警事件
+     */
+    @Async
+    public void processAlarmEvent(AlarmEventDTO dto) {
+        try {
+            log.info("开始处理告警事件: eventId={}, device={}, channel={}, type={}", 
+                dto.getEventId(), dto.getDeviceName(), 
+                dto.getChannelName(), dto.getEventType());
+            
+            // 1. 更新处理状态
+            dto.setProcessed(true);
+            alarmEventStore.put(dto.getEventId(), dto);
+            
+            // 2. 根据事件类型进行不同处理
+            switch (dto.getEventType()) {
+                case "IntrusionDetection":
+                    handleIntrusionDetection(dto);
+                    break;
+                case "CrowdDetection":
+                    handleCrowdDetection(dto);
+                    break;
+                case "HelmetDetection":
+                    handleHelmetDetection(dto);
+                    break;
+                case "RegionIntrusion":
+                    handleRegionIntrusion(dto);
+                    break;
+                default:
+                    handleDefaultAlarm(dto);
+            }
+            
+            // 3. 发送通知(可集成企业微信、钉钉、短信等)
+            //todo 触发消息提醒
+//            sendNotification(dto);
+            
+            log.info("告警事件处理完成: eventId={}", dto.getEventId());
+            
+        } catch (Exception e) {
+            log.error("处理告警事件失败: eventId={}", dto.getEventId(), e);
+        }
+    }
+    
+    private void handleIntrusionDetection(AlarmEventDTO dto) {
+        log.warn("入侵检测告警: {} 在 {} 检测到入侵", 
+            dto.getChannelName(), dto.getEventTime());
+        // 具体业务逻辑
+    }
+    
+    private void handleCrowdDetection(AlarmEventDTO dto) {
+        log.warn("人员聚集告警: {} 检测到人员聚集", dto.getChannelName());
+        // 具体业务逻辑
+    }
+    
+    private void handleHelmetDetection(AlarmEventDTO dto) {
+        log.warn("安全帽检测告警: {} 检测到未戴安全帽", dto.getChannelName());
+        // 具体业务逻辑
+    }
+    
+    private void handleRegionIntrusion(AlarmEventDTO dto) {
+        log.warn("区域入侵告警: {} 检测到区域入侵", dto.getChannelName());
+        // 具体业务逻辑
+    }
+    
+    private void handleDefaultAlarm(AlarmEventDTO dto) {
+        log.warn("通用告警: {} - {}", dto.getEventType(), dto.getEventDescription());
+    }
+    
+    private void sendNotification(AlarmEventDTO dto) {
+        // TODO: 集成通知服务
+        log.info("发送告警通知: eventId={}, type={}", 
+            dto.getEventId(), dto.getEventType());
+    }
+    
+    /**
+     * 查询告警事件
+     */
+    public AlarmEventDTO getAlarmEvent(String eventId) {
+        return alarmEventStore.get(eventId);
+    }
+    
+    /**
+     * 统计今日告警
+     */
+    public long countTodayAlarms() {
+        LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
+        return alarmEventStore.values().stream()
+            .filter(event -> event.getEventTime().isAfter(today))
+            .count();
+    }
+}

+ 17 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/DeviceInfo.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class DeviceInfo {
+    private String deviceId;
+    private String deviceName;
+    private String deviceType;
+    private String serialNumber;
+    private String firmwareVersion;
+    private String ipAddress;
+    private String status;
+    private LocalDateTime lastHeartbeat;
+}

+ 124 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/HikHttpClient.java

@@ -0,0 +1,124 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+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.config.RequestConfig;
+import org.apache.http.client.methods.*;
+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;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+public class HikHttpClient {
+    
+    private Map<String, CloseableHttpClient> clientCache = new ConcurrentHashMap<>();
+    
+    public CloseableHttpClient getHttpClient(String username, String password, int timeout) {
+        String key = username + "@" + password;
+        return clientCache.computeIfAbsent(key, k -> createHttpClient(username, password, timeout));
+    }
+    
+    private CloseableHttpClient createHttpClient(String username, String password, int timeout) {
+        CredentialsProvider provider = new BasicCredentialsProvider();
+        provider.setCredentials(
+            AuthScope.ANY,
+            new UsernamePasswordCredentials(username, password)
+        );
+        
+        RequestConfig requestConfig = RequestConfig.custom()
+            .setConnectTimeout(timeout)
+            .setSocketTimeout(timeout)
+            .setConnectionRequestTimeout(timeout)
+            .build();
+        
+        return HttpClients.custom()
+            .setDefaultCredentialsProvider(provider)
+            .setDefaultRequestConfig(requestConfig)
+            .build();
+    }
+    
+    public String executeRequest(HttpRequestBase request, String username, 
+                               String password, int timeout) throws IOException {
+        CloseableHttpClient client = getHttpClient(username, password, timeout);
+        try (CloseableHttpResponse response = client.execute(request)) {
+            return handleResponse(response);
+        }
+    }
+    
+    public String doGet(String url, String username, String password, 
+                       int timeout, Map<String, String> headers) throws IOException {
+        HttpGet httpGet = new HttpGet(url);
+        setHeaders(httpGet, headers);
+        return executeRequest(httpGet, username, password, timeout);
+    }
+    
+    public String doPost(String url, String body, String username, String password,
+                        int timeout, Map<String, String> headers) throws IOException {
+        HttpPost httpPost = new HttpPost(url);
+        setHeaders(httpPost, headers);
+        if (body != null) {
+            httpPost.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
+        }
+        return executeRequest(httpPost, username, password, timeout);
+    }
+    
+    public String doPut(String url, String body, String username, String password,
+                       int timeout, Map<String, String> headers) throws IOException {
+        HttpPut httpPut = new HttpPut(url);
+        setHeaders(httpPut, headers);
+        if (body != null) {
+            httpPut.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
+        }
+        return executeRequest(httpPut, username, password, timeout);
+    }
+    
+    private void setHeaders(HttpRequestBase request, Map<String, String> headers) {
+        if (headers != null) {
+            headers.forEach(request::setHeader);
+        }
+        // 默认JSON头
+        if (request.getFirstHeader("Content-Type") == null) {
+            request.setHeader("Content-Type", "application/json");
+        }
+    }
+    
+    private String handleResponse(HttpResponse response) throws IOException {
+        int statusCode = response.getStatusLine().getStatusCode();
+        HttpEntity entity = response.getEntity();
+        String responseString = EntityUtils.toString(entity, StandardCharsets.UTF_8);
+        
+        log.debug("HTTP Status: {}, Response: {}", statusCode, responseString);
+        
+        if (statusCode >= 200 && statusCode < 300) {
+            return responseString;
+        } else {
+            log.error("HTTP Error: {} - {}", statusCode, responseString);
+            throw new IOException("HTTP Error: " + statusCode);
+        }
+    }
+    
+    @PreDestroy
+    public void destroy() {
+        clientCache.values().forEach(client -> {
+            try {
+                client.close();
+            } catch (IOException e) {
+                log.error("Error closing HTTP client", e);
+            }
+        });
+    }
+}

+ 201 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/HikIsapiController.java

@@ -0,0 +1,201 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.security.PermitAll;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestController
+@RequestMapping("/hikvision")
+@RequiredArgsConstructor
+public class HikIsapiController {
+    
+    private final HikIsapiService hikIsapiService;
+    private final AlarmEventService alarmEventService;
+    
+    /**
+     * 接收超脑告警回调(JSON格式)
+     */
+    @PostMapping("/alarm/callback")
+    @PermitAll
+    public ResponseEntity<Map<String, Object>> handleAlarmCallback(
+            @RequestBody String jsonPayload) {
+        
+        log.info("接收到告警回调请求: {}", jsonPayload);
+        
+        try {
+            // 处理告警
+            hikIsapiService.handleAlarmCallback(jsonPayload);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", 0,
+                "message", "success",
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (Exception e) {
+            log.error("处理告警回调失败", e);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", -1,
+                "message", e.getMessage(),
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.badRequest().body(response);
+        }
+    }
+    
+    /**
+     * 订阅事件通知
+     */
+    @PostMapping("/subscribe/{deviceId}")
+    //@ApiOperation("订阅设备事件通知")
+    public ResponseEntity<Map<String, Object>> subscribeEvents(
+            @PathVariable String deviceId) {
+        
+        try {
+            String result = hikIsapiService.subscribeEvents(deviceId);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", 0,
+                "message", "订阅成功",
+                "data", result,
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (IOException e) {
+            log.error("订阅失败", e);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", -1,
+                "message", "订阅失败: " + e.getMessage(),
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.badRequest().body(response);
+        }
+    }
+    
+    /**
+     * 取消订阅
+     */
+    @PostMapping("/unsubscribe/{deviceId}")
+    //@ApiOperation("取消订阅设备事件")
+    public ResponseEntity<Map<String, Object>> unsubscribeEvents(
+            @PathVariable String deviceId) {
+        
+        try {
+            String result = hikIsapiService.unsubscribeEvents(deviceId);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", 0,
+                "message", "取消订阅成功",
+                "data", result,
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (IOException e) {
+            log.error("取消订阅失败", e);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", -1,
+                "message", "取消订阅失败: " + e.getMessage(),
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.badRequest().body(response);
+        }
+    }
+    
+    /**
+     * 获取设备信息
+     */
+    @GetMapping("/device/{deviceId}/info")
+    //@ApiOperation("获取设备信息")
+    public ResponseEntity<Map<String, Object>> getDeviceInfo(
+            @PathVariable String deviceId) {
+        
+        try {
+            String result = hikIsapiService.getDeviceInfo(deviceId);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", 0,
+                "message", "获取成功",
+                "data", result,
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.ok(response);
+            
+        } catch (IOException e) {
+            log.error("获取设备信息失败", e);
+            
+            Map<String, Object> response = ImmutableMap.of(
+                "code", -1,
+                "message", "获取失败: " + e.getMessage(),
+                "timestamp", System.currentTimeMillis()
+            );
+            
+            return ResponseEntity.badRequest().body(response);
+        }
+    }
+    
+    /**
+     * 查询告警事件
+     */
+    @GetMapping("/alarms")
+    //@ApiOperation("查询告警事件")
+    public ResponseEntity<Map<String, Object>> getAlarms(
+            @RequestParam(required = false) String deviceId,
+            @RequestParam(required = false) String eventType,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime) {
+        
+        // TODO: 从数据库查询
+        Map<String, Object> response = ImmutableMap.of(
+            "code", 0,
+            "message", "查询成功",
+            "data", ImmutableList.of(),
+            "timestamp", System.currentTimeMillis()
+        );
+        
+        return ResponseEntity.ok(response);
+    }
+    
+    /**
+     * 接收图片回调
+     */
+    @PostMapping("/image/callback")
+    //@ApiOperation("接收图片回调")
+    public ResponseEntity<Map<String, Object>> handleImageCallback(
+            @RequestBody Map<String, Object> imageData) {
+        
+        log.info("接收到图片回调: {}", imageData);
+        
+        // TODO: 保存图片到本地或云存储
+        
+        Map<String, Object> response = ImmutableMap.of(
+            "code", 0,
+            "message", "success",
+            "timestamp", System.currentTimeMillis()
+        );
+        
+        return ResponseEntity.ok(response);
+    }
+}

+ 194 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/HikIsapiService.java

@@ -0,0 +1,194 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class HikIsapiService {
+    
+    private final HikHttpClient httpClient;
+    private final HikvisionProperties properties;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final AlarmEventService alarmEventService;
+    
+    /**
+     * 订阅事件通知
+     */
+    public String subscribeEvents(String deviceId) throws IOException {
+        HikvisionProperties.IsapiConfig.DeviceConfig device = getDeviceConfig(deviceId);
+        String url = device.getBaseUrl() + device.getPaths().getEventNotification();
+        
+        // 构建JSON订阅请求
+        Map<String, Object> subscribeRequest = buildSubscribeRequest(deviceId);
+        String jsonRequest = objectMapper.writeValueAsString(subscribeRequest);
+        
+        log.info("订阅事件通知: deviceId={}, url={}", deviceId, url);
+        
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Content-Type", "application/json");
+        
+        String response = httpClient.doPost(url, jsonRequest, 
+            device.getUsername(), device.getPassword(), 
+            device.getTimeout(), headers);
+        
+        log.info("订阅成功: deviceId={}, response={}", deviceId, response);
+        return response;
+    }
+    
+    /**
+     * 构建订阅JSON
+     */
+    private Map<String, Object> buildSubscribeRequest(String deviceId) {
+        Map<String, Object> request = new HashMap<>();
+        request.put("method", "Event.notification");
+        request.put("id", UUID.randomUUID().toString());
+        
+        Map<String, Object> params = new HashMap<>();
+        params.put("protocol", "json");
+        
+        // 回调地址配置
+        HikvisionProperties.IsapiConfig.CallbackConfig callback = 
+            properties.getIsapi().getCallback();
+        
+        if (callback != null && callback.getEnabled()) {
+            Map<String, Object> httpHost = new HashMap<>();
+            httpHost.put("url", callback.getLocalUrl());
+            httpHost.put("method", "POST");
+            httpHost.put("timeout", 10);
+            httpHost.put("contentType", "application/json");
+            
+            Map<String, Object> notification = new HashMap<>();
+            notification.put("httpHost", httpHost);
+            notification.put("heartbeatInterval", 
+                properties.getIsapi().getDevices().stream()
+                    .filter(d -> d.getId().equals(deviceId))
+                    .findFirst()
+                    .map(d -> d.getSubscription().getHeartbeatInterval())
+                    .orElse(30));
+            
+            params.put("notification", notification);
+        }
+        
+        request.put("params", params);
+        return request;
+    }
+    
+    /**
+     * 取消订阅
+     */
+    public String unsubscribeEvents(String deviceId) throws IOException {
+        HikvisionProperties.IsapiConfig.DeviceConfig device = getDeviceConfig(deviceId);
+        String url = device.getBaseUrl() + device.getPaths().getEventNotification();
+        
+        Map<String, Object> unsubscribeRequest = new HashMap<>();
+        unsubscribeRequest.put("method", "Event.unsubscribe");
+        unsubscribeRequest.put("id", UUID.randomUUID().toString());
+        
+        String jsonRequest = objectMapper.writeValueAsString(unsubscribeRequest);
+        
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Content-Type", "application/json");
+        
+        return httpClient.doPost(url, jsonRequest, 
+            device.getUsername(), device.getPassword(), 
+            device.getTimeout(), headers);
+    }
+    
+    /**
+     * 获取设备信息
+     */
+    public String getDeviceInfo(String deviceId) throws IOException {
+        HikvisionProperties.IsapiConfig.DeviceConfig device = getDeviceConfig(deviceId);
+        String url = device.getBaseUrl() + device.getPaths().getDeviceInfo();
+        
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Content-Type", "application/json");
+        
+        return httpClient.doGet(url, 
+            device.getUsername(), device.getPassword(), 
+            device.getTimeout(), headers);
+    }
+    
+    /**
+     * 处理告警回调
+     */
+    public void handleAlarmCallback(String jsonPayload) {
+        try {
+            log.info("收到告警回调: {}", jsonPayload);
+            
+            // 解析JSON
+            IsapiAlarmEvent alarmEvent = objectMapper.readValue(
+                jsonPayload, IsapiAlarmEvent.class);
+            
+            // 转换为DTO
+//            AlarmEventDTO dto = convertToDTO(alarmEvent);
+            
+            // 保存到数据库
+//            alarmEventService.saveAlarmEvent(dto);
+            
+            // 触发告警处理流程
+//            alarmEventService.processAlarmEvent(dto);
+            
+
+        } catch (JsonProcessingException e) {
+            log.error("JSON解析失败: {}", jsonPayload, e);
+            throw new RuntimeException("JSON解析失败", e);
+        }
+    }
+    
+    /**
+     * 转换告警事件
+     */
+//    private AlarmEventDTO convertToDTO(IsapiAlarmEvent alarmEvent) {
+//        AlarmEventDTO dto = new AlarmEventDTO();
+//
+//        if (alarmEvent.getParams() != null) {
+//            dto.setEventId(alarmEvent.getParams().getEventID());
+//            dto.setDeviceId(alarmEvent.getParams().getDeviceID());
+//            dto.setDeviceName(alarmEvent.getParams().getDeviceName());
+//            dto.setChannelId(alarmEvent.getParams().getChannelID());
+//            dto.setChannelName(alarmEvent.getParams().getChannelName());
+//            dto.setEventType(alarmEvent.getParams().getEventType());
+//            dto.setEventDescription(alarmEvent.getParams().getEventDescription());
+//            dto.setEventTime(alarmEvent.getParams().getDateTime());
+//            dto.setEventState(alarmEvent.getParams().getEventState());
+//        }
+//
+//        if (alarmEvent.getParams() != null &&
+//            alarmEvent.getParams().getData() != null &&
+//            alarmEvent.getParams().getData().getAlarmEvent() != null &&
+//            alarmEvent.getParams().getData().getAlarmEvent().getEventList() != null &&
+//            alarmEvent.getParams().getData().getAlarmEvent().getEventList().getEvent() != null &&
+//            !alarmEvent.getParams().getData().getAlarmEvent().getEventList().getEvent().isEmpty()) {
+//
+//            IsapiAlarmEvent.EventData.AlarmEvent.EventList.Event event =
+//                alarmEvent.getParams().getData().getAlarmEvent().getEventList().getEvent().get(0);
+//
+//            dto.setConfidenceLevel(event.getConfidenceLevel());
+//            dto.setRegionCoordinates(event.getRegionCoordinates());
+//            dto.setTargetCoordinates(event.getTargetCoordinates());
+//            dto.setImageUrl(event.getImageURL());
+//            dto.setVideoUrl(event.getVideoURL());
+//        }
+//
+//        return dto;
+//    }
+    
+    private HikvisionProperties.IsapiConfig.DeviceConfig getDeviceConfig(String deviceId) {
+        return properties.getIsapi().getDevices().stream()
+            .filter(d -> d.getId().equals(deviceId))
+            .findFirst()
+            .orElseThrow(() -> new IllegalArgumentException("设备不存在: " + deviceId));
+    }
+}

+ 83 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/HikvisionProperties.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "hikvision")
+public class HikvisionProperties {
+    
+    private IsapiConfig isapi;
+    private AlarmConfig alarm;
+    
+    @Data
+    public static class IsapiConfig {
+        private List<DeviceConfig> devices;
+        private CallbackConfig callback;
+        private StorageConfig storage;
+        
+        @Data
+        public static class DeviceConfig {
+            private String id;
+            private String name;
+            private String ip;
+            private Integer port;
+            private String username;
+            private String password;
+            private String protocol;
+            private Integer timeout;
+            private PathConfig paths;
+            private SubscriptionConfig subscription;
+            
+            public String getBaseUrl() {
+                return String.format("%s://%s:%d", protocol, ip, port);
+            }
+        }
+        
+        @Data
+        public static class PathConfig {
+            private String eventNotification;
+            private String intelligenceAnalysis;
+            private String systemStatus;
+            private String deviceInfo;
+        }
+        
+        @Data
+        public static class SubscriptionConfig {
+            private Integer heartbeatInterval;
+            private String eventTypes;
+        }
+        
+        @Data
+        public static class CallbackConfig {
+            private Boolean enabled;
+            private String localUrl;
+            private String imageUrl;
+            private Integer retryTimes;
+            private Integer retryInterval;
+        }
+        
+        @Data
+        public static class StorageConfig {
+            private Boolean saveAlarm;
+            private Boolean saveImage;
+            private String imagePath;
+        }
+    }
+    
+    @Data
+    public static class AlarmConfig {
+        private List<EventType> eventTypes;
+        
+        @Data
+        public static class EventType {
+            private String name;
+            private String code;
+            private String level;
+        }
+    }
+}

+ 136 - 0
yudao-module-pms/yudao-module-pms-biz/src/main/java/cn/iocoder/yudao/module/pms/hik/IsapiAlarmEvent.java

@@ -0,0 +1,136 @@
+package cn.iocoder.yudao.module.pms.hik;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class IsapiAlarmEvent {
+    private String ipAddress;
+    private String portNo;
+    private String protocol;
+    private String dateTime;
+    private Integer activePostCount;
+    private String eventState;
+    private String presetNo;
+    private String eventType;
+    private String eventDescription;
+    @JsonProperty("MPName")
+    private String mpName;
+    private String url;
+    private String taskId;
+    @JsonProperty("channelID")
+    private String channelID;
+    private String protocolType;
+    private String childDevID;
+
+
+//    @JsonProperty("method")
+//    private String method;  // 固定为"Event.notification"
+//
+//    @JsonProperty("params")
+//    private EventParams params;
+//
+//    @Data
+//    public static class EventParams {
+//        @JsonProperty("activePostCount")
+//        private Integer activePostCount;
+//
+//        @JsonProperty("eventType")
+//        private String eventType;
+//
+//        @JsonProperty("eventDescription")
+//        private String eventDescription;
+//
+//        @JsonProperty("eventState")
+//        private String eventState;  // "active", "inactive"
+//
+//        @JsonProperty("dateTime")
+//        @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
+//        private LocalDateTime dateTime;
+//
+//        @JsonProperty("deviceID")
+//        private String deviceID;
+//
+//        @JsonProperty("deviceName")
+//        private String deviceName;
+//
+//        @JsonProperty("channelID")
+//        private String channelID;
+//
+//        @JsonProperty("channelName")
+//        private String channelName;
+//
+//        @JsonProperty("eventID")
+//        private String eventID;
+//
+//        @JsonProperty("seq")
+//        private Integer seq;
+//
+//        @JsonProperty("data")
+//        private EventData data;
+//    }
+//
+//    @Data
+//    public static class EventData {
+//
+//        @JsonProperty("AlarmEvent")
+//        private AlarmEvent alarmEvent;
+//
+//        @Data
+//        public static class AlarmEvent {
+//
+//            @JsonProperty("EventList")
+//            private EventList eventList;
+//
+//            @Data
+//            public static class EventList {
+//
+//                @JsonProperty("Event")
+//                private List<Event> event;
+//
+//                @Data
+//                public static class Event {
+//
+//                    @JsonProperty("eventType")
+//                    private String eventType;
+//
+//                    @JsonProperty("eventState")
+//                    private String eventState;
+//
+//                    @JsonProperty("eventDescription")
+//                    private String eventDescription;
+//
+//                    @JsonProperty("startDateTime")
+//                    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
+//                    private LocalDateTime startDateTime;
+//
+//                    @JsonProperty("endDateTime")
+//                    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
+//                    private LocalDateTime endDateTime;
+//
+//                    @JsonProperty("smartType")
+//                    private String smartType;
+//
+//                    @JsonProperty("regionCoordinates")
+//                    private String regionCoordinates;
+//
+//                    @JsonProperty("targetCoordinates")
+//                    private String targetCoordinates;
+//
+//                    @JsonProperty("confidenceLevel")
+//                    private Double confidenceLevel;
+//
+//                    @JsonProperty("imageURL")
+//                    private String imageURL;
+//
+//                    @JsonProperty("videoURL")
+//                    private String videoURL;
+//                }
+//            }
+//        }
+//    }
+}

+ 59 - 2
yudao-server/src/main/resources/application-dev.yaml

@@ -303,11 +303,68 @@ file:
   upload-path: D:\wenjian
 
 sip:
-  enabled: true                            # 是否启用视频监控SIP,true为启用
+  enabled: false                            # 是否启用视频监控SIP,true为启用
   ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP
   ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP
   ip: 192.168.188.149
   port: 5061                                # SIP端口(保持默认)
   domain: 3402000000                        # 由省级、市级、区级、基层编号组成
   id: 34020000002000000001                  # 同上,另外增加编号,(可保持默认)
-  password: 12345678
+  password: 12345678
+
+  hikvision:
+    # ISAPI协议配置
+    isapi:
+      # 超脑设备配置
+      devices:
+        - id: "nvr-001"
+          name: "超脑巡检设备1"
+          ip: "172.26.0.52"
+          port: 80
+          username: "admin"
+          password: "rqny@szh2026"
+          protocol: "http"
+          timeout: 10000
+          # ISAPI接口路径
+          paths:
+            event-notification: "/ISAPI/Event/notification/"
+            intelligence-analysis: "/ISAPI/Intelligent/Analysis/"
+            system-status: "/ISAPI/System/status"
+            device-info: "/ISAPI/System/deviceInfo"
+          # 订阅配置
+          subscription:
+            heartbeat-interval: 30
+            event-types: "All"
+      # 回调配置
+      callback:
+        enabled: true
+        # 本地接收告警的接口地址(超脑会调用这个地址)
+        local-url: "http://172.26.0.3:8080/admin-api/hikvision/alarm/callback"
+        # 用于接收图片的接口
+        image-url: "http://172.26.0.3:8080/admin-api/hikvision/image/callback"
+        retry-times: 3
+        retry-interval: 1000
+      # 存储配置
+      storage:
+        save-alarm: true
+        save-image: false
+        image-path: "./alarm-images/"
+
+    # 告警事件处理配置
+    alarm:
+      event-types:
+        - name: "入侵检测"
+          code: "IntrusionDetection"
+          level: "HIGH"
+        - name: "人员聚集"
+          code: "CrowdDetection"
+          level: "MEDIUM"
+        - name: "安全帽检测"
+          code: "HelmetDetection"
+          level: "MEDIUM"
+        - name: "区域入侵"
+          code: "RegionIntrusion"
+          level: "HIGH"
+        - name: "设备离线"
+          code: "DeviceOffline"
+          level: "CRITICAL"

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

@@ -246,6 +246,7 @@ yudao:
     permit-all_urls:
       - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
       - /admin-api/rq/iot-app/**
+      - /admin-api/hikvision/**
   websocket:
     enable: true # websocket的开关
     path: /infra/ws # 路径