Browse Source

Merge remote-tracking branch 'origin/master'

wuxiang 3 ngày trước cách đây
mục cha
commit
6480506134
18 tập tin đã thay đổi với 548 bổ sung86 xóa
  1. 0 1
      supervision-admin/src/main/java/com/supervision/web/ConnectSDK/util/DeviceSimulator.java
  2. 37 43
      supervision-admin/src/main/java/com/supervision/web/WebSocket/EventWebSocket.java
  3. 1 1
      supervision-admin/src/main/java/com/supervision/web/WebSocket/SpringContextHolder.java
  4. 1 1
      supervision-admin/src/main/java/com/supervision/web/WebSocket/WebSocketConfig.java
  5. 67 0
      supervision-admin/src/main/java/com/supervision/web/controller/safety/SafetyController.java
  6. 44 40
      supervision-admin/src/main/java/com/supervision/web/peopleGateManage/controller/EventLogController.java
  7. 68 0
      supervision-admin/src/main/java/com/supervision/web/util/DoorGateEventSimulator.java
  8. 14 0
      supervision-system/src/main/java/com/supervision/safety/domain/CreateIssueReq.java
  9. 13 0
      supervision-system/src/main/java/com/supervision/safety/domain/DashboardVo.java
  10. 5 0
      supervision-system/src/main/java/com/supervision/safety/domain/IssueStatus.java
  11. 17 0
      supervision-system/src/main/java/com/supervision/safety/domain/PageResult.java
  12. 5 0
      supervision-system/src/main/java/com/supervision/safety/domain/RiskLevel.java
  13. 17 0
      supervision-system/src/main/java/com/supervision/safety/domain/SafetyIssue.java
  14. 10 0
      supervision-system/src/main/java/com/supervision/safety/domain/UpdateStatusReq.java
  15. 69 0
      supervision-system/src/main/java/com/supervision/safety/mapper/SafetyIssueMapper.java
  16. 49 0
      supervision-system/src/main/java/com/supervision/safety/mapper/SafetyIssueSqlProvider.java
  17. 29 0
      supervision-system/src/main/java/com/supervision/safety/service/SafetyService.java
  18. 102 0
      supervision-system/src/main/java/com/supervision/safety/service/impl/SafetyServiceImpl.java

+ 0 - 1
supervision-admin/src/main/java/com/supervision/web/ConnectSDK/util/DeviceSimulator.java

@@ -3,7 +3,6 @@ package com.supervision.web.ConnectSDK.util;
 import com.supervision.web.ConnectSDK.carCamera.CarCameraDeviceManager;
 import com.supervision.web.ConnectSDK.carDoor.CarDoorDeviceManager;
 import com.supervision.web.ConnectSDK.peopleDoor.PeopleDoorDeviceManager;
-import com.supervision.WebSocket.EventWebSocket;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 

+ 37 - 43
supervision-admin/src/main/java/com/supervision/WebSocket/EventWebSocket.java → supervision-admin/src/main/java/com/supervision/web/WebSocket/EventWebSocket.java

@@ -1,4 +1,4 @@
-package com.supervision.WebSocket;
+package com.supervision.web.WebSocket;
 
 
 import com.alibaba.fastjson.JSON;
@@ -11,33 +11,23 @@ import org.springframework.stereotype.Component;
 import javax.websocket.*;
 import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
-import java.io.*;
+import java.io.IOException;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
-/**
- * WebSocket 实时推送服务(兼容 JDK 1.8)
- * 每个用户登录后建立独立连接,通过 Token 鉴权
- */
 @Component
 @ServerEndpoint("/websocket/{token}")
 public class EventWebSocket {
 
     private static final Logger logger = LoggerFactory.getLogger(EventWebSocket.class);
 
-    /** 保存所有在线用户连接 */
     private static final Map<Long, Session> USER_SESSION_MAP = new ConcurrentHashMap<>();
 
     private Long userId;
 
-    /**
-     * 连接建立成功调用的方法
-     * */
     @OnOpen
     public void onOpen(Session session, @PathParam("token") String token) {
-        logger.info("🟢 WebSocket 请求到达,----------连接成功");
-        logger.info("🟢 WebSocket 请求到达,----------session: {}", session);
-        logger.info("🟢 WebSocket 请求到达,----------token: {}", token);
+        logger.info("🟢 WebSocket 请求到达,token={}", token);
         try {
             TokenService tokenService = SpringContextHolder.getBean(TokenService.class);
             LoginUser loginUser = tokenService.getLoginUserByToken(token);
@@ -45,14 +35,11 @@ public class EventWebSocket {
                 session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Invalid token"));
                 return;
             }
-
             this.userId = loginUser.getUserId();
             USER_SESSION_MAP.put(userId, session);
-            logger.info("✅ 用户 " + loginUser.getUsername() + " 已连接 WebSocket");
+            logger.info("✅ 用户 {} 已连接 WebSocket", loginUser.getUsername());
         } catch (Exception e) {
-            try {
-                session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Auth failed"));
-            } catch (IOException ignored) {}
+            try { session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Auth failed")); } catch (IOException ignored) {}
         }
     }
 
@@ -67,49 +54,56 @@ public class EventWebSocket {
     public void onClose() {
         if (userId != null) {
             USER_SESSION_MAP.remove(userId);
-            logger.info("❌ 用户 " + userId + " 已断开 WebSocket");
+            logger.info("❌ 用户 {} 已断开 WebSocket", userId);
         }
     }
 
     @OnError
     public void onError(Session session, Throwable error) {
-        System.err.println("WebSocket 错误:" + error.getMessage());
+        logger.error("WebSocket 错误:{}", error.getMessage());
     }
-
-    /** 广播消息给所有在线用户 */
-    public static void broadcast(String type, Object data) {
-        String msg = JSON.toJSONString(new WebSocketMessage(type, data));
-        for (Session session : USER_SESSION_MAP.values()) {
-            if (session.isOpen()) {
-                session.getAsyncRemote().sendText(msg);
-            }
+    // 给某个用户发送事件
+    // 门禁事件
+    // EventWebSocket.sendToUser(userId, "doorGate", Map.of("gateId", 1, "status", "open"));
+    // 车闸事件
+    // EventWebSocket.sendToUser(userId, "carGate", Map.of("carId", "A12345", "status", "pass"));
+    public static void sendToUser(Long userId, String type, Long deviceId, Object payload) {
+        Session session = USER_SESSION_MAP.get(userId);
+        if (session != null && session.isOpen()) {
+            WebSocketMessage msg = new WebSocketMessage(type, deviceId, payload);
+            session.getAsyncRemote().sendText(JSON.toJSONString(msg));
         }
     }
 
-    /** 向特定用户发送消息 */
-    public static void sendToUser(Long userId, Object data) {
-        Session session = USER_SESSION_MAP.get(userId);
-        if (session != null && session.isOpen()) {
-            session.getAsyncRemote().sendText(JSON.toJSONString(data));
+    // 广播给所有在线用户
+    public static void broadcast(String type, Long deviceId, Object payload) {
+        WebSocketMessage msg = new WebSocketMessage(type, deviceId, payload);
+        String json = JSON.toJSONString(msg);
+        for (Session session : USER_SESSION_MAP.values()) {
+            if (session.isOpen()) session.getAsyncRemote().sendText(json);
         }
     }
 
-    /** 简单封装的消息结构 */
+    // 消息结构
     private static class WebSocketMessage {
+
         private String type;
-        private Object data;
 
-        public WebSocketMessage(String type, Object data) {
+        private Long deviceId;
+
+        private Object payload;
+
+        public WebSocketMessage(String type, Long deviceId, Object payload) {
             this.type = type;
-            this.data = data;
+            this.deviceId = deviceId;
+            this.payload = payload;
         }
 
-        public String getType() {
-            return type;
-        }
+        public String getType() { return type; }
+
+        public Long getDeviceId() { return deviceId; }
+
+        public Object getPayload() { return payload; }
 
-        public Object getData() {
-            return data;
-        }
     }
 }

+ 1 - 1
supervision-admin/src/main/java/com/supervision/WebSocket/SpringContextHolder.java → supervision-admin/src/main/java/com/supervision/web/WebSocket/SpringContextHolder.java

@@ -1,4 +1,4 @@
-package com.supervision.WebSocket;
+package com.supervision.web.WebSocket;
 
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;

+ 1 - 1
supervision-admin/src/main/java/com/supervision/WebSocket/WebSocketConfig.java → supervision-admin/src/main/java/com/supervision/web/WebSocket/WebSocketConfig.java

@@ -1,4 +1,4 @@
-package com.supervision.WebSocket;
+package com.supervision.web.WebSocket;
 
 
 import org.slf4j.Logger;

+ 67 - 0
supervision-admin/src/main/java/com/supervision/web/controller/safety/SafetyController.java

@@ -0,0 +1,67 @@
+package com.supervision.web.controller.safety;
+
+import com.supervision.safety.domain.*;
+import com.supervision.safety.service.SafetyService;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@RestController
+@RequestMapping("/api/safety")
+public class SafetyController {
+
+    @Resource
+    private SafetyService safetyService;
+
+    // 台账列表(分页 + 条件)
+    @GetMapping("/issues")
+    public PageResult<SafetyIssue> issues(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) RiskLevel level,
+            @RequestParam(required = false) IssueStatus status,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date from,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date to,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize
+    ) {
+        return safetyService.search(keyword, level, status, from, to, pageNum, pageSize);
+    }
+
+    // 录入问题
+    @PostMapping("/issues")
+    public Map<String, Object> create(@Validated @RequestBody CreateIssueReq req) {
+        Long id = safetyService.createIssue(req);
+        Map<String, Object> ret = new HashMap<>();
+        ret.put("success", true);
+        ret.put("issueId", id);
+        return ret;
+    }
+
+    // 标记状态(已整改/未整改)
+    @PutMapping("/issues/{id}/status")
+    public Map<String, Object> updateStatus(@PathVariable Long id,
+                                            @RequestBody @Validated UpdateStatusReq req) {
+        safetyService.updateIssueStatus(id, req.getStatus());
+        return Collections.singletonMap("success", true);
+    }
+
+    // 删除(逻辑删除)
+    @DeleteMapping("/issues/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        safetyService.deleteIssue(id);
+        return Collections.singletonMap("success", true);
+    }
+
+    // 仪表盘(数量 + 趋势 + Top5)
+    @GetMapping("/dashboard")
+    public DashboardVo dashboard(@RequestParam(defaultValue = "7") int days) {
+        DashboardVo vo = new DashboardVo();
+        vo.setCounters(safetyService.counters());
+        vo.setWeeklyTrend(safetyService.weeklyTrend(days));
+        vo.setTop5High(safetyService.top5High());
+        return vo;
+    }
+}

+ 44 - 40
supervision-admin/src/main/java/com/supervision/web/peopleGateManage/controller/EventLogController.java

@@ -1,6 +1,7 @@
 package com.supervision.web.peopleGateManage.controller;
 
 import com.supervision.web.peopleGateManage.service.EventLogService;
+import com.supervision.web.util.DoorGateEventSimulator;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -20,44 +21,47 @@ public class EventLogController {
 //        Integer pageSize = Integer.parseInt(params.getOrDefault("pageSize","20").toString());
 //        return eventLogService.getEventLogs(params);
 //    }
-@GetMapping("/list")
-public Map<String, Object> list(
-        @RequestParam(required = false) String eventType,
-        @RequestParam(required = false) String personId,
-        @RequestParam(required = false) String jobNumber,
-        @RequestParam(required = false) String name,
-        @RequestParam(required = false) String remoteHost,
-        @RequestParam(required = false) String reader,
-        @RequestParam(required = false) String detectorId,
-        @RequestParam(required = false) String operation,
-        @RequestParam(required = false) String startTime,
-        @RequestParam(required = false) String endTime,
-        @RequestParam(defaultValue = "1") Integer pageNum,
-        @RequestParam(defaultValue = "24") Integer pageSize
-) {
-    // 强制为非 null 值
-    int pn = (pageNum == null || pageNum < 1) ? 1 : pageNum;
-    int ps = (pageSize == null || pageSize < 1) ? 24 : pageSize;
-    int offset = (pn - 1) * ps;
-
-    // 如果你的 Service 接收 Map,你也可以组装 Map,并且确保都是正确类型
-    Map<String, Object> params = new HashMap<>();
-    params.put("eventType", eventType);
-    params.put("personId", personId);
-    params.put("jobNumber", jobNumber);
-    params.put("name", name);
-    params.put("remoteHost", remoteHost);
-    params.put("reader", reader);
-    params.put("detectorId", detectorId);
-    params.put("operation", operation);
-    params.put("startTime", startTime);
-    params.put("endTime", endTime);
-    params.put("offset", offset);     // **Integer**
-    params.put("pageSize", ps);       // **Integer**
-
-    // 调用 Service(内部调用 Mapper.selectByCondition(params...))
-    Map<String, Object> result = eventLogService.getEventLogs(params);
-
-    return result;
-}
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String eventType,
+            @RequestParam(required = false) String personId,
+            @RequestParam(required = false) String jobNumber,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String remoteHost,
+            @RequestParam(required = false) String reader,
+            @RequestParam(required = false) String detectorId,
+            @RequestParam(required = false) String operation,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "24") Integer pageSize
+    ) {
+        // 强制为非 null 值
+        int pn = (pageNum == null || pageNum < 1) ? 1 : pageNum;
+        int ps = (pageSize == null || pageSize < 1) ? 24 : pageSize;
+        int offset = (pn - 1) * ps;
+
+        // 如果你的 Service 接收 Map,你也可以组装 Map,并且确保都是正确类型
+        Map<String, Object> params = new HashMap<>();
+        params.put("eventType", eventType);
+        params.put("personId", personId);
+        params.put("jobNumber", jobNumber);
+        params.put("name", name);
+        params.put("remoteHost", remoteHost);
+        params.put("reader", reader);
+        params.put("detectorId", detectorId);
+        params.put("operation", operation);
+        params.put("startTime", startTime);
+        params.put("endTime", endTime);
+        params.put("offset", offset);     // **Integer**
+        params.put("pageSize", ps);       // **Integer**
+
+        // 调用 Service(内部调用 Mapper.selectByCondition(params...))
+        Map<String, Object> result = eventLogService.getEventLogs(params);
+
+        DoorGateEventSimulator.getStart();
+
+        return result;
+    }
+
 }

+ 68 - 0
supervision-admin/src/main/java/com/supervision/web/util/DoorGateEventSimulator.java

@@ -0,0 +1,68 @@
+package com.supervision.web.util;
+
+import com.supervision.web.WebSocket.EventWebSocket;
+import java.util.*;
+
+public class DoorGateEventSimulator {
+
+    private static Random random = new Random();
+
+    // 生成 NET_DVR_TIME
+    private static Map<String, Integer> generateTime() {
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.SECOND, -random.nextInt(3600)); // 随机1小时内
+        Map<String, Integer> time = new HashMap<>();
+        time.put("dwYear", cal.get(Calendar.YEAR));
+        time.put("dwMonth", cal.get(Calendar.MONTH) + 1);
+        time.put("dwDay", cal.get(Calendar.DAY_OF_MONTH));
+        time.put("dwHour", cal.get(Calendar.HOUR_OF_DAY));
+        time.put("dwMinute", cal.get(Calendar.MINUTE));
+        time.put("dwSecond", cal.get(Calendar.SECOND));
+        return time;
+    }
+
+    // 生成 NET_DVR_IPADDR
+    private static Map<String, String> generateIp() {
+        Map<String, String> ip = new HashMap<>();
+        ip.put("sIpV4", "192.168.1." + (random.nextInt(200) + 1));
+        ip.put("sIpV6", ""); // 暂时留空
+        return ip;
+    }
+
+    public static void getStart() {
+        Random random = new Random();
+        long[] deviceIds = {1, 2, 3}; // 三个设备 ID
+        for (int i = 0; i < 30; i++) {
+            Map<String, Object> doorGateEvent = new HashMap<>();
+            doorGateEvent.put("dwSize", 128);
+            doorGateEvent.put("dwMajor", random.nextInt(5));
+            doorGateEvent.put("dwMinor", random.nextInt(10));
+            doorGateEvent.put("struTime", generateTime());
+            doorGateEvent.put("sNetUser", "user" + (random.nextInt(10) + 1));
+            doorGateEvent.put("struRemoteHostAddr", generateIp());
+
+            Map<String, Object> acsEventInfo = new HashMap<>();
+            acsEventInfo.put("eventType", random.nextBoolean() ? "card" : "face");
+            acsEventInfo.put("gateId", random.nextInt(5) + 1);
+            acsEventInfo.put("status", random.nextBoolean() ? "open" : "close");
+            doorGateEvent.put("struAcsEventInfo", acsEventInfo);
+
+            doorGateEvent.put("dwPicDataLen", random.nextBoolean() ? 1024 : 0);
+            doorGateEvent.put("pPicData", random.nextBoolean() ? "http://example.com/pic.jpg" : null);
+            doorGateEvent.put("wInductiveEventType", random.nextInt(3));
+            doorGateEvent.put("byPicTransType", random.nextInt(2));
+            doorGateEvent.put("dwIOTChannelNo", random.nextInt(5));
+
+            // ✅ 随机设备ID
+            long deviceId = deviceIds[random.nextInt(deviceIds.length)];
+
+            EventWebSocket.broadcast("doorGate", deviceId, doorGateEvent);
+
+            try {
+                Thread.sleep(500); // 模拟事件间隔
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 14 - 0
supervision-system/src/main/java/com/supervision/safety/domain/CreateIssueReq.java

@@ -0,0 +1,14 @@
+package com.supervision.safety.domain;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.Date;
+
+@Data
+public class CreateIssueReq {
+    @NotBlank private String description;
+    @NotNull  private RiskLevel riskLevel;
+    @NotNull  private Date foundAt;
+}

+ 13 - 0
supervision-system/src/main/java/com/supervision/safety/domain/DashboardVo.java

@@ -0,0 +1,13 @@
+package com.supervision.safety.domain;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class DashboardVo {
+    private Map<String, Long> counters;
+    private List<Map<String, Object>> weeklyTrend;
+    private List<SafetyIssue> top5High;
+}

+ 5 - 0
supervision-system/src/main/java/com/supervision/safety/domain/IssueStatus.java

@@ -0,0 +1,5 @@
+package com.supervision.safety.domain;
+
+public enum IssueStatus {
+    PENDING, RECTIFIED
+}

+ 17 - 0
supervision-system/src/main/java/com/supervision/safety/domain/PageResult.java

@@ -0,0 +1,17 @@
+package com.supervision.safety.domain;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class PageResult<T> {
+    private long total;
+    private int pageNum;
+    private int pageSize;
+    private List<T> records;
+
+    public PageResult(long total, int pageNum, int pageSize, List<T> records) {
+        this.total = total; this.pageNum = pageNum; this.pageSize = pageSize; this.records = records;
+    }
+}

+ 5 - 0
supervision-system/src/main/java/com/supervision/safety/domain/RiskLevel.java

@@ -0,0 +1,5 @@
+package com.supervision.safety.domain;
+
+public enum RiskLevel {
+    HIGH, MEDIUM, LOW
+}

+ 17 - 0
supervision-system/src/main/java/com/supervision/safety/domain/SafetyIssue.java

@@ -0,0 +1,17 @@
+package com.supervision.safety.domain;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class SafetyIssue {
+    private Long id;
+    private String description;
+    private RiskLevel riskLevel;
+    private IssueStatus status;
+    private Date foundAt;
+    private Date rectifiedAt;
+    private Boolean deleted;
+    private Date createTime;
+    private Date updateTime;
+}

+ 10 - 0
supervision-system/src/main/java/com/supervision/safety/domain/UpdateStatusReq.java

@@ -0,0 +1,10 @@
+package com.supervision.safety.domain;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class UpdateStatusReq {
+    @NotNull private IssueStatus status;
+}

+ 69 - 0
supervision-system/src/main/java/com/supervision/safety/mapper/SafetyIssueMapper.java

@@ -0,0 +1,69 @@
+package com.supervision.safety.mapper;
+
+import com.supervision.safety.domain.*;
+import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.type.EnumTypeHandler;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface SafetyIssueMapper {
+
+    // === 基础 ===
+    @Select("SELECT id, description, risk_level, status, found_at, rectified_at, deleted, create_time, update_time " +
+            "FROM safety_issue WHERE id=#{id} AND deleted=0")
+    @Results(id="IssueMap", value = {
+            @Result(column="risk_level", property="riskLevel", javaType=RiskLevel.class,
+                    typeHandler=EnumTypeHandler.class),
+            @Result(column="status", property="status", javaType=IssueStatus.class,
+                    typeHandler=EnumTypeHandler.class)
+    })
+    SafetyIssue findById(@Param("id") Long id);
+
+    @Insert("INSERT INTO safety_issue(description, risk_level, status, found_at, rectified_at, deleted, create_time, update_time) " +
+            "VALUES(#{description}, #{riskLevel,typeHandler=org.apache.ibatis.type.EnumTypeHandler}, " +
+            "#{status,typeHandler=org.apache.ibatis.type.EnumTypeHandler}, #{foundAt}, #{rectifiedAt}, 0, NOW(), NOW())")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int insert(SafetyIssue issue);
+
+    @Update("UPDATE safety_issue SET status=#{status,typeHandler=org.apache.ibatis.type.EnumTypeHandler}, " +
+            "rectified_at=#{rectifiedAt}, update_time=NOW() WHERE id=#{id} AND deleted=0")
+    int updateStatus(@Param("id") Long id,
+                     @Param("status") IssueStatus status,
+                     @Param("rectifiedAt") Date rectifiedAt);
+
+    @Update("UPDATE safety_issue SET deleted=1, update_time=NOW() WHERE id=#{id} AND deleted=0")
+    int logicalDelete(@Param("id") Long id);
+
+    // === 查询/分页 ===
+    @SelectProvider(type=SafetyIssueSqlProvider.class, method="searchSql")
+    @ResultMap("IssueMap")
+    List<SafetyIssue> search(@Param("keyword") String keyword,
+                             @Param("level") RiskLevel level,
+                             @Param("status") IssueStatus status,
+                             @Param("from") Date from,
+                             @Param("to") Date to,
+                             @Param("limit") int limit,
+                             @Param("offset") int offset);
+
+    @SelectProvider(type=SafetyIssueSqlProvider.class, method="countSql")
+    long count(@Param("keyword") String keyword,
+               @Param("level") RiskLevel level,
+               @Param("status") IssueStatus status,
+               @Param("from") Date from,
+               @Param("to") Date to);
+
+    // === 统计 ===
+    @SelectProvider(type=SafetyIssueSqlProvider.class, method="countPendingByLevelSql")
+    List<Map<String, Object>> countPendingByLevel();
+
+    @SelectProvider(type=SafetyIssueSqlProvider.class, method="trendByDaySql")
+    List<Map<String, Object>> trendByDay(@Param("from") Date from,
+                                         @Param("to") Date to);
+
+    @SelectProvider(type=SafetyIssueSqlProvider.class, method="top5HighSql")
+    @ResultMap("IssueMap")
+    List<SafetyIssue> top5High();
+}

+ 49 - 0
supervision-system/src/main/java/com/supervision/safety/mapper/SafetyIssueSqlProvider.java

@@ -0,0 +1,49 @@
+package com.supervision.safety.mapper;
+
+import java.util.Map;
+
+public class SafetyIssueSqlProvider {
+
+    public String searchSql(Map<String, Object> p) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("SELECT id, description, risk_level, status, found_at, rectified_at, ")
+                .append("deleted, create_time, update_time FROM safety_issue WHERE deleted=0 ");
+        if (p.get("keyword") != null) sb.append("AND description LIKE CONCAT('%', #{keyword}, '%') ");
+        if (p.get("level")   != null) sb.append("AND risk_level = #{level} ");
+        if (p.get("status")  != null) sb.append("AND status = #{status} ");
+        if (p.get("from")    != null) sb.append("AND found_at >= #{from} ");
+        if (p.get("to")      != null) sb.append("AND found_at <  #{to} ");
+        sb.append("ORDER BY found_at DESC ");
+        sb.append("LIMIT #{limit} OFFSET #{offset}");
+        return sb.toString();
+    }
+
+    public String countSql(Map<String, Object> p) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("SELECT COUNT(*) FROM safety_issue WHERE deleted=0 ");
+        if (p.get("keyword") != null) sb.append("AND description LIKE CONCAT('%', #{keyword}, '%') ");
+        if (p.get("level")   != null) sb.append("AND risk_level = #{level} ");
+        if (p.get("status")  != null) sb.append("AND status = #{status} ");
+        if (p.get("from")    != null) sb.append("AND found_at >= #{from} ");
+        if (p.get("to")      != null) sb.append("AND found_at <  #{to} ");
+        return sb.toString();
+    }
+
+    public String countPendingByLevelSql() {
+        return "SELECT risk_level AS lvl, COUNT(*) AS cnt " +
+                "FROM safety_issue WHERE deleted=0 AND status='PENDING' GROUP BY risk_level";
+    }
+
+    public String trendByDaySql(Map<String, Object> p) {
+        return "SELECT DATE_FORMAT(found_at, '%Y-%m-%d') AS d, COUNT(*) AS c " +
+                "FROM safety_issue WHERE deleted=0 AND found_at >= #{from} AND found_at < #{to} " +
+                "GROUP BY d ORDER BY d";
+    }
+
+    public String top5HighSql() {
+        return "SELECT id, description, risk_level, status, found_at, rectified_at, " +
+                "deleted, create_time, update_time " +
+                "FROM safety_issue WHERE deleted=0 AND risk_level='HIGH' AND status='PENDING' " +
+                "ORDER BY found_at DESC LIMIT 5";
+    }
+}

+ 29 - 0
supervision-system/src/main/java/com/supervision/safety/service/SafetyService.java

@@ -0,0 +1,29 @@
+package com.supervision.safety.service;
+
+import java.util.List;
+import java.util.Map;
+
+import com.supervision.safety.domain.*;
+
+import java.util.Date;
+
+public interface SafetyService {
+
+    Long createIssue(CreateIssueReq req);
+
+    void updateIssueStatus(Long id, IssueStatus status);
+
+    void deleteIssue(Long id);
+
+    PageResult<SafetyIssue> search(String keyword,
+                                   RiskLevel level,
+                                   IssueStatus status,
+                                   Date from, Date to,
+                                   int pageNum, int pageSize);
+
+    Map<String, Long> counters();
+
+    List<Map<String, Object>> weeklyTrend(int days);
+
+    List<SafetyIssue> top5High();
+}

+ 102 - 0
supervision-system/src/main/java/com/supervision/safety/service/impl/SafetyServiceImpl.java

@@ -0,0 +1,102 @@
+package com.supervision.safety.service.impl;
+
+import com.supervision.safety.domain.*;
+import com.supervision.safety.mapper.SafetyIssueMapper;
+import com.supervision.safety.service.SafetyService;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class SafetyServiceImpl implements SafetyService {
+
+    @Resource
+    private SafetyIssueMapper mapper;
+
+    @Override
+    public Long createIssue(CreateIssueReq req) {
+        SafetyIssue i = new SafetyIssue();
+        i.setDescription(req.getDescription());
+        i.setRiskLevel(req.getRiskLevel());
+        i.setStatus(IssueStatus.PENDING);
+        i.setFoundAt(req.getFoundAt());
+        i.setRectifiedAt(null);
+        i.setDeleted(false);
+        i.setCreateTime(new Date());
+        i.setUpdateTime(new Date());
+        mapper.insert(i);
+        return i.getId();
+    }
+
+    @Override
+    public void updateIssueStatus(Long id, IssueStatus status) {
+        Date rectifiedAt = (status == IssueStatus.RECTIFIED) ? new Date() : null;
+        int n = mapper.updateStatus(id, status, rectifiedAt);
+        if (n == 0) throw new RuntimeException("Issue not found or deleted");
+    }
+
+    @Override
+    public void deleteIssue(Long id) {
+        mapper.logicalDelete(id);
+    }
+
+    @Override
+    public PageResult<SafetyIssue> search(String keyword, RiskLevel level, IssueStatus status,
+                                          Date from, Date to, int pageNum, int pageSize) {
+        int limit = pageSize;
+        int offset = (Math.max(pageNum, 1) - 1) * pageSize;
+        List<SafetyIssue> list = mapper.search(keyword, level, status, from, to, limit, offset);
+        long total = mapper.count(keyword, level, status, from, to);
+        return new PageResult<>(total, pageNum, pageSize, list);
+    }
+
+    @Override
+    public Map<String, Long> counters() {
+        Map<String, Long> map = new HashMap<>();
+        map.put("HIGH", 0L); map.put("MEDIUM", 0L); map.put("LOW", 0L);
+        List<Map<String, Object>> rows = mapper.countPendingByLevel();
+        if (!CollectionUtils.isEmpty(rows)) {
+            for (Map<String,Object> r : rows) {
+                String lvl = String.valueOf(r.get("lvl"));
+                Long cnt = ((Number) r.get("cnt")).longValue();
+                map.put(lvl, cnt);
+            }
+        }
+        return map;
+    }
+
+    @Override
+    public List<Map<String, Object>> weeklyTrend(int days) {
+        Calendar cal = Calendar.getInstance();
+        Date to = cal.getTime();
+        cal.add(Calendar.DATE, -days + 1);
+        Date from = cal.getTime();
+
+        List<Map<String, Object>> rows = mapper.trendByDay(from, to);
+        Map<String, Long> hit = new HashMap<>();
+        for (Map<String, Object> r : rows) {
+            hit.put(String.valueOf(r.get("d")), ((Number) r.get("c")).longValue());
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        List<Map<String, Object>> out = new ArrayList<>();
+        Calendar c = Calendar.getInstance(); c.setTime(from);
+        while (!c.getTime().after(to)) {
+            String key = sdf.format(c.getTime());
+            Map<String,Object> item = new HashMap<>();
+            item.put("date", key);
+            item.put("count", hit.getOrDefault(key, 0L));
+            out.add(item);
+            c.add(Calendar.DATE, 1);
+        }
+        return out;
+    }
+
+    @Override
+    public List<SafetyIssue> top5High() {
+        return mapper.top5High();
+    }
+}