ソースを参照

安全风险预警模块新增

gao 2 日 前
コミット
d0ba620da7

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

+ 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();
+    }
+}