Parcourir la source

fix:应急事件bug修改

null il y a 3 semaines
Parent
commit
05b2bc4415
29 fichiers modifiés avec 2396 ajouts et 0 suppressions
  1. 60 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventAssessController.java
  2. 136 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventController.java
  3. 74 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventRecordController.java
  4. 79 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisPlanLibraryController.java
  5. 137 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEvent.java
  6. 92 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventAssess.java
  7. 66 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventRecord.java
  8. 66 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlanLibrary.java
  9. 65 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlanMatchRecord.java
  10. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventAssessSaveDTO.java
  11. 59 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventPageDTO.java
  12. 40 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventRecordPageDTO.java
  13. 139 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventVO.java
  14. 29 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventStatsVO.java
  15. 37 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanMatchDTO.java
  16. 46 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanMatchResultVO.java
  17. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventAssessMapper.java
  18. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventMapper.java
  19. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventRecordMapper.java
  20. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisPlanLibraryMapper.java
  21. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisPlanMatchRecordMapper.java
  22. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventAssessService.java
  23. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventRecordService.java
  24. 77 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventService.java
  25. 42 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisPlanLibraryService.java
  26. 172 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventAssessServiceImpl.java
  27. 102 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventRecordServiceImpl.java
  28. 543 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventServiceImpl.java
  29. 191 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisPlanLibraryServiceImpl.java

+ 60 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventAssessController.java

@@ -0,0 +1,60 @@
+package com.zksy.web.controller.dispatch;
+
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.dispatch.domain.DisEventAssess;
+import com.zksy.dispatch.dto.DisEventAssessSaveDTO;
+import com.zksy.dispatch.service.DisEventAssessService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 应急事件事后评估接口(评估挂靠统一事件主表)
+ * 事件列表(已解除/已关闭) -> 四维评分评估 -> 生成报告/录入案例库
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/dispatch/assess")
+@Api(tags = "应急事件事后评估")
+public class DisEventAssessController {
+
+    @Autowired
+    private DisEventAssessService service;
+
+    @GetMapping("/findByEventId")
+    @ApiOperation("查询单个事件的评估记录(无则返回 null)")
+    @Anonymous
+    public AjaxResult findByEventId(@RequestParam Long eventId) {
+        return AjaxResult.success(service.findByEventId(eventId));
+    }
+
+    @GetMapping("/listByEventIds")
+    @ApiOperation("批量查询事件的评估记录(列表标记已评估/待评估)")
+    @Anonymous
+    public AjaxResult listByEventIds(@RequestParam List<Long> eventIds) {
+        return AjaxResult.success(service.listByEventIds(eventIds));
+    }
+
+    @PostMapping("/save")
+    @ApiOperation("保存/更新事件评估(有则更新,无则新增;自动计算综合分与等级)")
+    @Log(title = "保存事件事后评估", businessType = BusinessType.INSERT)
+    public AjaxResult save(@RequestBody DisEventAssessSaveDTO dto) {
+        DisEventAssess saved = service.saveAssess(dto);
+        return AjaxResult.success("评估保存成功", saved);
+    }
+
+    @DeleteMapping("/delete")
+    @ApiOperation("删除事件的评估记录(重新评估)")
+    @Log(title = "删除事件事后评估", businessType = BusinessType.DELETE)
+    public AjaxResult delete(@RequestParam Long eventId) {
+        service.deleteByEventId(eventId);
+        return AjaxResult.success("评估已重置");
+    }
+}

+ 136 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventController.java

@@ -0,0 +1,136 @@
+package com.zksy.web.controller.dispatch;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.dispatch.domain.DisEvent;
+import com.zksy.dispatch.domain.DisEventTrack;
+import com.zksy.dispatch.domain.DisEventTransfer;
+import com.zksy.dispatch.dto.DisEventPageDTO;
+import com.zksy.dispatch.dto.DisEventVO;
+import com.zksy.dispatch.dto.EventStatsVO;
+import com.zksy.dispatch.service.DisEventService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 应急事件统一档案接口(全生命周期闭环)
+ * 接报 -> 受理 -> 核实 -> 转接/报送 -> 跟踪/催办 -> 解除 -> 关闭
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/dispatch/event")
+@Api(tags = "应急事件统一档案(闭环)")
+public class DisEventController {
+
+    @Autowired
+    private DisEventService service;
+
+    @GetMapping("/findByPage")
+    @ApiOperation("统一事件分页查询(多条件+状态过滤)")
+    @Anonymous
+    public AjaxResult findByPage(DisEventPageDTO dto) {
+        Page<DisEventVO> page = service.findByPage(dto);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/getDetail/{id}")
+    @ApiOperation("事件详情(主表信息+各环节明细)")
+    @Anonymous
+    public AjaxResult getDetail(@PathVariable Long id) {
+        return AjaxResult.success(service.getDetail(id));
+    }
+
+    @GetMapping("/stats")
+    @ApiOperation("多维条件统计(事件总数/损失合计/平均损失/重大及以上事件数)")
+    @Anonymous
+    public AjaxResult stats(DisEventPageDTO dto) {
+        EventStatsVO stats = service.stats(dto);
+        return AjaxResult.success(stats);
+    }
+
+    @PostMapping("/saveReport")
+    @ApiOperation("新增接报(自动生成事件编号,创建统一事件主表)")
+    @Log(title = "新增事件接报", businessType = BusinessType.INSERT)
+    public AjaxResult saveReport(@RequestBody DisEvent event) {
+        DisEvent saved = service.saveReport(event);
+        return AjaxResult.success("接报成功", saved);
+    }
+
+    @PostMapping("/accept")
+    @ApiOperation("受理(待受理 -> 待核实)")
+    @Log(title = "受理事件", businessType = BusinessType.UPDATE)
+    public AjaxResult accept(@RequestBody DisEvent event) {
+        service.accept(event.getId());
+        return AjaxResult.success("受理成功");
+    }
+
+    @PostMapping("/verify")
+    @ApiOperation("核实(更新主表初判信息,不实事件为终止态)")
+    @Log(title = "核实事件", businessType = BusinessType.UPDATE)
+    public AjaxResult verify(@RequestBody DisEvent event) {
+        service.verify(event);
+        return AjaxResult.success("核实成功");
+    }
+
+    @PostMapping("/transfer")
+    @ApiOperation("转接(已核实 -> 处置中,更新责任单位)")
+    @Log(title = "转接事件", businessType = BusinessType.UPDATE)
+    public AjaxResult transfer(@RequestBody DisEventTransfer transfer) {
+        service.transfer(transfer);
+        return AjaxResult.success("转接成功");
+    }
+
+    @PostMapping("/report")
+    @ApiOperation("报送(更新报送层级与附件)")
+    @Log(title = "报送事件", businessType = BusinessType.UPDATE)
+    public AjaxResult report(@RequestBody DisEventTransfer transfer) {
+        service.report(transfer);
+        return AjaxResult.success("报送成功");
+    }
+
+    @PostMapping("/track")
+    @ApiOperation("跟踪(写跟踪单与日志,更新回访时间)")
+    @Log(title = "跟踪事件", businessType = BusinessType.INSERT)
+    public AjaxResult track(@RequestBody DisEventTrack track) {
+        service.track(track);
+        return AjaxResult.success("跟踪记录保存成功");
+    }
+
+    @PostMapping("/release")
+    @ApiOperation("解除(处置中 -> 已解除)")
+    @Log(title = "解除事件", businessType = BusinessType.UPDATE)
+    public AjaxResult release(@RequestBody DisEvent event) {
+        service.release(event.getId());
+        return AjaxResult.success("事件已解除");
+    }
+
+    @PostMapping("/close")
+    @ApiOperation("关闭(已解除 -> 已关闭,记录关闭原因/损失)")
+    @Log(title = "关闭事件", businessType = BusinessType.UPDATE)
+    public AjaxResult close(@RequestBody DisEvent event) {
+        service.close(event);
+        return AjaxResult.success("事件已关闭");
+    }
+
+    @PutMapping("/updateById")
+    @ApiOperation("编辑事件基本信息(编号/状态不可改)")
+    @Log(title = "修改事件", businessType = BusinessType.UPDATE)
+    public AjaxResult updateById(@RequestBody DisEvent entity) {
+        service.updateEvent(entity);
+        return AjaxResult.success("修改成功");
+    }
+
+    @DeleteMapping("/deleteById")
+    @ApiOperation("删除事件")
+    @Log(title = "删除事件", businessType = BusinessType.DELETE)
+    public AjaxResult deleteById(@RequestParam Long id) {
+        boolean deleted = service.removeById(id);
+        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+}

+ 74 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisEventRecordController.java

@@ -0,0 +1,74 @@
+package com.zksy.web.controller.dispatch;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.dispatch.domain.DisEventRecord;
+import com.zksy.dispatch.dto.DisEventRecordPageDTO;
+import com.zksy.dispatch.dto.EventStatsVO;
+import com.zksy.dispatch.service.DisEventRecordService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/dispatch/event-record")
+@Api(tags = "应急事件数据查询与统计")
+public class DisEventRecordController {
+
+    @Autowired
+    private DisEventRecordService service;
+
+    @GetMapping("/findByPage")
+    @ApiOperation("多维条件分页查询")
+    @Anonymous
+    public AjaxResult findByPage(DisEventRecordPageDTO dto) {
+        Page<DisEventRecord> page = service.findByPage(dto);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/stats")
+    @ApiOperation("多维条件统计(事件总数/损失合计/平均损失/重大及以上事件数)")
+    @Anonymous
+    public AjaxResult stats(DisEventRecordPageDTO dto) {
+        EventStatsVO stats = service.stats(dto);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/getById/{id}")
+    @ApiOperation("根据ID查询事件记录")
+    @Anonymous
+    public AjaxResult getById(@PathVariable Long id) {
+        DisEventRecord entity = service.getById(id);
+        return entity != null ? AjaxResult.success(entity) : AjaxResult.error("事件记录不存在");
+    }
+
+    @PostMapping("/save")
+    @ApiOperation("新增事件记录")
+    @Log(title = "新增事件记录", businessType = BusinessType.INSERT)
+    public AjaxResult save(@RequestBody DisEventRecord entity) {
+        boolean saved = service.save(entity);
+        return saved ? AjaxResult.success("新增成功", saved) : AjaxResult.error("新增失败");
+    }
+
+    @PutMapping("/updateById")
+    @ApiOperation("修改事件记录")
+    @Log(title = "修改事件记录", businessType = BusinessType.UPDATE)
+    public AjaxResult updateById(@RequestBody DisEventRecord entity) {
+        boolean updated = service.updateById(entity);
+        return updated ? AjaxResult.success("修改成功", updated) : AjaxResult.error("修改失败");
+    }
+
+    @DeleteMapping("/deleteById")
+    @ApiOperation("删除事件记录")
+    @Log(title = "删除事件记录", businessType = BusinessType.DELETE)
+    public AjaxResult deleteById(@RequestParam Long id) {
+        boolean deleted = service.removeById(id);
+        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+}

+ 79 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/dispatch/DisPlanLibraryController.java

@@ -0,0 +1,79 @@
+package com.zksy.web.controller.dispatch;
+
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.dispatch.domain.DisPlanMatchRecord;
+import com.zksy.dispatch.dto.PlanMatchDTO;
+import com.zksy.dispatch.dto.PlanMatchResultVO;
+import com.zksy.dispatch.service.DisPlanLibraryService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/dispatch/plan-library")
+@Api(tags = "数字预案库匹配")
+public class DisPlanLibraryController {
+
+    @Autowired
+    private DisPlanLibraryService service;
+
+    @GetMapping("/list")
+    @ApiOperation("查询数字预案库全部预案")
+    @Anonymous
+    public AjaxResult list() {
+        return AjaxResult.success(service.listAll());
+    }
+
+    @PostMapping("/match")
+    @ApiOperation("按事件条件加权匹配预案")
+    @Log(title = "匹配应急预案", businessType = BusinessType.OTHER)
+    public AjaxResult match(@RequestBody PlanMatchDTO dto) {
+        List<PlanMatchResultVO> results = service.match(dto, currentUser());
+        return AjaxResult.success(results);
+    }
+
+    @PostMapping("/matchByTopic")
+    @ApiOperation("按专题匹配预案")
+    @Log(title = "专题匹配应急预案", businessType = BusinessType.OTHER)
+    public AjaxResult matchByTopic(@RequestBody PlanMatchDTO dto) {
+        List<PlanMatchResultVO> results = service.matchByTopic(dto.getTopic(), currentUser());
+        return AjaxResult.success(results);
+    }
+
+    @GetMapping("/history")
+    @ApiOperation("查询最近匹配记录")
+    @Anonymous
+    public AjaxResult history(@RequestParam(value = "limit", defaultValue = "10") Integer limit) {
+        return AjaxResult.success(service.getHistory(limit));
+    }
+
+    @PostMapping("/apply")
+    @ApiOperation("应用预案")
+    @Log(title = "应用应急预案", businessType = BusinessType.OTHER)
+    public AjaxResult apply(@RequestBody PlanMatchDTO dto) {
+        if (dto.getPlanId() == null) {
+            return AjaxResult.error("预案ID不能为空");
+        }
+        return AjaxResult.success("预案应用成功");
+    }
+
+    /**
+     * 获取当前登录用户名,未登录时返回null
+     */
+    private String currentUser() {
+        try {
+            return SecurityUtils.getUsername();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

+ 137 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEvent.java

@@ -0,0 +1,137 @@
+package com.zksy.dispatch.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 应急事件统一档案(全生命周期主表)
+ * 状态机:待受理 -> 待核实 -> 已核实 -> 处置中 -> 已解除 -> 已关闭;不实事件为终止态
+ * 各环节业务表(接报/核实/转接/催办/跟踪)通过 eventId 挂靠本表,形成业务闭环
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "dis_event", schema = "app_user")
+public class DisEvent {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("事件编号(SJ+yyyyMMddHHmmss 自动生成,全局唯一)")
+    private String eventNo;
+
+    @ApiModelProperty("事件名称")
+    private String eventName;
+
+    @ApiModelProperty("事件类型:排水/供水/燃气/其他")
+    private String eventType;
+
+    @ApiModelProperty("事件类别:管道泄漏/塌陷/积水/设备故障")
+    private String category;
+
+    @ApiModelProperty("事件等级:一般/较大/重大/特别重大")
+    @TableField("\"level\"")
+    private String level;
+
+    @ApiModelProperty("发生地点")
+    private String location;
+
+    @ApiModelProperty("发生区域:城南/城北/太常")
+    private String region;
+
+    @ApiModelProperty("事件描述")
+    private String description;
+
+    @ApiModelProperty("事件来源:巡查上报/呼叫中心/视频监控/上级下达")
+    private String source;
+
+    @ApiModelProperty("来源渠道")
+    private String channel;
+
+    @ApiModelProperty("接报人")
+    private String reporter;
+
+    @ApiModelProperty("接报时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime reportTime;
+
+    @ApiModelProperty("发生时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime happenTime;
+
+    @ApiModelProperty("初判时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime judgeTime;
+
+    @ApiModelProperty("核实人")
+    private String verifier;
+
+    @ApiModelProperty("核实时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime verifyTime;
+
+    @ApiModelProperty("核实意见")
+    private String verifyOpinion;
+
+    @ApiModelProperty("责任单位(转接部门/责任部门)")
+    private String department;
+
+    @ApiModelProperty("处置负责人")
+    private String handler;
+
+    @ApiModelProperty("应用预案ID(关联 dis_plan_library)")
+    private Long planId;
+
+    @ApiModelProperty("应用预案名称")
+    private String planName;
+
+    @ApiModelProperty("直接经济损失(万元)")
+    private BigDecimal lossAmount;
+
+    @ApiModelProperty("事件状态:待受理/待核实/已核实/处置中/已解除/已关闭/不实事件")
+    private String status;
+
+    @ApiModelProperty("关闭原因:事件不存在/处置完成/误报")
+    private String closeReason;
+
+    @ApiModelProperty("关闭说明")
+    private String closeRemark;
+
+    @ApiModelProperty("关闭时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime closeTime;
+
+    @TableField(exist = false)
+    @ApiModelProperty("是否真实(核实入参:真实/不实,不落库)")
+    private String isReal;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0=正常 2=删除)")
+    private String delFlag;
+}

+ 92 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventAssess.java

@@ -0,0 +1,92 @@
+package com.zksy.dispatch.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 事件事后评估表(挂靠统一事件主表,一个事件一条评估记录)
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "dis_event_assess", schema = "app_user")
+public class DisEventAssess {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("关联统一事件主表ID(dis_event.id)")
+    private Long relEventId;
+
+    @ApiModelProperty("事件编号")
+    private String eventNo;
+
+    @ApiModelProperty("应急组织体系评分(0-5,允许0.5步进)")
+    private BigDecimal orgScore;
+
+    @ApiModelProperty("应急组织体系评语")
+    private String orgComment;
+
+    @ApiModelProperty("应急能力评分(0-5,允许0.5步进)")
+    private BigDecimal capScore;
+
+    @ApiModelProperty("应急能力评语")
+    private String capComment;
+
+    @ApiModelProperty("处置效果评分(0-5,允许0.5步进)")
+    private BigDecimal effectScore;
+
+    @ApiModelProperty("处置效果评语")
+    private String effectComment;
+
+    @ApiModelProperty("灾害损失评分(0-5,允许0.5步进)")
+    private BigDecimal lossScore;
+
+    @ApiModelProperty("灾害损失评语")
+    private String lossComment;
+
+    @ApiModelProperty("综合评分(四维平均,保留1位小数)")
+    private BigDecimal totalScore;
+
+    @ApiModelProperty("评价等级:优秀/良好/一般/较差")
+    private String grade;
+
+    @ApiModelProperty("是否录入案例库:0=否 1=是")
+    private String caseFlag;
+
+    @ApiModelProperty("评估人")
+    private String assessor;
+
+    @ApiModelProperty("评估时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime assessTime;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0=正常 2=删除)")
+    private String delFlag;
+}

+ 66 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisEventRecord.java

@@ -0,0 +1,66 @@
+package com.zksy.dispatch.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 应急事件数据记录(数据查询与统计)
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "dis_event_record", schema = "app_user")
+public class DisEventRecord {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("事件名称")
+    private String eventName;
+
+    @ApiModelProperty("事件类型(自然灾害/事故灾难/公共卫生/社会安全/管网泄漏/管道堵塞)")
+    private String eventType;
+
+    @ApiModelProperty("发生时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private LocalDate eventTime;
+
+    @ApiModelProperty("事件等级(一般/较大/重大/特别重大)")
+    private String eventLevel;
+
+    @ApiModelProperty("地域(太常/城东/城南/城北片区)")
+    private String region;
+
+    @ApiModelProperty("责任部门(应急管理局/消防救援局/市政管理局/水务局)")
+    private String department;
+
+    @ApiModelProperty("直接经济损失(万元)")
+    private BigDecimal lossAmount;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0=正常 2=删除)")
+    private String delFlag;
+}

+ 66 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlanLibrary.java

@@ -0,0 +1,66 @@
+package com.zksy.dispatch.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 数字预案库(应急预案选择匹配)
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "dis_plan_library", schema = "app_user")
+public class DisPlanLibrary {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("预案名称")
+    private String planName;
+
+    @ApiModelProperty("事件类型(排水/供水/燃气)")
+    private String eventType;
+
+    @ApiModelProperty("事件级别")
+    private String eventLevel;
+
+    @ApiModelProperty("发生区域(城南/城北/太常)")
+    private String eventRegion;
+
+    @ApiModelProperty("责任单位(供水局/燃气公司/排水公司)")
+    private String responsibleUnit;
+
+    @ApiModelProperty("所属专题(排水专题/供水专题/燃气专题)")
+    private String topic;
+
+    @ApiModelProperty("预案概要")
+    private String planSummary;
+
+    @ApiModelProperty("预案内容")
+    private String planContent;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0=正常 2=删除)")
+    private String delFlag;
+}

+ 65 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/domain/DisPlanMatchRecord.java

@@ -0,0 +1,65 @@
+package com.zksy.dispatch.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 预案匹配/应用记录(最近匹配记录)
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "dis_plan_match_record", schema = "app_user")
+public class DisPlanMatchRecord {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("事件类型")
+    private String eventType;
+
+    @ApiModelProperty("事件级别")
+    private String eventLevel;
+
+    @ApiModelProperty("发生区域")
+    private String eventRegion;
+
+    @ApiModelProperty("责任单位")
+    private String responsibleUnit;
+
+    @ApiModelProperty("匹配类型(manual手动/topic专题/apply应用)")
+    private String matchType;
+
+    @ApiModelProperty("预案ID")
+    private Long planId;
+
+    @ApiModelProperty("预案名称")
+    private String planName;
+
+    @ApiModelProperty("匹配度(0-100)")
+    private Integer matchScore;
+
+    @ApiModelProperty("记录描述")
+    private String recordDesc;
+
+    @ApiModelProperty("操作人")
+    private String createBy;
+
+    @ApiModelProperty("操作时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableLogic(value = "0", delval = "2")
+    @ApiModelProperty("删除标志(0=正常 2=删除)")
+    private String delFlag;
+}

+ 45 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventAssessSaveDTO.java

@@ -0,0 +1,45 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 事件事后评估保存入参
+ * 场景一:生成评估报告(带四维评分/评语,自动计算综合分与等级)
+ * 场景二:录入案例库(仅传 eventId + caseFlag,不覆盖已有评估内容)
+ */
+@Data
+public class DisEventAssessSaveDTO {
+
+    @ApiModelProperty("关联统一事件主表ID(必填)")
+    private Long eventId;
+
+    @ApiModelProperty("应急组织体系评分(0-5)")
+    private BigDecimal orgScore;
+
+    @ApiModelProperty("应急组织体系评语")
+    private String orgComment;
+
+    @ApiModelProperty("应急能力评分(0-5)")
+    private BigDecimal capScore;
+
+    @ApiModelProperty("应急能力评语")
+    private String capComment;
+
+    @ApiModelProperty("处置效果评分(0-5)")
+    private BigDecimal effectScore;
+
+    @ApiModelProperty("处置效果评语")
+    private String effectComment;
+
+    @ApiModelProperty("灾害损失评分(0-5)")
+    private BigDecimal lossScore;
+
+    @ApiModelProperty("灾害损失评语")
+    private String lossComment;
+
+    @ApiModelProperty("是否录入案例库:0=否 1=是")
+    private String caseFlag;
+}

+ 59 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventPageDTO.java

@@ -0,0 +1,59 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@ApiModel(value = "统一事件分页查询入参", description = "应急事件统一档案分页查询入参")
+public class DisEventPageDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "事件编号(模糊)")
+    private String eventNo;
+
+    @ApiModelProperty(value = "事件名称(模糊)")
+    private String eventName;
+
+    @ApiModelProperty(value = "事件类型")
+    private String eventType;
+
+    @ApiModelProperty(value = "事件类别")
+    private String category;
+
+    @ApiModelProperty(value = "事件等级")
+    private String level;
+
+    @ApiModelProperty(value = "事件来源")
+    private String source;
+
+    @ApiModelProperty(value = "事件状态(单状态过滤)")
+    private String status;
+
+    @ApiModelProperty(value = "事件状态列表(多状态过滤,优先于status)")
+    private List<String> statusList;
+
+    @ApiModelProperty(value = "发生区域")
+    private String region;
+
+    @ApiModelProperty(value = "责任单位")
+    private String department;
+
+    @ApiModelProperty(value = "开始日期(yyyy-MM-dd,按发生时间)")
+    private String startDate;
+
+    @ApiModelProperty(value = "结束日期(yyyy-MM-dd,按发生时间)")
+    private String endDate;
+
+    @ApiModelProperty(value = "损失情况(100万以下/100-500万/500-1000万/1000万以上)")
+    private String lossRange;
+
+    @ApiModelProperty(value = "当前记录起始索引", required = true)
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数", required = true)
+    private Long pageSize;
+}

+ 40 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventRecordPageDTO.java

@@ -0,0 +1,40 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel(value = "应急事件记录分页查询入参", description = "数据查询与统计分页查询入参")
+public class DisEventRecordPageDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "事件名称(模糊)")
+    private String eventName;
+
+    @ApiModelProperty(value = "事件类型")
+    private String eventType;
+
+    @ApiModelProperty(value = "发生时间(yyyy-MM-dd)")
+    private String eventTime;
+
+    @ApiModelProperty(value = "事件等级")
+    private String eventLevel;
+
+    @ApiModelProperty(value = "地域")
+    private String region;
+
+    @ApiModelProperty(value = "责任部门")
+    private String department;
+
+    @ApiModelProperty(value = "损失情况(100万以下/100-500万/500-1000万/1000万以上)")
+    private String lossRange;
+
+    @ApiModelProperty(value = "当前记录起始索引", required = true)
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数", required = true)
+    private Long pageSize;
+}

+ 139 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/DisEventVO.java

@@ -0,0 +1,139 @@
+package com.zksy.dispatch.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 统一事件详情/列表展示VO
+ * 在主表字段基础上,扩展最近环节信息(转接/报送/跟踪),便于各业务页面直接展示
+ */
+@Data
+@ApiModel(value = "统一事件展示VO", description = "统一事件详情/列表展示VO")
+public class DisEventVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "事件编号")
+    private String eventNo;
+
+    @ApiModelProperty(value = "事件名称")
+    private String eventName;
+
+    @ApiModelProperty(value = "事件类型:排水/供水/燃气/其他")
+    private String eventType;
+
+    @ApiModelProperty(value = "事件类别:管道泄漏/塌陷/积水/设备故障")
+    private String category;
+
+    @ApiModelProperty(value = "事件等级:一般/较大/重大/特别重大")
+    private String level;
+
+    @ApiModelProperty(value = "发生地点")
+    private String location;
+
+    @ApiModelProperty(value = "发生区域:城南/城北/太常")
+    private String region;
+
+    @ApiModelProperty(value = "事件描述")
+    private String description;
+
+    @ApiModelProperty(value = "事件来源:巡查上报/呼叫中心/视频监控/上级下达")
+    private String source;
+
+    @ApiModelProperty(value = "来源渠道")
+    private String channel;
+
+    @ApiModelProperty(value = "接报人")
+    private String reporter;
+
+    @ApiModelProperty(value = "接报时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime reportTime;
+
+    @ApiModelProperty(value = "发生时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime happenTime;
+
+    @ApiModelProperty(value = "初判时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime judgeTime;
+
+    @ApiModelProperty(value = "核实人")
+    private String verifier;
+
+    @ApiModelProperty(value = "核实时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime verifyTime;
+
+    @ApiModelProperty(value = "核实意见")
+    private String verifyOpinion;
+
+    @ApiModelProperty(value = "责任单位(转接部门/责任部门)")
+    private String department;
+
+    @ApiModelProperty(value = "处置负责人")
+    private String handler;
+
+    @ApiModelProperty(value = "应用预案ID")
+    private Long planId;
+
+    @ApiModelProperty(value = "应用预案名称")
+    private String planName;
+
+    @ApiModelProperty(value = "直接经济损失(万元)")
+    private BigDecimal lossAmount;
+
+    @ApiModelProperty(value = "事件状态")
+    private String status;
+
+    @ApiModelProperty(value = "关闭原因")
+    private String closeReason;
+
+    @ApiModelProperty(value = "关闭说明")
+    private String closeRemark;
+
+    @ApiModelProperty(value = "关闭时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime closeTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty(value = "最近转接部门")
+    private String transferDept;
+
+    @ApiModelProperty(value = "最近报送层级")
+    private String reportTargets;
+
+    @ApiModelProperty(value = "最近转接时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime transferTime;
+
+    @ApiModelProperty(value = "关联转接记录ID(最近一条)")
+    private Long transferId;
+
+    @ApiModelProperty(value = "最近回访时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime lastVisitTime;
+
+    @ApiModelProperty(value = "下次回访时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime nextVisitTime;
+
+    @ApiModelProperty(value = "关联跟踪记录ID(最近一条)")
+    private Long trackId;
+
+    @ApiModelProperty(value = "环节明细(详情用):verifyList/transferList/trackList/urgeList")
+    private Map<String, List<Map<String, Object>>> process;
+}

+ 29 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/EventStatsVO.java

@@ -0,0 +1,29 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 应急事件统计数据
+ */
+@Data
+@ApiModel(value = "应急事件统计数据", description = "多维条件查询后的统计结果")
+public class EventStatsVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "事件总数")
+    private Long totalEvents;
+
+    @ApiModelProperty(value = "直接经济损失合计(万元)")
+    private BigDecimal totalLoss;
+
+    @ApiModelProperty(value = "平均损失(万元)")
+    private BigDecimal avgLoss;
+
+    @ApiModelProperty(value = "重大及以上事件数")
+    private Long majorEvents;
+}

+ 37 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanMatchDTO.java

@@ -0,0 +1,37 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel(value = "预案匹配入参", description = "突发事件条件设定匹配入参")
+public class PlanMatchDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "事件类型(排水/供水/燃气)")
+    private String eventType;
+
+    @ApiModelProperty(value = "事件级别")
+    private String eventLevel;
+
+    @ApiModelProperty(value = "发生区域(城南/城北/太常)")
+    private String eventRegion;
+
+    @ApiModelProperty(value = "责任单位(供水局/燃气公司/排水公司)")
+    private String responsibleUnit;
+
+    @ApiModelProperty(value = "专题名称(专题匹配时传入)")
+    private String topic;
+
+    @ApiModelProperty(value = "预案ID(应用预案时传入)")
+    private Long planId;
+
+    @ApiModelProperty(value = "预案名称(应用预案时传入)")
+    private String planName;
+
+    @ApiModelProperty(value = "匹配度(应用预案时传入)")
+    private Integer matchScore;
+}

+ 46 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/dto/PlanMatchResultVO.java

@@ -0,0 +1,46 @@
+package com.zksy.dispatch.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 预案匹配结果
+ */
+@Data
+@ApiModel(value = "预案匹配结果", description = "预案匹配结果(含匹配度)")
+public class PlanMatchResultVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "预案ID")
+    private Long id;
+
+    @ApiModelProperty(value = "预案名称")
+    private String planName;
+
+    @ApiModelProperty(value = "事件类型")
+    private String eventType;
+
+    @ApiModelProperty(value = "事件级别")
+    private String eventLevel;
+
+    @ApiModelProperty(value = "发生区域")
+    private String eventRegion;
+
+    @ApiModelProperty(value = "责任单位")
+    private String responsibleUnit;
+
+    @ApiModelProperty(value = "所属专题")
+    private String topic;
+
+    @ApiModelProperty(value = "预案概要")
+    private String planSummary;
+
+    @ApiModelProperty(value = "预案内容")
+    private String planContent;
+
+    @ApiModelProperty(value = "匹配度(0-100)")
+    private Integer matchScore;
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventAssessMapper.java

@@ -0,0 +1,9 @@
+package com.zksy.dispatch.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.dispatch.domain.DisEventAssess;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DisEventAssessMapper extends BaseMapper<DisEventAssess> {
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventMapper.java

@@ -0,0 +1,9 @@
+package com.zksy.dispatch.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.dispatch.domain.DisEvent;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DisEventMapper extends BaseMapper<DisEvent> {
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisEventRecordMapper.java

@@ -0,0 +1,9 @@
+package com.zksy.dispatch.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.dispatch.domain.DisEventRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DisEventRecordMapper extends BaseMapper<DisEventRecord> {
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisPlanLibraryMapper.java

@@ -0,0 +1,9 @@
+package com.zksy.dispatch.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.dispatch.domain.DisPlanLibrary;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DisPlanLibraryMapper extends BaseMapper<DisPlanLibrary> {
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/mapper/DisPlanMatchRecordMapper.java

@@ -0,0 +1,9 @@
+package com.zksy.dispatch.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zksy.dispatch.domain.DisPlanMatchRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DisPlanMatchRecordMapper extends BaseMapper<DisPlanMatchRecord> {
+}

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventAssessService.java

@@ -0,0 +1,34 @@
+package com.zksy.dispatch.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zksy.dispatch.domain.DisEventAssess;
+import com.zksy.dispatch.dto.DisEventAssessSaveDTO;
+
+import java.util.List;
+
+/**
+ * 事件事后评估 Service(评估与统一事件主表挂靠,一个事件一条评估记录)
+ */
+public interface DisEventAssessService extends IService<DisEventAssess> {
+
+    /**
+     * 查询单个事件的评估记录(无则返回 null)
+     */
+    DisEventAssess findByEventId(Long eventId);
+
+    /**
+     * 批量查询事件的评估记录(用于列表标记"已评估/待评估")
+     */
+    List<DisEventAssess> listByEventIds(List<Long> eventIds);
+
+    /**
+     * 保存/更新评估(有则更新,无则新增;自动计算综合分与等级;
+     * 仅传 eventId+caseFlag 时不覆盖已有评分内容)
+     */
+    DisEventAssess saveAssess(DisEventAssessSaveDTO dto);
+
+    /**
+     * 删除事件的评估记录(重新评估)
+     */
+    void deleteByEventId(Long eventId);
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventRecordService.java

@@ -0,0 +1,20 @@
+package com.zksy.dispatch.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zksy.dispatch.domain.DisEventRecord;
+import com.zksy.dispatch.dto.DisEventRecordPageDTO;
+import com.zksy.dispatch.dto.EventStatsVO;
+
+public interface DisEventRecordService extends IService<DisEventRecord> {
+
+    /**
+     * 多维条件分页查询
+     */
+    Page<DisEventRecord> findByPage(DisEventRecordPageDTO dto);
+
+    /**
+     * 多维条件统计(事件总数/损失合计/平均损失/重大及以上事件数)
+     */
+    EventStatsVO stats(DisEventRecordPageDTO dto);
+}

+ 77 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisEventService.java

@@ -0,0 +1,77 @@
+package com.zksy.dispatch.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zksy.dispatch.domain.DisEvent;
+import com.zksy.dispatch.domain.DisEventTrack;
+import com.zksy.dispatch.domain.DisEventTransfer;
+import com.zksy.dispatch.dto.DisEventPageDTO;
+import com.zksy.dispatch.dto.DisEventVO;
+import com.zksy.dispatch.dto.EventStatsVO;
+
+/**
+ * 应急事件统一档案 Service(全生命周期闭环)
+ * 接报 -> 受理 -> 核实 -> 转接/报送 -> 跟踪/催办 -> 解除 -> 关闭
+ */
+public interface DisEventService extends IService<DisEvent> {
+
+    /**
+     * 新增接报:自动生成事件编号,创建统一事件主表并同步写接报明细
+     */
+    DisEvent saveReport(DisEvent event);
+
+    /**
+     * 分页查询(主表多条件,含最近环节扩展信息)
+     */
+    Page<DisEventVO> findByPage(DisEventPageDTO dto);
+
+    /**
+     * 事件详情(主表信息 + 各环节明细:核实/转接/跟踪/催办)
+     */
+    DisEventVO getDetail(Long id);
+
+    /**
+     * 受理:待受理 -> 待核实
+     */
+    void accept(Long id);
+
+    /**
+     * 核实:更新主表初判信息并写核实明细;不实事件为终止态
+     */
+    void verify(DisEvent event);
+
+    /**
+     * 转接:已核实 -> 处置中,更新责任单位并写转接明细
+     */
+    void transfer(DisEventTransfer transfer);
+
+    /**
+     * 报送:更新转接明细报送层级与附件,事件保持处置中
+     */
+    void report(DisEventTransfer transfer);
+
+    /**
+     * 跟踪:写跟踪单与跟踪日志,更新回访时间
+     */
+    void track(DisEventTrack track);
+
+    /**
+     * 解除:处置中 -> 已解除
+     */
+    void release(Long id);
+
+    /**
+     * 关闭:已解除 -> 已关闭,记录关闭原因/说明/损失
+     */
+    void close(DisEvent event);
+
+    /**
+     * 多维条件统计(与分页查询同一套筛选条件)
+     */
+    EventStatsVO stats(DisEventPageDTO dto);
+
+    /**
+     * 编辑事件基本信息(事件编号/状态不可改,状态通过流转接口)
+     */
+    void updateEvent(DisEvent event);
+}

+ 42 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/DisPlanLibraryService.java

@@ -0,0 +1,42 @@
+package com.zksy.dispatch.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zksy.dispatch.domain.DisPlanLibrary;
+import com.zksy.dispatch.domain.DisPlanMatchRecord;
+import com.zksy.dispatch.dto.PlanMatchDTO;
+import com.zksy.dispatch.dto.PlanMatchResultVO;
+
+import java.util.List;
+
+public interface DisPlanLibraryService extends IService<DisPlanLibrary> {
+
+    /**
+     * 查询数字预案库全部预案
+     */
+    List<DisPlanLibrary> listAll();
+
+    /**
+     * 按事件条件加权匹配预案(类型40/级别25/区域20/单位15)
+     *
+     * @param dto      匹配条件
+     * @param operUser 操作人
+     * @return 按匹配度降序的结果,并记录匹配历史
+     */
+    List<PlanMatchResultVO> match(PlanMatchDTO dto, String operUser);
+
+    /**
+     * 按专题匹配预案
+     *
+     * @param topic    专题名称
+     * @param operUser 操作人
+     * @return 该专题下的预案列表(匹配度90),并记录匹配历史
+     */
+    List<PlanMatchResultVO> matchByTopic(String topic, String operUser);
+
+    /**
+     * 查询最近匹配/应用记录
+     *
+     * @param limit 记录条数
+     */
+    List<DisPlanMatchRecord> getHistory(int limit);
+}

+ 172 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventAssessServiceImpl.java

@@ -0,0 +1,172 @@
+package com.zksy.dispatch.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.dispatch.domain.DisEvent;
+import com.zksy.dispatch.domain.DisEventAssess;
+import com.zksy.dispatch.dto.DisEventAssessSaveDTO;
+import com.zksy.dispatch.mapper.DisEventAssessMapper;
+import com.zksy.dispatch.mapper.DisEventMapper;
+import com.zksy.dispatch.service.DisEventAssessService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 事件事后评估 Service 实现
+ */
+@Slf4j
+@Service
+public class DisEventAssessServiceImpl extends ServiceImpl<DisEventAssessMapper, DisEventAssess>
+        implements DisEventAssessService {
+
+    @Autowired
+    private DisEventMapper eventMapper;
+
+    @Override
+    public DisEventAssess findByEventId(Long eventId) {
+        if (eventId == null) {
+            return null;
+        }
+        List<DisEventAssess> list = list(new LambdaQueryWrapper<DisEventAssess>()
+                .eq(DisEventAssess::getRelEventId, eventId)
+                .orderByDesc(DisEventAssess::getId));
+        return CollectionUtils.isEmpty(list) ? null : list.get(0);
+    }
+
+    @Override
+    public List<DisEventAssess> listByEventIds(List<Long> eventIds) {
+        if (CollectionUtils.isEmpty(eventIds)) {
+            return Collections.emptyList();
+        }
+        return list(new LambdaQueryWrapper<DisEventAssess>()
+                .in(DisEventAssess::getRelEventId, eventIds)
+                .orderByDesc(DisEventAssess::getId));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DisEventAssess saveAssess(DisEventAssessSaveDTO dto) {
+        if (dto.getEventId() == null) {
+            throw new ServiceException("事件ID不能为空");
+        }
+        DisEvent event = eventMapper.selectById(dto.getEventId());
+        if (event == null) {
+            throw new ServiceException("事件不存在:" + dto.getEventId());
+        }
+        LocalDateTime now = LocalDateTime.now();
+        DisEventAssess db = findByEventId(dto.getEventId());
+        boolean hasScore = dto.getOrgScore() != null || dto.getCapScore() != null
+                || dto.getEffectScore() != null || dto.getLossScore() != null;
+        if (db == null) {
+            db = new DisEventAssess();
+            db.setRelEventId(event.getId());
+            db.setEventNo(event.getEventNo());
+            db.setCreateTime(now);
+            db.setCreateBy(currentUser());
+            db.setDelFlag("0");
+        }
+        // 评分/评语非空才更新(录入案例库场景不覆盖已有评估)
+        if (dto.getOrgScore() != null) {
+            db.setOrgScore(dto.getOrgScore());
+        }
+        if (dto.getOrgComment() != null) {
+            db.setOrgComment(dto.getOrgComment());
+        }
+        if (dto.getCapScore() != null) {
+            db.setCapScore(dto.getCapScore());
+        }
+        if (dto.getCapComment() != null) {
+            db.setCapComment(dto.getCapComment());
+        }
+        if (dto.getEffectScore() != null) {
+            db.setEffectScore(dto.getEffectScore());
+        }
+        if (dto.getEffectComment() != null) {
+            db.setEffectComment(dto.getEffectComment());
+        }
+        if (dto.getLossScore() != null) {
+            db.setLossScore(dto.getLossScore());
+        }
+        if (dto.getLossComment() != null) {
+            db.setLossComment(dto.getLossComment());
+        }
+        if (dto.getCaseFlag() != null) {
+            db.setCaseFlag(dto.getCaseFlag());
+        }
+        // 有评分时计算综合分与等级
+        if (hasScore) {
+            BigDecimal total = calcTotal(db.getOrgScore(), db.getCapScore(),
+                    db.getEffectScore(), db.getLossScore());
+            db.setTotalScore(total);
+            db.setGrade(calcGrade(total));
+            db.setAssessor(currentUser());
+            db.setAssessTime(now);
+        }
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        saveOrUpdate(db);
+        return db;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteByEventId(Long eventId) {
+        if (eventId == null) {
+            throw new ServiceException("事件ID不能为空");
+        }
+        DisEventAssess db = findByEventId(eventId);
+        if (db != null) {
+            removeById(db.getId());
+        }
+    }
+
+    /**
+     * 综合评分:四维平均,保留 1 位小数
+     */
+    private BigDecimal calcTotal(BigDecimal org, BigDecimal cap, BigDecimal effect, BigDecimal loss) {
+        BigDecimal sum = nvl(org).add(nvl(cap)).add(nvl(effect)).add(nvl(loss));
+        return sum.divide(BigDecimal.valueOf(4), 1, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal nvl(BigDecimal v) {
+        return v == null ? BigDecimal.ZERO : v;
+    }
+
+    /**
+     * 评价等级:>=4.5 优秀,>=3.5 良好,>=2.5 一般,其余较差
+     */
+    private String calcGrade(BigDecimal total) {
+        if (total.compareTo(new BigDecimal("4.5")) >= 0) {
+            return "优秀";
+        }
+        if (total.compareTo(new BigDecimal("3.5")) >= 0) {
+            return "良好";
+        }
+        if (total.compareTo(new BigDecimal("2.5")) >= 0) {
+            return "一般";
+        }
+        return "较差";
+    }
+
+    /**
+     * 当前登录用户(未登录时返回系统)
+     */
+    private String currentUser() {
+        try {
+            return SecurityUtils.getUsername();
+        } catch (Exception e) {
+            return "system";
+        }
+    }
+}

+ 102 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventRecordServiceImpl.java

@@ -0,0 +1,102 @@
+package com.zksy.dispatch.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.dispatch.domain.DisEventRecord;
+import com.zksy.dispatch.dto.DisEventRecordPageDTO;
+import com.zksy.dispatch.dto.EventStatsVO;
+import com.zksy.dispatch.mapper.DisEventRecordMapper;
+import com.zksy.dispatch.service.DisEventRecordService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 应急事件数据记录 Service 实现(数据查询与统计)
+ */
+@Slf4j
+@Service
+public class DisEventRecordServiceImpl extends ServiceImpl<DisEventRecordMapper, DisEventRecord>
+        implements DisEventRecordService {
+
+    @Override
+    public Page<DisEventRecord> findByPage(DisEventRecordPageDTO dto) {
+        Page<DisEventRecord> page = new Page<>(dto.getPageNum(), dto.getPageSize());
+        return this.page(page, buildWrapper(dto));
+    }
+
+    @Override
+    public EventStatsVO stats(DisEventRecordPageDTO dto) {
+        List<DisEventRecord> records = this.list(buildWrapper(dto));
+        EventStatsVO vo = new EventStatsVO();
+        vo.setTotalEvents((long) records.size());
+        BigDecimal totalLoss = records.stream()
+                .map(DisEventRecord::getLossAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        vo.setTotalLoss(totalLoss);
+        vo.setAvgLoss(records.isEmpty() ? BigDecimal.ZERO
+                : totalLoss.divide(BigDecimal.valueOf(records.size()), 1, RoundingMode.HALF_UP));
+        long majorEvents = records.stream()
+                .filter(r -> "重大".equals(r.getEventLevel()) || "特别重大".equals(r.getEventLevel()))
+                .count();
+        vo.setMajorEvents(majorEvents);
+        return vo;
+    }
+
+    /**
+     * 构建多维条件查询条件(与页面筛选逻辑一致)
+     */
+    private LambdaQueryWrapper<DisEventRecord> buildWrapper(DisEventRecordPageDTO dto) {
+        LambdaQueryWrapper<DisEventRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.isNotEmpty(dto.getEventName()), DisEventRecord::getEventName, dto.getEventName());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getEventType()), DisEventRecord::getEventType, dto.getEventType());
+        if (StringUtils.isNotEmpty(dto.getEventTime())) {
+            try {
+                wrapper.eq(DisEventRecord::getEventTime, LocalDate.parse(dto.getEventTime()));
+            } catch (Exception e) {
+                log.warn("事件时间格式错误: {}", dto.getEventTime());
+            }
+        }
+        wrapper.eq(StringUtils.isNotEmpty(dto.getEventLevel()), DisEventRecord::getEventLevel, dto.getEventLevel());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getRegion()), DisEventRecord::getRegion, dto.getRegion());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getDepartment()), DisEventRecord::getDepartment, dto.getDepartment());
+        applyLossRange(wrapper, dto.getLossRange());
+        wrapper.orderByDesc(DisEventRecord::getEventTime);
+        return wrapper;
+    }
+
+    /**
+     * 损失情况区间条件
+     */
+    private void applyLossRange(LambdaQueryWrapper<DisEventRecord> wrapper, String lossRange) {
+        if (StringUtils.isEmpty(lossRange)) {
+            return;
+        }
+        switch (lossRange) {
+            case "100万以下":
+                wrapper.lt(DisEventRecord::getLossAmount, new BigDecimal(100));
+                break;
+            case "100-500万":
+                wrapper.ge(DisEventRecord::getLossAmount, new BigDecimal(100))
+                        .lt(DisEventRecord::getLossAmount, new BigDecimal(500));
+                break;
+            case "500-1000万":
+                wrapper.ge(DisEventRecord::getLossAmount, new BigDecimal(500))
+                        .lt(DisEventRecord::getLossAmount, new BigDecimal(1000));
+                break;
+            case "1000万以上":
+                wrapper.ge(DisEventRecord::getLossAmount, new BigDecimal(1000));
+                break;
+            default:
+                break;
+        }
+    }
+}

+ 543 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisEventServiceImpl.java

@@ -0,0 +1,543 @@
+package com.zksy.dispatch.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.dispatch.domain.DisEvent;
+import com.zksy.dispatch.domain.DisEventReport;
+import com.zksy.dispatch.domain.DisEventTrack;
+import com.zksy.dispatch.domain.DisEventTrackLog;
+import com.zksy.dispatch.domain.DisEventTransfer;
+import com.zksy.dispatch.domain.DisEventUrge;
+import com.zksy.dispatch.domain.DisEventVerify;
+import com.zksy.dispatch.dto.DisEventPageDTO;
+import com.zksy.dispatch.dto.DisEventVO;
+import com.zksy.dispatch.dto.EventStatsVO;
+import com.zksy.dispatch.mapper.DisEventMapper;
+import com.zksy.dispatch.mapper.DisEventReportMapper;
+import com.zksy.dispatch.mapper.DisEventTrackLogMapper;
+import com.zksy.dispatch.mapper.DisEventTrackMapper;
+import com.zksy.dispatch.mapper.DisEventTransferMapper;
+import com.zksy.dispatch.mapper.DisEventUrgeMapper;
+import com.zksy.dispatch.mapper.DisEventVerifyMapper;
+import com.zksy.dispatch.service.DisEventService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 应急事件统一档案 Service 实现(全生命周期闭环)
+ * 状态机:待受理 -> 待核实 -> 已核实 -> 处置中 -> 已解除 -> 已关闭;不实事件为终止态
+ */
+@Slf4j
+@Service
+public class DisEventServiceImpl extends ServiceImpl<DisEventMapper, DisEvent>
+        implements DisEventService {
+
+    /** 事件状态常量 */
+    private static final String STATUS_WAIT_ACCEPT = "待受理";
+    private static final String STATUS_WAIT_VERIFY = "待核实";
+    private static final String STATUS_VERIFIED = "已核实";
+    private static final String STATUS_PROCESSING = "处置中";
+    private static final String STATUS_RELEASED = "已解除";
+    private static final String STATUS_CLOSED = "已关闭";
+    private static final String STATUS_FALSE = "不实事件";
+
+    /** 事件编号格式:SJ+yyyyMMddHHmmss */
+    private static final DateTimeFormatter NO_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    @Autowired
+    private DisEventReportMapper reportMapper;
+
+    @Autowired
+    private DisEventVerifyMapper verifyMapper;
+
+    @Autowired
+    private DisEventTransferMapper transferMapper;
+
+    @Autowired
+    private DisEventTrackMapper trackMapper;
+
+    @Autowired
+    private DisEventTrackLogMapper trackLogMapper;
+
+    @Autowired
+    private DisEventUrgeMapper urgeMapper;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DisEvent saveReport(DisEvent event) {
+        LocalDateTime now = LocalDateTime.now();
+        // 自动生成全局唯一事件编号
+        event.setEventNo("SJ" + now.format(NO_FORMAT));
+        event.setStatus(STATUS_WAIT_ACCEPT);
+        event.setReportTime(now);
+        event.setCreateTime(now);
+        event.setCreateBy(currentUser());
+        save(event);
+        // 同步写接报明细(挂靠主表)
+        DisEventReport report = new DisEventReport();
+        report.setRelEventId(event.getId());
+        report.setEventNo(event.getEventNo());
+        report.setSource(event.getSource());
+        report.setLocation(event.getLocation());
+        report.setHappenTime(event.getHappenTime());
+        report.setDescription(event.getDescription());
+        report.setReporter(event.getReporter());
+        report.setReportTime(event.getReportTime());
+        report.setChannel(event.getChannel());
+        report.setStatus(STATUS_WAIT_ACCEPT);
+        report.setCreateTime(now);
+        reportMapper.insert(report);
+        return event;
+    }
+
+    @Override
+    public Page<DisEventVO> findByPage(DisEventPageDTO dto) {
+        Page<DisEvent> page = new Page<>(dto.getPageNum(), dto.getPageSize());
+        LambdaQueryWrapper<DisEvent> wrapper = buildWrapper(dto);
+        wrapper.orderByDesc(DisEvent::getHappenTime).orderByDesc(DisEvent::getCreateTime);
+        page(page, wrapper);
+
+        Page<DisEventVO> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+        List<DisEvent> records = page.getRecords();
+        if (CollectionUtils.isEmpty(records)) {
+            voPage.setRecords(Collections.emptyList());
+            return voPage;
+        }
+        List<Long> ids = records.stream().map(DisEvent::getId).collect(Collectors.toList());
+        Map<Long, DisEventTransfer> transferMap = latestTransferMap(ids);
+        Map<Long, DisEventTrack> trackMap = latestTrackMap(ids);
+        List<DisEventVO> vos = records.stream().map(e -> {
+            DisEventVO vo = new DisEventVO();
+            BeanUtils.copyProperties(e, vo);
+            DisEventTransfer t = transferMap.get(e.getId());
+            if (t != null) {
+                vo.setTransferDept(t.getTransferDept());
+                vo.setReportTargets(t.getReportTargets());
+                vo.setTransferTime(t.getTransferTime());
+                vo.setTransferId(t.getId());
+            }
+            DisEventTrack tk = trackMap.get(e.getId());
+            if (tk != null) {
+                vo.setLastVisitTime(tk.getLastVisitTime());
+                vo.setNextVisitTime(tk.getNextVisitTime());
+                vo.setTrackId(tk.getId());
+            }
+            return vo;
+        }).collect(Collectors.toList());
+        voPage.setRecords(vos);
+        return voPage;
+    }
+
+    @Override
+    public DisEventVO getDetail(Long id) {
+        DisEvent e = getById(id);
+        if (e == null) {
+            throw new ServiceException("事件不存在:" + id);
+        }
+        DisEventVO vo = new DisEventVO();
+        BeanUtils.copyProperties(e, vo);
+        Map<String, List<Map<String, Object>>> process = new HashMap<>();
+        process.put("transferList", listMap(transferMapper.selectList(
+                new LambdaQueryWrapper<DisEventTransfer>()
+                        .eq(DisEventTransfer::getRelEventId, id)
+                        .orderByDesc(DisEventTransfer::getId))));
+        process.put("verifyList", listMap(verifyMapper.selectList(
+                new LambdaQueryWrapper<DisEventVerify>()
+                        .eq(DisEventVerify::getRelEventId, id)
+                        .orderByDesc(DisEventVerify::getId))));
+        process.put("trackList", listMap(trackMapper.selectList(
+                new LambdaQueryWrapper<DisEventTrack>()
+                        .eq(DisEventTrack::getRelEventId, id)
+                        .orderByDesc(DisEventTrack::getId))));
+        process.put("urgeList", listMap(urgeMapper.selectList(
+                new LambdaQueryWrapper<DisEventUrge>()
+                        .eq(DisEventUrge::getRelEventId, id)
+                        .orderByDesc(DisEventUrge::getId))));
+        vo.setProcess(process);
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void accept(Long id) {
+        DisEvent db = requireEvent(id);
+        if (!STATUS_WAIT_ACCEPT.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可受理");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        db.setStatus(STATUS_WAIT_VERIFY);
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+        // 同步接报明细状态
+        DisEventReport report = reportMapper.selectOne(
+                new LambdaQueryWrapper<DisEventReport>()
+                        .eq(DisEventReport::getRelEventId, id)
+                        .orderByDesc(DisEventReport::getId)
+                        .last("LIMIT 1"));
+        if (report != null) {
+            report.setStatus(STATUS_WAIT_VERIFY);
+            report.setUpdateTime(now);
+            reportMapper.updateById(report);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void verify(DisEvent event) {
+        DisEvent db = requireEvent(event.getId());
+        if (!STATUS_WAIT_VERIFY.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可核实");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        db.setCategory(event.getCategory());
+        db.setLevel(event.getLevel());
+        db.setJudgeTime(now);
+        db.setVerifier(currentUser());
+        db.setVerifyTime(now);
+        db.setVerifyOpinion(event.getVerifyOpinion());
+        // 不实事件为终止态
+        db.setStatus("不实".equals(event.getIsReal()) ? STATUS_FALSE : STATUS_VERIFIED);
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+        // 写核实明细(挂靠主表)
+        DisEventVerify v = new DisEventVerify();
+        v.setRelEventId(db.getId());
+        v.setReportId(null);
+        v.setEventNo(db.getEventNo());
+        v.setCategory(event.getCategory());
+        v.setLevel(event.getLevel());
+        v.setLocation(db.getLocation());
+        v.setDescription(db.getDescription());
+        v.setJudgeTime(now);
+        v.setVerifier(currentUser());
+        v.setVerifyTime(now);
+        v.setOpinion(event.getVerifyOpinion());
+        v.setIsReal(event.getIsReal());
+        v.setStatus(db.getStatus());
+        v.setCreateTime(now);
+        verifyMapper.insert(v);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void transfer(DisEventTransfer transfer) {
+        DisEvent db = requireEvent(transfer.getRelEventId());
+        if (!STATUS_VERIFIED.equals(db.getStatus()) && !STATUS_PROCESSING.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可转接");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        // 更新主表:责任单位 + 进入处置中
+        db.setDepartment(transfer.getTransferDept());
+        db.setStatus(STATUS_PROCESSING);
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+        // 写转接明细(挂靠主表)
+        DisEventTransfer t = new DisEventTransfer();
+        t.setRelEventId(db.getId());
+        t.setEventNo(db.getEventNo());
+        t.setCategory(db.getCategory());
+        t.setLevel(db.getLevel());
+        t.setSummary(db.getDescription());
+        t.setTransferDept(transfer.getTransferDept());
+        t.setTransferRemark(transfer.getTransferRemark());
+        t.setTransferTime(now);
+        t.setStatus("待报送");
+        t.setOperator(currentUser());
+        t.setCreateTime(now);
+        transferMapper.insert(t);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void report(DisEventTransfer transfer) {
+        DisEventTransfer t = transferMapper.selectById(transfer.getId());
+        if (t == null) {
+            throw new ServiceException("转接记录不存在:" + transfer.getId());
+        }
+        LocalDateTime now = LocalDateTime.now();
+        t.setReportTargets(transfer.getReportTargets());
+        t.setAttachPaths(transfer.getAttachPaths());
+        if (StringUtils.isBlank(t.getReportEventNo())) {
+            t.setReportEventNo("BS" + now.format(NO_FORMAT));
+        }
+        t.setStatus("已报送");
+        t.setUpdateTime(now);
+        transferMapper.updateById(t);
+        // 事件保持处置中
+        if (t.getRelEventId() != null) {
+            DisEvent db = getById(t.getRelEventId());
+            if (db != null && !STATUS_PROCESSING.equals(db.getStatus())) {
+                db.setStatus(STATUS_PROCESSING);
+                db.setUpdateTime(now);
+                db.setUpdateBy(currentUser());
+                updateById(db);
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void track(DisEventTrack track) {
+        DisEvent db = requireEvent(track.getRelEventId());
+        if (!STATUS_PROCESSING.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可跟踪");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        // 写跟踪单(挂靠主表)
+        DisEventTrack tk = new DisEventTrack();
+        tk.setRelEventId(db.getId());
+        tk.setEventNo(db.getEventNo());
+        tk.setCategory(db.getCategory());
+        tk.setLevel(db.getLevel());
+        tk.setLocation(db.getLocation());
+        tk.setDescription(db.getDescription());
+        tk.setHandler(StringUtils.isNotBlank(track.getHandler()) ? track.getHandler() : currentUser());
+        tk.setStatus(STATUS_PROCESSING);
+        tk.setLastVisitTime(now);
+        tk.setNextVisitTime(track.getNextVisitTime());
+        tk.setCreateTime(now);
+        trackMapper.insert(tk);
+        // 写跟踪日志(时间线展示)
+        DisEventTrackLog logRec = new DisEventTrackLog();
+        logRec.setTrackId(tk.getId());
+        logRec.setEventNo(db.getEventNo());
+        logRec.setTitle("跟踪记录");
+        logRec.setContent(track.getDescription());
+        logRec.setType("primary");
+        logRec.setAssessment(track.getAssessment());
+        logRec.setNeedUpgrade(track.getNeedUpgrade());
+        logRec.setActionTime(now);
+        logRec.setCreateTime(now);
+        trackLogMapper.insert(logRec);
+        // 更新主表处置负责人与回访时间
+        db.setHandler(tk.getHandler());
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void release(Long id) {
+        DisEvent db = requireEvent(id);
+        if (!STATUS_PROCESSING.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可解除");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        db.setStatus(STATUS_RELEASED);
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void close(DisEvent event) {
+        DisEvent db = requireEvent(event.getId());
+        if (!STATUS_RELEASED.equals(db.getStatus())) {
+            throw new ServiceException("当前状态【" + db.getStatus() + "】不可关闭");
+        }
+        LocalDateTime now = LocalDateTime.now();
+        db.setStatus(STATUS_CLOSED);
+        db.setCloseReason(event.getCloseReason());
+        db.setCloseRemark(event.getCloseRemark());
+        db.setCloseTime(now);
+        if (event.getLossAmount() != null) {
+            db.setLossAmount(event.getLossAmount());
+        }
+        db.setUpdateTime(now);
+        db.setUpdateBy(currentUser());
+        updateById(db);
+    }
+
+    @Override
+    public EventStatsVO stats(DisEventPageDTO dto) {
+        List<DisEvent> events = list(buildWrapper(dto));
+        EventStatsVO vo = new EventStatsVO();
+        vo.setTotalEvents((long) events.size());
+        BigDecimal totalLoss = events.stream()
+                .map(DisEvent::getLossAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        vo.setTotalLoss(totalLoss);
+        long lossCount = events.stream().filter(e -> e.getLossAmount() != null).count();
+        vo.setAvgLoss(lossCount == 0 ? BigDecimal.ZERO
+                : totalLoss.divide(BigDecimal.valueOf(lossCount), 2, RoundingMode.HALF_UP));
+        long majorEvents = events.stream()
+                .filter(e -> "重大".equals(e.getLevel()) || "特别重大".equals(e.getLevel()))
+                .count();
+        vo.setMajorEvents(majorEvents);
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateEvent(DisEvent event) {
+        DisEvent db = requireEvent(event.getId());
+        BeanUtils.copyProperties(event, db,
+                "id", "eventNo", "status", "reportTime", "createBy", "createTime", "updateTime", "delFlag");
+        db.setUpdateTime(LocalDateTime.now());
+        db.setUpdateBy(currentUser());
+        updateById(db);
+    }
+
+    /**
+     * 构建多维条件查询条件(列表与统计共用)
+     */
+    private LambdaQueryWrapper<DisEvent> buildWrapper(DisEventPageDTO dto) {
+        LambdaQueryWrapper<DisEvent> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.isNotBlank(dto.getEventNo()), DisEvent::getEventNo, dto.getEventNo());
+        wrapper.like(StringUtils.isNotBlank(dto.getEventName()), DisEvent::getEventName, dto.getEventName());
+        wrapper.eq(StringUtils.isNotBlank(dto.getEventType()), DisEvent::getEventType, dto.getEventType());
+        wrapper.eq(StringUtils.isNotBlank(dto.getCategory()), DisEvent::getCategory, dto.getCategory());
+        wrapper.eq(StringUtils.isNotBlank(dto.getLevel()), DisEvent::getLevel, dto.getLevel());
+        wrapper.eq(StringUtils.isNotBlank(dto.getSource()), DisEvent::getSource, dto.getSource());
+        wrapper.eq(StringUtils.isNotBlank(dto.getRegion()), DisEvent::getRegion, dto.getRegion());
+        wrapper.eq(StringUtils.isNotBlank(dto.getDepartment()), DisEvent::getDepartment, dto.getDepartment());
+        if (!CollectionUtils.isEmpty(dto.getStatusList())) {
+            wrapper.in(DisEvent::getStatus, dto.getStatusList());
+        } else {
+            wrapper.eq(StringUtils.isNotBlank(dto.getStatus()), DisEvent::getStatus, dto.getStatus());
+        }
+        if (StringUtils.isNotBlank(dto.getStartDate())) {
+            try {
+                wrapper.ge(DisEvent::getHappenTime, LocalDate.parse(dto.getStartDate()).atStartOfDay());
+            } catch (Exception e) {
+                log.warn("开始日期格式错误: {}", dto.getStartDate());
+            }
+        }
+        if (StringUtils.isNotBlank(dto.getEndDate())) {
+            try {
+                wrapper.lt(DisEvent::getHappenTime, LocalDate.parse(dto.getEndDate()).plusDays(1).atStartOfDay());
+            } catch (Exception e) {
+                log.warn("结束日期格式错误: {}", dto.getEndDate());
+            }
+        }
+        applyLossRange(wrapper, dto.getLossRange());
+        return wrapper;
+    }
+
+    /**
+     * 损失情况区间条件
+     */
+    private void applyLossRange(LambdaQueryWrapper<DisEvent> wrapper, String lossRange) {
+        if (StringUtils.isBlank(lossRange)) {
+            return;
+        }
+        switch (lossRange) {
+            case "100万以下":
+                wrapper.lt(DisEvent::getLossAmount, new BigDecimal(100));
+                break;
+            case "100-500万":
+                wrapper.ge(DisEvent::getLossAmount, new BigDecimal(100))
+                        .lt(DisEvent::getLossAmount, new BigDecimal(500));
+                break;
+            case "500-1000万":
+                wrapper.ge(DisEvent::getLossAmount, new BigDecimal(500))
+                        .lt(DisEvent::getLossAmount, new BigDecimal(1000));
+                break;
+            case "1000万以上":
+                wrapper.ge(DisEvent::getLossAmount, new BigDecimal(1000));
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * 查询每个事件最近一条转接记录
+     */
+    private Map<Long, DisEventTransfer> latestTransferMap(List<Long> ids) {
+        Map<Long, DisEventTransfer> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(ids)) {
+            return map;
+        }
+        List<DisEventTransfer> list = transferMapper.selectList(
+                new LambdaQueryWrapper<DisEventTransfer>()
+                        .in(DisEventTransfer::getRelEventId, ids)
+                        .orderByDesc(DisEventTransfer::getId));
+        for (DisEventTransfer t : list) {
+            map.putIfAbsent(t.getRelEventId(), t);
+        }
+        return map;
+    }
+
+    /**
+     * 查询每个事件最近一条跟踪记录
+     */
+    private Map<Long, DisEventTrack> latestTrackMap(List<Long> ids) {
+        Map<Long, DisEventTrack> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(ids)) {
+            return map;
+        }
+        List<DisEventTrack> list = trackMapper.selectList(
+                new LambdaQueryWrapper<DisEventTrack>()
+                        .in(DisEventTrack::getRelEventId, ids)
+                        .orderByDesc(DisEventTrack::getId));
+        for (DisEventTrack tk : list) {
+            map.putIfAbsent(tk.getRelEventId(), tk);
+        }
+        return map;
+    }
+
+    /**
+     * 实体列表转 Map 列表(环节明细序列化)
+     */
+    private List<Map<String, Object>> listMap(List<?> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        return list.stream().map(item -> objectMapper.convertValue(item, new TypeReference<Map<String, Object>>() {}))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 获取事件并校验存在
+     */
+    private DisEvent requireEvent(Long id) {
+        DisEvent db = getById(id);
+        if (db == null) {
+            throw new ServiceException("事件不存在:" + id);
+        }
+        return db;
+    }
+
+    /**
+     * 当前登录用户(未登录时返回系统)
+     */
+    private String currentUser() {
+        try {
+            return SecurityUtils.getUsername();
+        } catch (Exception e) {
+            return "system";
+        }
+    }
+}

+ 191 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/dispatch/service/impl/DisPlanLibraryServiceImpl.java

@@ -0,0 +1,191 @@
+package com.zksy.dispatch.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.dispatch.domain.DisPlanLibrary;
+import com.zksy.dispatch.domain.DisPlanMatchRecord;
+import com.zksy.dispatch.dto.PlanMatchDTO;
+import com.zksy.dispatch.dto.PlanMatchResultVO;
+import com.zksy.dispatch.mapper.DisPlanLibraryMapper;
+import com.zksy.dispatch.mapper.DisPlanMatchRecordMapper;
+import com.zksy.dispatch.service.DisPlanLibraryService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 数字预案库 Service 实现:突发事件条件设定 → 预案匹配
+ */
+@Slf4j
+@Service
+public class DisPlanLibraryServiceImpl extends ServiceImpl<DisPlanLibraryMapper, DisPlanLibrary>
+        implements DisPlanLibraryService {
+
+    /** 加权评分:事件类型40 / 事件级别25 / 发生区域20 / 责任单位15 */
+    private static final int SCORE_TYPE = 40;
+    private static final int SCORE_LEVEL = 25;
+    private static final int SCORE_REGION = 20;
+    private static final int SCORE_UNIT = 15;
+    /** 专题匹配基础匹配度 */
+    private static final int TOPIC_BASE_SCORE = 90;
+    /** 最近匹配记录保留上限(超出删除最旧) */
+    private static final int MAX_HISTORY = 20;
+
+    @Autowired
+    private DisPlanMatchRecordMapper matchRecordMapper;
+
+    @Override
+    public List<DisPlanLibrary> listAll() {
+        LambdaQueryWrapper<DisPlanLibrary> wrapper = new LambdaQueryWrapper<>();
+        wrapper.orderByAsc(DisPlanLibrary::getEventType);
+        wrapper.orderByDesc(DisPlanLibrary::getId);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public List<PlanMatchResultVO> match(PlanMatchDTO dto, String operUser) {
+        List<DisPlanLibrary> plans = this.list();
+        List<PlanMatchResultVO> results = plans.stream().map(plan -> {
+            PlanMatchResultVO vo = toVO(plan);
+            int score = 0;
+            if (StringUtils.isNotEmpty(dto.getEventType()) && dto.getEventType().equals(plan.getEventType())) {
+                score += SCORE_TYPE;
+            }
+            if (StringUtils.isNotEmpty(dto.getEventLevel()) && dto.getEventLevel().equals(plan.getEventLevel())) {
+                score += SCORE_LEVEL;
+            }
+            if (StringUtils.isNotEmpty(dto.getEventRegion()) && dto.getEventRegion().equals(plan.getEventRegion())) {
+                score += SCORE_REGION;
+            }
+            if (StringUtils.isNotEmpty(dto.getResponsibleUnit()) && dto.getResponsibleUnit().equals(plan.getResponsibleUnit())) {
+                score += SCORE_UNIT;
+            }
+            vo.setMatchScore(score);
+            return vo;
+        }).filter(vo -> vo.getMatchScore() > 0)
+                .sorted(Comparator.comparing(PlanMatchResultVO::getMatchScore).reversed())
+                .collect(Collectors.toList());
+
+        if (!results.isEmpty()) {
+            PlanMatchResultVO top = results.get(0);
+            String desc = "手动匹配 " + StringUtils.defaultString(dto.getEventType(), "-")
+                    + "/" + StringUtils.defaultString(dto.getEventRegion(), "-")
+                    + "/" + StringUtils.defaultString(dto.getEventLevel(), "-");
+            saveRecord(dto, "manual", top.getId(), top.getPlanName(), top.getMatchScore(), desc, operUser);
+        }
+        return results;
+    }
+
+    @Override
+    public List<PlanMatchResultVO> matchByTopic(String topic, String operUser) {
+        LambdaQueryWrapper<DisPlanLibrary> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StringUtils.isNotEmpty(topic), DisPlanLibrary::getTopic, topic);
+        List<PlanMatchResultVO> results = this.list(wrapper).stream().map(plan -> {
+            PlanMatchResultVO vo = toVO(plan);
+            vo.setMatchScore(TOPIC_BASE_SCORE);
+            return vo;
+        }).collect(Collectors.toList());
+
+        return results;
+    }
+
+    @Override
+    public List<DisPlanMatchRecord> getHistory(int limit) {
+        LambdaQueryWrapper<DisPlanMatchRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.orderByDesc(DisPlanMatchRecord::getCreateTime);
+        wrapper.last("LIMIT " + limit);
+        return matchRecordMapper.selectList(wrapper);
+    }
+
+    private void saveRecord(PlanMatchDTO dto, String matchType, Long planId, String planName,
+                            Integer matchScore, String recordDesc, String operUser) {
+        try {
+            // 相同匹配条件不重复生成记录,仅更新预案信息与时间
+            DisPlanMatchRecord existing = findSameConditionRecord(dto);
+            if (existing != null) {
+                DisPlanMatchRecord update = new DisPlanMatchRecord();
+                update.setId(existing.getId());
+                update.setPlanId(planId);
+                update.setPlanName(planName);
+                update.setMatchScore(matchScore);
+                update.setRecordDesc(recordDesc);
+                update.setCreateBy(operUser);
+                update.setCreateTime(LocalDateTime.now());
+                matchRecordMapper.updateById(update);
+                log.info("预案匹配记录已更新: 条件={}/{}/{}/{}",
+                        dto.getEventType(), dto.getEventRegion(), dto.getEventLevel(), dto.getResponsibleUnit());
+                return;
+            }
+
+            DisPlanMatchRecord record = new DisPlanMatchRecord();
+            record.setEventType(dto.getEventType());
+            record.setEventLevel(dto.getEventLevel());
+            record.setEventRegion(dto.getEventRegion());
+            record.setResponsibleUnit(dto.getResponsibleUnit());
+            record.setMatchType(matchType);
+            record.setPlanId(planId);
+            record.setPlanName(planName);
+            record.setMatchScore(matchScore);
+            record.setRecordDesc(recordDesc);
+            record.setCreateBy(operUser);
+            record.setCreateTime(LocalDateTime.now());
+            matchRecordMapper.insert(record);
+            trimHistory();
+        } catch (Exception e) {
+            log.error("记录预案匹配历史失败", e);
+        }
+    }
+
+    /**
+     * 查询相同匹配条件的已有记录(四个条件全部一致),不存在返回null
+     */
+    private DisPlanMatchRecord findSameConditionRecord(PlanMatchDTO dto) {
+        LambdaQueryWrapper<DisPlanMatchRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(DisPlanMatchRecord::getMatchType, "manual");
+        wrapper.eq(StringUtils.isNotEmpty(dto.getEventType()), DisPlanMatchRecord::getEventType, dto.getEventType());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getEventLevel()), DisPlanMatchRecord::getEventLevel, dto.getEventLevel());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getEventRegion()), DisPlanMatchRecord::getEventRegion, dto.getEventRegion());
+        wrapper.eq(StringUtils.isNotEmpty(dto.getResponsibleUnit()), DisPlanMatchRecord::getResponsibleUnit, dto.getResponsibleUnit());
+        wrapper.last("LIMIT 1");
+        return matchRecordMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 控制最近匹配记录总量,超出上限删除最旧记录
+     */
+    private void trimHistory() {
+        try {
+            List<DisPlanMatchRecord> all = matchRecordMapper.selectList(
+                    new LambdaQueryWrapper<DisPlanMatchRecord>()
+                            .orderByDesc(DisPlanMatchRecord::getCreateTime));
+            if (all.size() > MAX_HISTORY) {
+                for (int i = MAX_HISTORY; i < all.size(); i++) {
+                    matchRecordMapper.deleteById(all.get(i).getId());
+                }
+                log.info("最近匹配记录超上限,已清理 {} 条", all.size() - MAX_HISTORY);
+            }
+        } catch (Exception e) {
+            log.error("清理最近匹配记录失败", e);
+        }
+    }
+
+    private PlanMatchResultVO toVO(DisPlanLibrary plan) {
+        PlanMatchResultVO vo = new PlanMatchResultVO();
+        vo.setId(plan.getId());
+        vo.setPlanName(plan.getPlanName());
+        vo.setEventType(plan.getEventType());
+        vo.setEventLevel(plan.getEventLevel());
+        vo.setEventRegion(plan.getEventRegion());
+        vo.setResponsibleUnit(plan.getResponsibleUnit());
+        vo.setTopic(plan.getTopic());
+        vo.setPlanSummary(plan.getPlanSummary());
+        vo.setPlanContent(plan.getPlanContent());
+        return vo;
+    }
+}