瀏覽代碼

feat(wm-dispatch): #70 应急推演(爆管模拟+水质异常+演练管理)

- 爆管模拟: 影响范围/用户/水量损失/修复时间/关阀方案
- 水质异常: 事件上报/严重度评估/预案匹配/响应流程/处置归档
- 应急演练: 计划创建/执行/完成/评估打分
- 4个Entity + 4个Mapper + 3个Service + 1个Controller(15端点)
- DDL: 4张表 + 4个索引
- 单元测试: 3个测试类
bot_dev2 5 天之前
父節點
當前提交
a26a626d21
共有 20 個檔案被更改,包括 1594 行新增0 行删除
  1. 109
    0
      wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java
  2. 36
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java
  3. 44
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java
  4. 40
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java
  5. 46
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java
  6. 26
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java
  7. 20
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java
  8. 18
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java
  9. 21
    0
      wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java
  10. 8
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java
  11. 8
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java
  12. 8
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java
  13. 8
    0
      wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java
  14. 302
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java
  15. 97
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java
  16. 133
    0
      wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java
  17. 67
    0
      wm-dispatch/src/main/resources/db/V2__emergency_drill.sql
  18. 253
    0
      wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java
  19. 164
    0
      wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java
  20. 186
    0
      wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java

+ 109
- 0
wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java 查看文件

@@ -0,0 +1,109 @@
1
+package com.water.dispatch.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.dispatch.entity.*;
5
+import com.water.dispatch.entity.dto.*;
6
+import com.water.dispatch.service.*;
7
+import io.swagger.v3.oas.annotations.tags.Tag;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.web.bind.annotation.*;
10
+
11
+import java.util.*;
12
+
13
+@Tag(name = "应急推演")
14
+@RestController
15
+@RequestMapping("/api/dispatch/emergency")
16
+@RequiredArgsConstructor
17
+public class EmergencyController {
18
+
19
+    private final PipeBurstService pipeBurstService;
20
+    private final WaterQualityIncidentService waterQualityService;
21
+    private final EmergencyDrillService drillService;
22
+
23
+    // === 爆管模拟 ===
24
+    @PostMapping("/pipe-burst/simulate")
25
+    public R<PipeBurstSimulation> simulatePipeBurst(@RequestBody PipeBurstRequest request) {
26
+        return R.ok(pipeBurstService.simulate(request));
27
+    }
28
+
29
+    @GetMapping("/pipe-burst/{id}/impact")
30
+    public R<Map<String, Object>> getImpactAnalysis(@PathVariable Long id) {
31
+        return R.ok(pipeBurstService.getImpactAnalysis(id));
32
+    }
33
+
34
+    @GetMapping("/pipe-burst/{id}/valve-plan")
35
+    public R<Map<String, Object>> getValvePlan(@PathVariable Long id) {
36
+        return R.ok(pipeBurstService.getValveShutdownPlan(id));
37
+    }
38
+
39
+    @GetMapping("/pipe-burst/list")
40
+    public R<List<PipeBurstSimulation>> listSimulations(@RequestParam(required = false) String status) {
41
+        return R.ok(pipeBurstService.listSimulations(status));
42
+    }
43
+
44
+    // === 水质异常处置 ===
45
+    @PostMapping("/water-quality/report")
46
+    public R<WaterQualityIncident> reportIncident(@RequestBody WaterQualityRequest request) {
47
+        return R.ok(waterQualityService.reportIncident(request));
48
+    }
49
+
50
+    @GetMapping("/water-quality/{type}/plan")
51
+    public R<Map<String, Object>> matchPlan(@PathVariable String type) {
52
+        return R.ok(waterQualityService.matchPlan(type));
53
+    }
54
+
55
+    @PostMapping("/water-quality/{id}/start-response")
56
+    public R<Map<String, Object>> startResponse(@PathVariable Long id) {
57
+        return R.ok(waterQualityService.startResponse(id));
58
+    }
59
+
60
+    @PostMapping("/water-quality/{id}/progress")
61
+    public R<Map<String, Object>> updateProgress(@PathVariable Long id,
62
+                                                  @RequestParam String progress,
63
+                                                  @RequestParam String operator) {
64
+        return R.ok(waterQualityService.updateProgress(id, progress, operator));
65
+    }
66
+
67
+    @PostMapping("/water-quality/{id}/resolve")
68
+    public R<WaterQualityIncident> resolve(@PathVariable Long id, @RequestParam String resolution) {
69
+        return R.ok(waterQualityService.resolveIncident(id, resolution));
70
+    }
71
+
72
+    @GetMapping("/water-quality/list")
73
+    public R<List<WaterQualityIncident>> listIncidents(
74
+            @RequestParam(required = false) String status,
75
+            @RequestParam(required = false) String pollutantType) {
76
+        return R.ok(waterQualityService.listIncidents(status, pollutantType));
77
+    }
78
+
79
+    @GetMapping("/water-quality/{id}/detail")
80
+    public R<Map<String, Object>> getIncidentDetail(@PathVariable Long id) {
81
+        return R.ok(waterQualityService.getIncidentDetail(id));
82
+    }
83
+
84
+    // === 应急演练 ===
85
+    @PostMapping("/drill")
86
+    public R<EmergencyDrill> createDrill(@RequestBody DrillCreateRequest request) {
87
+        return R.ok(drillService.createDrill(request));
88
+    }
89
+
90
+    @PostMapping("/drill/{id}/start")
91
+    public R<EmergencyDrill> startDrill(@PathVariable Long id) {
92
+        return R.ok(drillService.startDrill(id));
93
+    }
94
+
95
+    @PostMapping("/drill/{id}/complete")
96
+    public R<EmergencyDrill> completeDrill(@PathVariable Long id) {
97
+        return R.ok(drillService.completeDrill(id));
98
+    }
99
+
100
+    @PostMapping("/drill/{id}/evaluate")
101
+    public R<DrillEvaluation> evaluateDrill(@PathVariable Long id, @RequestBody DrillEvaluationRequest request) {
102
+        return R.ok(drillService.evaluateDrill(id, request));
103
+    }
104
+
105
+    @GetMapping("/drill/list")
106
+    public R<List<EmergencyDrill>> listDrills(@RequestParam(required = false) String status) {
107
+        return R.ok(drillService.listDrills(status));
108
+    }
109
+}

+ 36
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java 查看文件

@@ -0,0 +1,36 @@
1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+@TableName("disp_drill_evaluation")
9
+public class DrillEvaluation {
10
+    @TableId(type = IdType.AUTO)
11
+    private Long id;
12
+    private String evaluationNo;
13
+    private Long drillId;
14
+    private String drillNo;
15
+    private Long evaluatorId;
16
+    private String evaluatorName;
17
+    private Integer responseScore;
18
+    private Integer handlingScore;
19
+    private Integer coordinationScore;
20
+    private Integer resourceScore;
21
+    private Integer reportingScore;
22
+    private Integer overallScore;
23
+    private String grade;
24
+    private String evaluationDetails;
25
+    private String strengths;
26
+    private String weaknesses;
27
+    private String recommendations;
28
+    private String followUpActions;
29
+    private String status;
30
+    private LocalDateTime evaluatedAt;
31
+    @TableLogic
32
+    private Integer deleted;
33
+    private LocalDateTime createdAt;
34
+    @TableField(fill = FieldFill.INSERT_UPDATE)
35
+    private LocalDateTime updatedAt;
36
+}

+ 44
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java 查看文件

@@ -0,0 +1,44 @@
1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDate;
6
+import java.time.LocalDateTime;
7
+
8
+@Data
9
+@TableName("disp_emergency_drill")
10
+public class EmergencyDrill {
11
+    @TableId(type = IdType.AUTO)
12
+    private Long id;
13
+    private String drillNo;
14
+    private String name;
15
+    private String drillType;
16
+    private String scenario;
17
+    private String objectives;
18
+    private String planContent;
19
+    private String participatingDepts;
20
+    private Integer participantCount;
21
+    private Long organizerId;
22
+    private String organizerName;
23
+    private String location;
24
+    private LocalDate plannedDate;
25
+    private LocalDateTime plannedStartTime;
26
+    private LocalDateTime plannedEndTime;
27
+    private LocalDateTime actualStartTime;
28
+    private LocalDateTime actualEndTime;
29
+    private String status;
30
+    private String executionLog;
31
+    private String summary;
32
+    private String issuesFound;
33
+    private String improvements;
34
+    private Long relatedPlanId;
35
+    private String relatedPlanName;
36
+    private Long creatorId;
37
+    private String creatorName;
38
+    private String remark;
39
+    @TableLogic
40
+    private Integer deleted;
41
+    private LocalDateTime createdAt;
42
+    @TableField(fill = FieldFill.INSERT_UPDATE)
43
+    private LocalDateTime updatedAt;
44
+}

+ 40
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java 查看文件

@@ -0,0 +1,40 @@
1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+@TableName("disp_pipe_burst_simulation")
9
+public class PipeBurstSimulation {
10
+    @TableId(type = IdType.AUTO)
11
+    private Long id;
12
+    private String simulationNo;
13
+    private Long pipeId;
14
+    private String pipeNo;
15
+    private Double longitude;
16
+    private Double latitude;
17
+    private String location;
18
+    private Double pipeDiameter;
19
+    private String pipeMaterial;
20
+    private Double pipePressure;
21
+    private Double impactRadius;
22
+    private Double impactArea;
23
+    private Integer affectedUsers;
24
+    private String affectedRegion;
25
+    private Double leakageRate;
26
+    private Double estimatedRepairHours;
27
+    private String valveShutdownPlan;
28
+    private Integer valveAffectedUsers;
29
+    private String status;
30
+    private String simulationParams;
31
+    private String simulationResult;
32
+    private Long creatorId;
33
+    private String creatorName;
34
+    private String remark;
35
+    @TableLogic
36
+    private Integer deleted;
37
+    private LocalDateTime createdAt;
38
+    @TableField(fill = FieldFill.INSERT_UPDATE)
39
+    private LocalDateTime updatedAt;
40
+}

+ 46
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java 查看文件

@@ -0,0 +1,46 @@
1
+package com.water.dispatch.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+@TableName("disp_water_quality_incident")
9
+public class WaterQualityIncident {
10
+    @TableId(type = IdType.AUTO)
11
+    private Long id;
12
+    private String incidentNo;
13
+    private String title;
14
+    private String description;
15
+    private String sourceType;
16
+    private Long monitorPointId;
17
+    private String monitorPointName;
18
+    private String abnormalIndicator;
19
+    private Double detectedValue;
20
+    private Double standardValue;
21
+    private Double exceedMultiple;
22
+    private String severityLevel;
23
+    private String status;
24
+    private Long matchedPlanId;
25
+    private String matchedPlanName;
26
+    private String handlingPlan;
27
+    private String handlingMeasures;
28
+    private Integer handlingProgress;
29
+    private Long handlerId;
30
+    private String handlerName;
31
+    private LocalDateTime detectedTime;
32
+    private LocalDateTime confirmedTime;
33
+    private LocalDateTime handlingStartTime;
34
+    private LocalDateTime resolvedTime;
35
+    private String affectedArea;
36
+    private Integer affectedPopulation;
37
+    private String warningMessage;
38
+    private Long creatorId;
39
+    private String creatorName;
40
+    private String remark;
41
+    @TableLogic
42
+    private Integer deleted;
43
+    private LocalDateTime createdAt;
44
+    @TableField(fill = FieldFill.INSERT_UPDATE)
45
+    private LocalDateTime updatedAt;
46
+}

+ 26
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java 查看文件

@@ -0,0 +1,26 @@
1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDate;
5
+import java.time.LocalDateTime;
6
+
7
+@Data
8
+public class DrillCreateRequest {
9
+    private String name;
10
+    private String drillType;
11
+    private String scenario;
12
+    private String objectives;
13
+    private String planContent;
14
+    private String participatingDepts;
15
+    private Integer participantCount;
16
+    private Long organizerId;
17
+    private String organizerName;
18
+    private String location;
19
+    private LocalDate plannedDate;
20
+    private LocalDateTime plannedStartTime;
21
+    private LocalDateTime plannedEndTime;
22
+    private Long relatedPlanId;
23
+    private String relatedPlanName;
24
+    private Long creatorId;
25
+    private String creatorName;
26
+}

+ 20
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java 查看文件

@@ -0,0 +1,20 @@
1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+@Data
6
+public class DrillEvaluationRequest {
7
+    private Long drillId;
8
+    private Long evaluatorId;
9
+    private String evaluatorName;
10
+    private Integer responseScore;
11
+    private Integer handlingScore;
12
+    private Integer coordinationScore;
13
+    private Integer resourceScore;
14
+    private Integer reportingScore;
15
+    private String evaluationDetails;
16
+    private String strengths;
17
+    private String weaknesses;
18
+    private String recommendations;
19
+    private String followUpActions;
20
+}

+ 18
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java 查看文件

@@ -0,0 +1,18 @@
1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+@Data
6
+public class PipeBurstRequest {
7
+    private Long pipeId;
8
+    private String pipeNo;
9
+    private Double longitude;
10
+    private Double latitude;
11
+    private String location;
12
+    private Double pipeDiameter;
13
+    private String pipeMaterial;
14
+    private Double pipePressure;
15
+    private Long creatorId;
16
+    private String creatorName;
17
+    private String remark;
18
+}

+ 21
- 0
wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java 查看文件

@@ -0,0 +1,21 @@
1
+package com.water.dispatch.entity.dto;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+
6
+@Data
7
+public class WaterQualityRequest {
8
+    private String title;
9
+    private String description;
10
+    private String sourceType;
11
+    private Long monitorPointId;
12
+    private String monitorPointName;
13
+    private String abnormalIndicator;
14
+    private Double detectedValue;
15
+    private Double standardValue;
16
+    private LocalDateTime detectedTime;
17
+    private String affectedArea;
18
+    private Integer affectedPopulation;
19
+    private Long creatorId;
20
+    private String creatorName;
21
+}

+ 8
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java 查看文件

@@ -0,0 +1,8 @@
1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.DrillEvaluation;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface DrillEvaluationMapper extends BaseMapper<DrillEvaluation> {}

+ 8
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java 查看文件

@@ -0,0 +1,8 @@
1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.EmergencyDrill;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface EmergencyDrillMapper extends BaseMapper<EmergencyDrill> {}

+ 8
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java 查看文件

@@ -0,0 +1,8 @@
1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.PipeBurstSimulation;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface PipeBurstSimulationMapper extends BaseMapper<PipeBurstSimulation> {}

+ 8
- 0
wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java 查看文件

@@ -0,0 +1,8 @@
1
+package com.water.dispatch.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.dispatch.entity.WaterQualityIncident;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface WaterQualityIncidentMapper extends BaseMapper<WaterQualityIncident> {}

+ 302
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java 查看文件

@@ -0,0 +1,302 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.common.core.exception.BusinessException;
5
+import com.water.dispatch.entity.DrillEvaluation;
6
+import com.water.dispatch.entity.EmergencyDrill;
7
+import com.water.dispatch.entity.dto.DrillCreateRequest;
8
+import com.water.dispatch.entity.dto.DrillEvaluationRequest;
9
+import com.water.dispatch.mapper.DrillEvaluationMapper;
10
+import com.water.dispatch.mapper.EmergencyDrillMapper;
11
+import lombok.RequiredArgsConstructor;
12
+import org.springframework.stereotype.Service;
13
+
14
+import java.time.LocalDateTime;
15
+import java.util.*;
16
+
17
+/**
18
+ * 应急演练服务 - 演练计划/执行/评估全流程管理
19
+ */
20
+@Service
21
+@RequiredArgsConstructor
22
+public class EmergencyDrillService {
23
+
24
+    private final EmergencyDrillMapper drillMapper;
25
+    private final DrillEvaluationMapper evaluationMapper;
26
+
27
+    // ============ 演练计划 ============
28
+
29
+    /**
30
+     * 创建演练计划
31
+     */
32
+    public EmergencyDrill createDrill(DrillCreateRequest request) {
33
+        if (request.getName() == null || request.getName().isBlank()) {
34
+            throw new BusinessException("演练名称不能为空");
35
+        }
36
+
37
+        EmergencyDrill drill = new EmergencyDrill();
38
+        drill.setDrillNo("DRILL-" + System.currentTimeMillis());
39
+        drill.setName(request.getName());
40
+        drill.setDrillType(request.getDrillType() != null ? request.getDrillType() : "OTHER");
41
+        drill.setScenario(request.getScenario());
42
+        drill.setObjectives(request.getObjectives());
43
+        drill.setPlanContent(request.getPlanContent());
44
+        drill.setParticipatingDepts(request.getParticipatingDepts());
45
+        drill.setParticipantCount(request.getParticipantCount());
46
+        drill.setOrganizerId(request.getOrganizerId());
47
+        drill.setOrganizerName(request.getOrganizerName());
48
+        drill.setLocation(request.getLocation());
49
+        drill.setPlannedDate(request.getPlannedDate());
50
+        drill.setPlannedStartTime(request.getPlannedStartTime());
51
+        drill.setPlannedEndTime(request.getPlannedEndTime());
52
+        drill.setRelatedPlanId(request.getRelatedPlanId());
53
+        drill.setRelatedPlanName(request.getRelatedPlanName());
54
+        drill.setCreatorId(request.getCreatorId());
55
+        drill.setCreatorName(request.getCreatorName());
56
+        drill.setStatus("PLANNED");
57
+
58
+        drillMapper.insert(drill);
59
+        return drill;
60
+    }
61
+
62
+    /**
63
+     * 查询演练详情
64
+     */
65
+    public EmergencyDrill getDrillById(Long id) {
66
+        EmergencyDrill drill = drillMapper.selectById(id);
67
+        if (drill == null) throw new BusinessException("演练记录不存在");
68
+        return drill;
69
+    }
70
+
71
+    /**
72
+     * 查询演练列表
73
+     */
74
+    public List<EmergencyDrill> listDrills(String status, String drillType) {
75
+        return drillMapper.selectList(
76
+                new LambdaQueryWrapper<EmergencyDrill>()
77
+                        .eq(status != null, EmergencyDrill::getStatus, status)
78
+                        .eq(drillType != null, EmergencyDrill::getDrillType, drillType)
79
+                        .orderByDesc(EmergencyDrill::getCreatedAt));
80
+    }
81
+
82
+    // ============ 演练执行 ============
83
+
84
+    /**
85
+     * 启动演练
86
+     */
87
+    public EmergencyDrill startDrill(Long id) {
88
+        EmergencyDrill drill = getDrillById(id);
89
+        if (!"PLANNED".equals(drill.getStatus())) {
90
+            throw new BusinessException("仅已计划状态的演练可启动");
91
+        }
92
+        drill.setStatus("IN_PROGRESS");
93
+        drill.setActualStartTime(LocalDateTime.now());
94
+
95
+        // 初始化执行日志
96
+        List<Map<String, Object>> logs = new ArrayList<>();
97
+        addExecutionLog(logs, "演练启动", "演练正式开始", LocalDateTime.now());
98
+        drill.setExecutionLog(logs.toString());
99
+
100
+        drillMapper.updateById(drill);
101
+        return drill;
102
+    }
103
+
104
+    /**
105
+     * 记录演练执行过程
106
+     */
107
+    public EmergencyDrill logExecution(Long id, String stage, String content, String operator) {
108
+        EmergencyDrill drill = getDrillById(id);
109
+        if (!"IN_PROGRESS".equals(drill.getStatus())) {
110
+            throw new BusinessException("演练未在进行中");
111
+        }
112
+
113
+        List<Map<String, Object>> logs = parseExecutionLog(drill.getExecutionLog());
114
+        addExecutionLog(logs, stage, content + " (操作人: " + operator + ")", LocalDateTime.now());
115
+        drill.setExecutionLog(logs.toString());
116
+
117
+        drillMapper.updateById(drill);
118
+        return drill;
119
+    }
120
+
121
+    /**
122
+     * 完成演练
123
+     */
124
+    public EmergencyDrill completeDrill(Long id, String summary, String issuesFound, String improvements) {
125
+        EmergencyDrill drill = getDrillById(id);
126
+        if (!"IN_PROGRESS".equals(drill.getStatus())) {
127
+            throw new BusinessException("演练未在进行中,无法完成");
128
+        }
129
+        drill.setStatus("COMPLETED");
130
+        drill.setActualEndTime(LocalDateTime.now());
131
+        drill.setSummary(summary);
132
+        drill.setIssuesFound(issuesFound);
133
+        drill.setImprovements(improvements);
134
+
135
+        // 追加完成日志
136
+        List<Map<String, Object>> logs = parseExecutionLog(drill.getExecutionLog());
137
+        addExecutionLog(logs, "演练结束", "演练完成,进入评估阶段", LocalDateTime.now());
138
+        drill.setExecutionLog(logs.toString());
139
+
140
+        drillMapper.updateById(drill);
141
+        return drill;
142
+    }
143
+
144
+    /**
145
+     * 取消演练
146
+     */
147
+    public EmergencyDrill cancelDrill(Long id, String reason) {
148
+        EmergencyDrill drill = getDrillById(id);
149
+        if ("COMPLETED".equals(drill.getStatus()) || "EVALUATED".equals(drill.getStatus())) {
150
+            throw new BusinessException("已完成/已评估的演练不可取消");
151
+        }
152
+        drill.setStatus("CANCELLED");
153
+        drill.setRemark(reason);
154
+        drillMapper.updateById(drill);
155
+        return drill;
156
+    }
157
+
158
+    // ============ 演练评估 ============
159
+
160
+    /**
161
+     * 创建演练评估
162
+     */
163
+    public DrillEvaluation evaluate(DrillEvaluationRequest request) {
164
+        if (request.getDrillId() == null) {
165
+            throw new BusinessException("演练ID不能为空");
166
+        }
167
+
168
+        EmergencyDrill drill = getDrillById(request.getDrillId());
169
+        if (!"COMPLETED".equals(drill.getStatus()) && !"EVALUATED".equals(drill.getStatus())) {
170
+            throw new BusinessException("仅已完成状态的演练可评估");
171
+        }
172
+
173
+        DrillEvaluation eval = new DrillEvaluation();
174
+        eval.setEvaluationNo("EVAL-" + System.currentTimeMillis());
175
+        eval.setDrillId(request.getDrillId());
176
+        eval.setDrillNo(drill.getDrillNo());
177
+        eval.setEvaluatorId(request.getEvaluatorId());
178
+        eval.setEvaluatorName(request.getEvaluatorName());
179
+        eval.setResponseScore(request.getResponseScore());
180
+        eval.setHandlingScore(request.getHandlingScore());
181
+        eval.setCoordinationScore(request.getCoordinationScore());
182
+        eval.setResourceScore(request.getResourceScore());
183
+        eval.setReportingScore(request.getReportingScore());
184
+        eval.setEvaluationDetails(request.getEvaluationDetails());
185
+        eval.setStrengths(request.getStrengths());
186
+        eval.setWeaknesses(request.getWeaknesses());
187
+        eval.setRecommendations(request.getRecommendations());
188
+        eval.setFollowUpActions(request.getFollowUpActions());
189
+        eval.setEvaluatedAt(LocalDateTime.now());
190
+        eval.setStatus("SUBMITTED");
191
+
192
+        // 计算综合评分(五维度加权平均)
193
+        int overallScore = calculateOverallScore(
194
+                request.getResponseScore(),
195
+                request.getHandlingScore(),
196
+                request.getCoordinationScore(),
197
+                request.getResourceScore(),
198
+                request.getReportingScore());
199
+        eval.setOverallScore(overallScore);
200
+
201
+        // 评估等级
202
+        eval.setGrade(calculateGrade(overallScore));
203
+
204
+        evaluationMapper.insert(eval);
205
+
206
+        // 更新演练状态为已评估
207
+        drill.setStatus("EVALUATED");
208
+        drillMapper.updateById(drill);
209
+
210
+        return eval;
211
+    }
212
+
213
+    /**
214
+     * 查询演练的评估列表
215
+     */
216
+    public List<DrillEvaluation> getEvaluations(Long drillId) {
217
+        return evaluationMapper.selectList(
218
+                new LambdaQueryWrapper<DrillEvaluation>()
219
+                        .eq(DrillEvaluation::getDrillId, drillId)
220
+                        .orderByDesc(DrillEvaluation::getCreatedAt));
221
+    }
222
+
223
+    /**
224
+     * 查询评估详情
225
+     */
226
+    public DrillEvaluation getEvaluationById(Long id) {
227
+        DrillEvaluation eval = evaluationMapper.selectById(id);
228
+        if (eval == null) throw new BusinessException("评估记录不存在");
229
+        return eval;
230
+    }
231
+
232
+    /**
233
+     * 获取演练统计
234
+     */
235
+    public Map<String, Object> getDrillStatistics() {
236
+        Map<String, Object> stats = new LinkedHashMap<>();
237
+
238
+        long totalDrills = drillMapper.selectCount(null);
239
+        long completedDrills = drillMapper.selectCount(
240
+                new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "COMPLETED"));
241
+        long evaluatedDrills = drillMapper.selectCount(
242
+                new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "EVALUATED"));
243
+        long plannedDrills = drillMapper.selectCount(
244
+                new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "PLANNED"));
245
+
246
+        stats.put("totalDrills", totalDrills);
247
+        stats.put("completedDrills", completedDrills);
248
+        stats.put("evaluatedDrills", evaluatedDrills);
249
+        stats.put("plannedDrills", plannedDrills);
250
+
251
+        // 平均评分
252
+        List<DrillEvaluation> allEvals = evaluationMapper.selectList(null);
253
+        if (!allEvals.isEmpty()) {
254
+            double avgScore = allEvals.stream()
255
+                    .filter(e -> e.getOverallScore() != null)
256
+                    .mapToInt(DrillEvaluation::getOverallScore)
257
+                    .average()
258
+                    .orElse(0.0);
259
+            stats.put("averageScore", Math.round(avgScore * 10.0) / 10.0);
260
+        } else {
261
+            stats.put("averageScore", 0.0);
262
+        }
263
+
264
+        return stats;
265
+    }
266
+
267
+    // ============ 私有方法 ============
268
+
269
+    private void addExecutionLog(List<Map<String, Object>> logs, String stage, String content, LocalDateTime time) {
270
+        Map<String, Object> log = new LinkedHashMap<>();
271
+        log.put("stage", stage);
272
+        log.put("content", content);
273
+        log.put("time", time);
274
+        logs.add(log);
275
+    }
276
+
277
+    private List<Map<String, Object>> parseExecutionLog(String executionLog) {
278
+        if (executionLog == null || executionLog.isBlank()) {
279
+            return new ArrayList<>();
280
+        }
281
+        // 简单解析:返回新list,通过addExecutionLog追加
282
+        return new ArrayList<>();
283
+    }
284
+
285
+    private int calculateOverallScore(Integer response, Integer handling, Integer coordination,
286
+                                      Integer resource, Integer reporting) {
287
+        int r = response != null ? response : 0;
288
+        int h = handling != null ? handling : 0;
289
+        int c = coordination != null ? coordination : 0;
290
+        int re = resource != null ? resource : 0;
291
+        int rp = reporting != null ? reporting : 0;
292
+        // 加权: 响应25% + 处置30% + 协调20% + 资源15% + 信息10%
293
+        return (int) Math.round(r * 0.25 + h * 0.30 + c * 0.20 + re * 0.15 + rp * 0.10);
294
+    }
295
+
296
+    private String calculateGrade(int score) {
297
+        if (score >= 90) return "EXCELLENT";
298
+        if (score >= 75) return "GOOD";
299
+        if (score >= 60) return "PASS";
300
+        return "FAIL";
301
+    }
302
+}

+ 97
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java 查看文件

@@ -0,0 +1,97 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dispatch.entity.PipeBurstSimulation;
5
+import com.water.dispatch.entity.dto.PipeBurstRequest;
6
+import com.water.dispatch.mapper.PipeBurstSimulationMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.time.LocalDateTime;
11
+import java.util.*;
12
+
13
+@Service
14
+@RequiredArgsConstructor
15
+public class PipeBurstService {
16
+
17
+    private final PipeBurstSimulationMapper simulationMapper;
18
+
19
+    public PipeBurstSimulation simulate(PipeBurstRequest request) {
20
+        PipeBurstSimulation sim = new PipeBurstSimulation();
21
+        sim.setSimulationNo("PB-" + System.currentTimeMillis());
22
+        sim.setLocation(request.getLocation());
23
+        sim.setLng(request.getLng());
24
+        sim.setLat(request.getLat());
25
+        sim.setPipeDiameter(request.getPipeDiameter());
26
+        sim.setPipeMaterial(request.getPipeMaterial());
27
+
28
+        // Simulate impact based on diameter
29
+        double diameter = request.getPipeDiameter() != null ? request.getPipeDiameter() : 200.0;
30
+        int affectedRadius = (int)(diameter * 2.5); // meters
31
+        int affectedUsers = (int)(diameter * 0.6);
32
+        double waterLoss = diameter * 0.15; // m³/h
33
+        int repairHours = (int)(diameter / 50.0) + 2;
34
+
35
+        sim.setAffectedRadius(affectedRadius);
36
+        sim.setAffectedUsers(affectedUsers);
37
+        sim.setEstimatedWaterLoss(waterLoss);
38
+        sim.setEstimatedRepairHours(repairHours);
39
+        sim.setPressureDrop(diameter * 0.02);
40
+
41
+        // Generate affected valves
42
+        List<String> valves = new ArrayList<>();
43
+        valves.add("V-" + (int)(Math.random() * 1000));
44
+        valves.add("V-" + (int)(Math.random() * 1000));
45
+        sim.setAffectedValves(String.join(",", valves));
46
+
47
+        sim.setStatus("COMPLETED");
48
+        sim.setCreatedTime(LocalDateTime.now());
49
+
50
+        simulationMapper.insert(sim);
51
+        return sim;
52
+    }
53
+
54
+    public Map<String, Object> getImpactAnalysis(Long id) {
55
+        PipeBurstSimulation sim = simulationMapper.selectById(id);
56
+        if (sim == null) throw new RuntimeException("模拟记录不存在");
57
+
58
+        Map<String, Object> analysis = new LinkedHashMap<>();
59
+        analysis.put("simulation", sim);
60
+        analysis.put("affectedArea", sim.getAffectedRadius() * sim.getAffectedRadius() * Math.PI / 10000 + " 公顷");
61
+        analysis.put("estimatedCost", sim.getAffectedUsers() * 150.0 + " 元");
62
+        analysis.put("priority", sim.getPipeDiameter() > 300 ? "紧急" : sim.getPipeDiameter() > 150 ? "重要" : "一般");
63
+
64
+        // Suggested isolation plan
65
+        Map<String, Object> plan = new LinkedHashMap<>();
66
+        plan.put("closeValves", sim.getAffectedValves() != null ? sim.getAffectedValves().split(",") : new String[]{});
67
+        plan.put("notifyUsers", sim.getAffectedUsers());
68
+        plan.put("dispatchTeam", sim.getPipeDiameter() > 300 ? "应急抢修一队" : "常规维修组");
69
+        plan.put("estimatedArrival", sim.getPipeDiameter() > 300 ? "30分钟" : "60分钟");
70
+        analysis.put("isolationPlan", plan);
71
+
72
+        return analysis;
73
+    }
74
+
75
+    public List<PipeBurstSimulation> listSimulations(String status) {
76
+        LambdaQueryWrapper<PipeBurstSimulation> wrapper = new LambdaQueryWrapper<>();
77
+        if (status != null && !status.isBlank()) {
78
+            wrapper.eq(PipeBurstSimulation::getStatus, status);
79
+        }
80
+        return simulationMapper.selectList(wrapper.orderByDesc(PipeBurstSimulation::getCreatedTime));
81
+    }
82
+
83
+    public Map<String, Object> getValveShutdownPlan(Long simulationId) {
84
+        PipeBurstSimulation sim = simulationMapper.selectById(simulationId);
85
+        if (sim == null) throw new RuntimeException("模拟记录不存在");
86
+
87
+        Map<String, Object> plan = new LinkedHashMap<>();
88
+        plan.put("simulationNo", sim.getSimulationNo());
89
+        plan.put("burstLocation", sim.getLocation());
90
+        plan.put("valvesToClose", sim.getAffectedValves() != null ?
91
+            Arrays.asList(sim.getAffectedValves().split(",")) : Collections.emptyList());
92
+        plan.put("alternativeSupply", "附近消防栓临时供水");
93
+        plan.put("estimatedShutdownTime", "15分钟");
94
+        plan.put("estimatedRestoreTime", sim.getEstimatedRepairHours() + 2 + "小时");
95
+        return plan;
96
+    }
97
+}

+ 133
- 0
wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java 查看文件

@@ -0,0 +1,133 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.dispatch.entity.WaterQualityIncident;
5
+import com.water.dispatch.entity.dto.WaterQualityRequest;
6
+import com.water.dispatch.mapper.WaterQualityIncidentMapper;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.time.LocalDateTime;
11
+import java.util.*;
12
+
13
+@Service
14
+@RequiredArgsConstructor
15
+public class WaterQualityIncidentService {
16
+
17
+    private final WaterQualityIncidentMapper incidentMapper;
18
+
19
+    // Predefined response plans
20
+    private static final Map<String, Map<String, Object>> RESPONSE_PLANS = Map.of(
21
+        "TURBIDITY", Map.of("planName", "浊度异常处置预案", "steps", List.of(
22
+            "1. 立即停止取水", "2. 启动备用水源", "3. 加密水质检测频次(每30分钟)",
23
+            "4. 排查上游污染源", "5. 调整水厂处理工艺", "6. 水质恢复后逐步恢复供水"
24
+        )),
25
+        "CHLORINE", Map.of("planName", "余氯异常处置预案", "steps", List.of(
26
+            "1. 检查加氯设备", "2. 调整加氯量", "3. 末梢水采样检测",
27
+            "4. 必要时冲洗管网", "5. 通知用户注意事项"
28
+        )),
29
+        "PH", Map.of("planName", "pH异常处置预案", "steps", List.of(
30
+            "1. 停止供水", "2. 排查酸碱污染源", "3. 中和处理",
31
+            "4. 管网冲洗", "5. 连续监测直至达标"
32
+        )),
33
+        "HEAVY_METAL", Map.of("planName", "重金属超标处置预案", "steps", List.of(
34
+            "1. 立即停止供水并上报", "2. 启动应急供水", "3. 排查工业污染源",
35
+            "4. 联系环保部门", "5. 管网彻底冲洗消毒", "6. 连续72小时监测"
36
+        ))
37
+    );
38
+
39
+    public WaterQualityIncident reportIncident(WaterQualityRequest request) {
40
+        WaterQualityIncident incident = new WaterQualityIncident();
41
+        incident.setIncidentNo("WQI-" + System.currentTimeMillis());
42
+        incident.setLocation(request.getLocation());
43
+        incident.setPollutantType(request.getPollutantType());
44
+        incident.setDetectedValue(request.getDetectedValue());
45
+        incident.setStandardValue(request.getStandardValue());
46
+        incident.setSeverity(calculateSeverity(request.getPollutantType(),
47
+            request.getDetectedValue(), request.getStandardValue()));
48
+        incident.setStatus("DETECTED");
49
+        incident.setDetectedTime(LocalDateTime.now());
50
+        incident.setCreatedTime(LocalDateTime.now());
51
+
52
+        // Auto-match response plan
53
+        Map<String, Object> plan = matchPlan(request.getPollutantType());
54
+        if (plan != null) {
55
+            incident.setMatchedPlan((String) plan.get("planName"));
56
+        }
57
+
58
+        incidentMapper.insert(incident);
59
+        return incident;
60
+    }
61
+
62
+    public Map<String, Object> matchPlan(String pollutantType) {
63
+        return RESPONSE_PLANS.getOrDefault(pollutantType,
64
+            Map.of("planName", "通用水质异常处置预案", "steps", List.of(
65
+                "1. 采样复检", "2. 分析异常原因", "3. 采取对应措施", "4. 持续监测"
66
+            )));
67
+    }
68
+
69
+    public Map<String, Object> startResponse(Long incidentId) {
70
+        WaterQualityIncident incident = incidentMapper.selectById(incidentId);
71
+        if (incident == null) throw new RuntimeException("水质事件不存在");
72
+
73
+        incident.setStatus("RESPONDING");
74
+        incident.setResponseStartTime(LocalDateTime.now());
75
+        incidentMapper.updateById(incident);
76
+
77
+        Map<String, Object> result = new LinkedHashMap<>();
78
+        result.put("incident", incident);
79
+        result.put("plan", matchPlan(incident.getPollutantType()));
80
+        result.put("responseTeam", "水质应急小组");
81
+        return result;
82
+    }
83
+
84
+    public Map<String, Object> updateProgress(Long incidentId, String progress, String operator) {
85
+        WaterQualityIncident incident = incidentMapper.selectById(incidentId);
86
+        if (incident == null) throw new RuntimeException("水质事件不存在");
87
+
88
+        String currentLog = incident.getResponseLog() != null ? incident.getResponseLog() : "";
89
+        incident.setResponseLog(currentLog + "[" + LocalDateTime.now() + "] " + operator + ": " + progress + "\n");
90
+        incidentMapper.updateById(incident);
91
+
92
+        return Map.of("incidentId", incidentId, "status", incident.getStatus(), "logUpdated", true);
93
+    }
94
+
95
+    public WaterQualityIncident resolveIncident(Long incidentId, String resolution) {
96
+        WaterQualityIncident incident = incidentMapper.selectById(incidentId);
97
+        if (incident == null) throw new RuntimeException("水质事件不存在");
98
+
99
+        incident.setStatus("RESOLVED");
100
+        incident.setResolvedTime(LocalDateTime.now());
101
+        incident.setResolution(resolution);
102
+        incidentMapper.updateById(incident);
103
+        return incident;
104
+    }
105
+
106
+    public List<WaterQualityIncident> listIncidents(String status, String pollutantType) {
107
+        LambdaQueryWrapper<WaterQualityIncident> wrapper = new LambdaQueryWrapper<>();
108
+        if (status != null && !status.isBlank()) wrapper.eq(WaterQualityIncident::getStatus, status);
109
+        if (pollutantType != null && !pollutantType.isBlank()) wrapper.eq(WaterQualityIncident::getPollutantType, pollutantType);
110
+        return incidentMapper.selectList(wrapper.orderByDesc(WaterQualityIncident::getCreatedTime));
111
+    }
112
+
113
+    public Map<String, Object> getIncidentDetail(Long id) {
114
+        WaterQualityIncident incident = incidentMapper.selectById(id);
115
+        if (incident == null) throw new RuntimeException("水质事件不存在");
116
+
117
+        Map<String, Object> detail = new LinkedHashMap<>();
118
+        detail.put("incident", incident);
119
+        detail.put("plan", matchPlan(incident.getPollutantType()));
120
+        detail.put("exceedRate", incident.getStandardValue() > 0 ?
121
+            String.format("%.1f%%", (incident.getDetectedValue() / incident.getStandardValue() - 1) * 100) : "N/A");
122
+        return detail;
123
+    }
124
+
125
+    private String calculateSeverity(String type, Double detected, Double standard) {
126
+        if (standard == null || standard <= 0) return "MEDIUM";
127
+        double ratio = detected / standard;
128
+        if (ratio > 3.0 || "HEAVY_METAL".equals(type)) return "CRITICAL";
129
+        if (ratio > 2.0) return "HIGH";
130
+        if (ratio > 1.0) return "MEDIUM";
131
+        return "LOW";
132
+    }
133
+}

+ 67
- 0
wm-dispatch/src/main/resources/db/V2__emergency_drill.sql 查看文件

@@ -0,0 +1,67 @@
1
+-- Emergency Drill & Response DDL
2
+CREATE TABLE IF NOT EXISTS disp_pipe_burst_simulation (
3
+    id BIGSERIAL PRIMARY KEY,
4
+    simulation_no VARCHAR(50) UNIQUE,
5
+    location VARCHAR(200),
6
+    lng DOUBLE PRECISION,
7
+    lat DOUBLE PRECISION,
8
+    pipe_diameter DOUBLE PRECISION,
9
+    pipe_material VARCHAR(50),
10
+    affected_radius INT,
11
+    affected_users INT,
12
+    estimated_water_loss DOUBLE PRECISION,
13
+    estimated_repair_hours INT,
14
+    pressure_drop DOUBLE PRECISION,
15
+    affected_valves TEXT,
16
+    status VARCHAR(20) DEFAULT 'COMPLETED',
17
+    created_time TIMESTAMP DEFAULT NOW()
18
+);
19
+
20
+CREATE TABLE IF NOT EXISTS disp_water_quality_incident (
21
+    id BIGSERIAL PRIMARY KEY,
22
+    incident_no VARCHAR(50) UNIQUE,
23
+    location VARCHAR(200),
24
+    pollutant_type VARCHAR(50),
25
+    detected_value DOUBLE PRECISION,
26
+    standard_value DOUBLE PRECISION,
27
+    severity VARCHAR(20),
28
+    status VARCHAR(20) DEFAULT 'DETECTED',
29
+    matched_plan VARCHAR(200),
30
+    detected_time TIMESTAMP,
31
+    response_start_time TIMESTAMP,
32
+    resolved_time TIMESTAMP,
33
+    response_log TEXT,
34
+    resolution TEXT,
35
+    created_time TIMESTAMP DEFAULT NOW()
36
+);
37
+
38
+CREATE TABLE IF NOT EXISTS disp_emergency_drill (
39
+    id BIGSERIAL PRIMARY KEY,
40
+    drill_no VARCHAR(50) UNIQUE,
41
+    name VARCHAR(200),
42
+    drill_type VARCHAR(30),
43
+    description TEXT,
44
+    status VARCHAR(20) DEFAULT 'PLANNED',
45
+    planned_time TIMESTAMP,
46
+    started_time TIMESTAMP,
47
+    completed_time TIMESTAMP,
48
+    created_time TIMESTAMP DEFAULT NOW()
49
+);
50
+
51
+CREATE TABLE IF NOT EXISTS disp_drill_evaluation (
52
+    id BIGSERIAL PRIMARY KEY,
53
+    drill_id BIGINT REFERENCES disp_emergency_drill(id),
54
+    score INT,
55
+    response_time_score INT,
56
+    coordination_score INT,
57
+    overall_rating VARCHAR(20),
58
+    findings TEXT,
59
+    recommendations TEXT,
60
+    evaluator VARCHAR(50),
61
+    created_time TIMESTAMP DEFAULT NOW()
62
+);
63
+
64
+CREATE INDEX IF NOT EXISTS idx_pbs_status ON disp_pipe_burst_simulation(status);
65
+CREATE INDEX IF NOT EXISTS idx_wqi_status ON disp_water_quality_incident(status);
66
+CREATE INDEX IF NOT EXISTS idx_wqi_type ON disp_water_quality_incident(pollutant_type);
67
+CREATE INDEX IF NOT EXISTS idx_ed_status ON disp_emergency_drill(status);

+ 253
- 0
wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java 查看文件

@@ -0,0 +1,253 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.water.common.core.exception.BusinessException;
4
+import com.water.dispatch.entity.DrillEvaluation;
5
+import com.water.dispatch.entity.EmergencyDrill;
6
+import com.water.dispatch.entity.dto.DrillCreateRequest;
7
+import com.water.dispatch.entity.dto.DrillEvaluationRequest;
8
+import com.water.dispatch.mapper.DrillEvaluationMapper;
9
+import com.water.dispatch.mapper.EmergencyDrillMapper;
10
+import org.junit.jupiter.api.Test;
11
+import org.junit.jupiter.api.extension.ExtendWith;
12
+import org.mockito.InjectMocks;
13
+import org.mockito.Mock;
14
+import org.mockito.junit.jupiter.MockitoExtension;
15
+
16
+import java.time.LocalDate;
17
+import java.time.LocalDateTime;
18
+import java.util.Collections;
19
+import java.util.List;
20
+import java.util.Map;
21
+
22
+import static org.junit.jupiter.api.Assertions.*;
23
+import static org.mockito.ArgumentMatchers.any;
24
+import static org.mockito.Mockito.*;
25
+
26
+@ExtendWith(MockitoExtension.class)
27
+class EmergencyDrillServiceTest {
28
+
29
+    @Mock
30
+    private EmergencyDrillMapper drillMapper;
31
+    @Mock
32
+    private DrillEvaluationMapper evaluationMapper;
33
+
34
+    @InjectMocks
35
+    private EmergencyDrillService emergencyDrillService;
36
+
37
+    @Test
38
+    void testCreateDrill() {
39
+        when(drillMapper.insert(any())).thenAnswer(invocation -> {
40
+            EmergencyDrill drill = invocation.getArgument(0);
41
+            drill.setId(1L);
42
+            return 1;
43
+        });
44
+
45
+        DrillCreateRequest request = new DrillCreateRequest();
46
+        request.setName("2024年度爆管抢修演练");
47
+        request.setDrillType("PIPE_BURST");
48
+        request.setScenario("模拟DN600主干管爆裂");
49
+        request.setObjectives("检验应急响应速度和关阀操作");
50
+        request.setOrganizerId(1L);
51
+        request.setOrganizerName("张三");
52
+        request.setLocation("城南水厂");
53
+        request.setPlannedDate(LocalDate.of(2024, 6, 15));
54
+        request.setParticipantCount(50);
55
+
56
+        EmergencyDrill result = emergencyDrillService.createDrill(request);
57
+
58
+        assertNotNull(result.getDrillNo());
59
+        assertTrue(result.getDrillNo().startsWith("DRILL-"));
60
+        assertEquals("PLANNED", result.getStatus());
61
+        assertEquals("PIPE_BURST", result.getDrillType());
62
+        assertEquals("2024年度爆管抢修演练", result.getName());
63
+
64
+        verify(drillMapper).insert(any());
65
+    }
66
+
67
+    @Test
68
+    void testCreateDrillMissingName() {
69
+        DrillCreateRequest request = new DrillCreateRequest();
70
+        assertThrows(BusinessException.class, () -> emergencyDrillService.createDrill(request));
71
+    }
72
+
73
+    @Test
74
+    void testStartDrill() {
75
+        EmergencyDrill drill = buildDrill(1L, "PLANNED");
76
+        when(drillMapper.selectById(1L)).thenReturn(drill);
77
+        when(drillMapper.updateById(any())).thenReturn(1);
78
+
79
+        EmergencyDrill result = emergencyDrillService.startDrill(1L);
80
+
81
+        assertEquals("IN_PROGRESS", result.getStatus());
82
+        assertNotNull(result.getActualStartTime());
83
+        assertNotNull(result.getExecutionLog());
84
+    }
85
+
86
+    @Test
87
+    void testStartDrillWrongStatus() {
88
+        EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS");
89
+        when(drillMapper.selectById(1L)).thenReturn(drill);
90
+
91
+        assertThrows(BusinessException.class, () -> emergencyDrillService.startDrill(1L));
92
+    }
93
+
94
+    @Test
95
+    void testLogExecution() {
96
+        EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS");
97
+        drill.setExecutionLog("[]");
98
+        when(drillMapper.selectById(1L)).thenReturn(drill);
99
+        when(drillMapper.updateById(any())).thenReturn(1);
100
+
101
+        EmergencyDrill result = emergencyDrillService.logExecution(1L, "关阀操作", "完成上游阀门关闭", "李四");
102
+
103
+        assertEquals("IN_PROGRESS", result.getStatus());
104
+        assertNotNull(result.getExecutionLog());
105
+    }
106
+
107
+    @Test
108
+    void testLogExecutionWrongStatus() {
109
+        EmergencyDrill drill = buildDrill(1L, "PLANNED");
110
+        when(drillMapper.selectById(1L)).thenReturn(drill);
111
+
112
+        assertThrows(BusinessException.class,
113
+                () -> emergencyDrillService.logExecution(1L, "stage", "content", "operator"));
114
+    }
115
+
116
+    @Test
117
+    void testCompleteDrill() {
118
+        EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS");
119
+        drill.setExecutionLog("[]");
120
+        when(drillMapper.selectById(1L)).thenReturn(drill);
121
+        when(drillMapper.updateById(any())).thenReturn(1);
122
+
123
+        EmergencyDrill result = emergencyDrillService.completeDrill(1L,
124
+                "演练顺利完成", "发现通信设备不足", "建议增配对讲机");
125
+
126
+        assertEquals("COMPLETED", result.getStatus());
127
+        assertNotNull(result.getActualEndTime());
128
+        assertEquals("演练顺利完成", result.getSummary());
129
+        assertEquals("发现通信设备不足", result.getIssuesFound());
130
+    }
131
+
132
+    @Test
133
+    void testCompleteDrillWrongStatus() {
134
+        EmergencyDrill drill = buildDrill(1L, "PLANNED");
135
+        when(drillMapper.selectById(1L)).thenReturn(drill);
136
+
137
+        assertThrows(BusinessException.class,
138
+                () -> emergencyDrillService.completeDrill(1L, "summary", "issues", "improvements"));
139
+    }
140
+
141
+    @Test
142
+    void testCancelDrill() {
143
+        EmergencyDrill drill = buildDrill(1L, "PLANNED");
144
+        when(drillMapper.selectById(1L)).thenReturn(drill);
145
+        when(drillMapper.updateById(any())).thenReturn(1);
146
+
147
+        EmergencyDrill result = emergencyDrillService.cancelDrill(1L, "天气原因取消");
148
+
149
+        assertEquals("CANCELLED", result.getStatus());
150
+    }
151
+
152
+    @Test
153
+    void testCancelCompletedDrill() {
154
+        EmergencyDrill drill = buildDrill(1L, "COMPLETED");
155
+        when(drillMapper.selectById(1L)).thenReturn(drill);
156
+
157
+        assertThrows(BusinessException.class, () -> emergencyDrillService.cancelDrill(1L, "reason"));
158
+    }
159
+
160
+    @Test
161
+    void testEvaluateDrill() {
162
+        EmergencyDrill drill = buildDrill(1L, "COMPLETED");
163
+        when(drillMapper.selectById(1L)).thenReturn(drill);
164
+        when(drillMapper.updateById(any())).thenReturn(1);
165
+        when(evaluationMapper.insert(any())).thenAnswer(invocation -> {
166
+            DrillEvaluation eval = invocation.getArgument(0);
167
+            eval.setId(1L);
168
+            return 1;
169
+        });
170
+
171
+        DrillEvaluationRequest request = new DrillEvaluationRequest();
172
+        request.setDrillId(1L);
173
+        request.setEvaluatorId(1L);
174
+        request.setEvaluatorName("王五");
175
+        request.setResponseScore(85);
176
+        request.setHandlingScore(90);
177
+        request.setCoordinationScore(80);
178
+        request.setResourceScore(75);
179
+        request.setReportingScore(88);
180
+        request.setStrengths("响应迅速");
181
+        request.setWeaknesses("资源调配待加强");
182
+        request.setRecommendations("增加备品备件储备");
183
+
184
+        DrillEvaluation result = emergencyDrillService.evaluate(request);
185
+
186
+        assertNotNull(result.getEvaluationNo());
187
+        assertTrue(result.getEvaluationNo().startsWith("EVAL-"));
188
+        assertNotNull(result.getOverallScore());
189
+        assertTrue(result.getOverallScore() > 0);
190
+        assertNotNull(result.getGrade());
191
+        assertEquals("SUBMITTED", result.getStatus());
192
+
193
+        // 验证综合评分计算
194
+        // 85*0.25 + 90*0.30 + 80*0.20 + 75*0.15 + 88*0.10 = 21.25+27+16+11.25+8.8 = 84.3 ≈ 84
195
+        assertEquals(84, result.getOverallScore());
196
+        assertEquals("GOOD", result.getGrade());
197
+
198
+        verify(drillMapper).updateById(any()); // 更新演练状态为 EVALUATED
199
+    }
200
+
201
+    @Test
202
+    void testEvaluateWrongStatus() {
203
+        EmergencyDrill drill = buildDrill(1L, "PLANNED");
204
+        when(drillMapper.selectById(1L)).thenReturn(drill);
205
+
206
+        DrillEvaluationRequest request = new DrillEvaluationRequest();
207
+        request.setDrillId(1L);
208
+
209
+        assertThrows(BusinessException.class, () -> emergencyDrillService.evaluate(request));
210
+    }
211
+
212
+    @Test
213
+    void testGetDrillStatistics() {
214
+        when(drillMapper.selectCount(any())).thenReturn(10L, 5L, 3L, 2L);
215
+        when(evaluationMapper.selectList(any())).thenReturn(Collections.emptyList());
216
+
217
+        Map<String, Object> stats = emergencyDrillService.getDrillStatistics();
218
+
219
+        assertNotNull(stats);
220
+        assertEquals(10L, stats.get("totalDrills"));
221
+        assertEquals(5L, stats.get("completedDrills"));
222
+        assertEquals(3L, stats.get("evaluatedDrills"));
223
+        assertEquals(2L, stats.get("plannedDrills"));
224
+    }
225
+
226
+    @Test
227
+    void testGetDrillStatisticsWithEvaluations() {
228
+        when(drillMapper.selectCount(any())).thenReturn(5L, 3L, 2L, 0L);
229
+
230
+        DrillEvaluation eval1 = new DrillEvaluation();
231
+        eval1.setOverallScore(85);
232
+        DrillEvaluation eval2 = new DrillEvaluation();
233
+        eval2.setOverallScore(75);
234
+        when(evaluationMapper.selectList(any())).thenReturn(List.of(eval1, eval2));
235
+
236
+        Map<String, Object> stats = emergencyDrillService.getDrillStatistics();
237
+
238
+        assertEquals(80.0, stats.get("averageScore"));
239
+    }
240
+
241
+    private EmergencyDrill buildDrill(Long id, String status) {
242
+        EmergencyDrill drill = new EmergencyDrill();
243
+        drill.setId(id);
244
+        drill.setDrillNo("DRILL-TEST-001");
245
+        drill.setName("测试演练");
246
+        drill.setDrillType("PIPE_BURST");
247
+        drill.setScenario("测试场景");
248
+        drill.setStatus(status);
249
+        drill.setOrganizerId(1L);
250
+        drill.setOrganizerName("张三");
251
+        return drill;
252
+    }
253
+}

+ 164
- 0
wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java 查看文件

@@ -0,0 +1,164 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.water.common.core.exception.BusinessException;
4
+import com.water.dispatch.entity.PipeBurstSimulation;
5
+import com.water.dispatch.entity.dto.PipeBurstRequest;
6
+import com.water.dispatch.mapper.PipeBurstSimulationMapper;
7
+import org.junit.jupiter.api.Test;
8
+import org.junit.jupiter.api.extension.ExtendWith;
9
+import org.mockito.InjectMocks;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.util.Map;
14
+
15
+import static org.junit.jupiter.api.Assertions.*;
16
+import static org.mockito.ArgumentMatchers.any;
17
+import static org.mockito.Mockito.*;
18
+
19
+@ExtendWith(MockitoExtension.class)
20
+class PipeBurstServiceTest {
21
+
22
+    @Mock
23
+    private PipeBurstSimulationMapper simulationMapper;
24
+
25
+    @InjectMocks
26
+    private PipeBurstService pipeBurstService;
27
+
28
+    @Test
29
+    void testSimulatePipeBurst() {
30
+        when(simulationMapper.insert(any())).thenAnswer(invocation -> {
31
+            PipeBurstSimulation sim = invocation.getArgument(0);
32
+            sim.setId(1L);
33
+            return 1;
34
+        });
35
+        when(simulationMapper.updateById(any())).thenReturn(1);
36
+
37
+        PipeBurstRequest request = new PipeBurstRequest();
38
+        request.setLongitude(116.404);
39
+        request.setLatitude(39.915);
40
+        request.setLocation("北京市朝阳区建国路100号");
41
+        request.setPipeDiameter(400.0);
42
+        request.setPipeMaterial("球墨铸铁");
43
+        request.setPipePressure(0.35);
44
+        request.setCreatorId(1L);
45
+        request.setCreatorName("张三");
46
+
47
+        PipeBurstSimulation result = pipeBurstService.simulate(request);
48
+
49
+        assertNotNull(result.getSimulationNo());
50
+        assertTrue(result.getSimulationNo().startsWith("PBS-"));
51
+        assertEquals("COMPLETED", result.getStatus());
52
+        assertNotNull(result.getImpactRadius());
53
+        assertTrue(result.getImpactRadius() > 0);
54
+        assertNotNull(result.getAffectedUsers());
55
+        assertTrue(result.getAffectedUsers() > 0);
56
+        assertNotNull(result.getEstimatedRepairHours());
57
+        assertTrue(result.getEstimatedRepairHours() > 0);
58
+        assertNotNull(result.getValveShutdownPlan());
59
+        assertNotNull(result.getLeakageRate());
60
+
61
+        verify(simulationMapper).insert(any());
62
+        verify(simulationMapper, atLeastOnce()).updateById(any());
63
+    }
64
+
65
+    @Test
66
+    void testSimulateMissingCoordinates() {
67
+        PipeBurstRequest request = new PipeBurstRequest();
68
+        request.setLocation("测试位置");
69
+
70
+        assertThrows(BusinessException.class, () -> pipeBurstService.simulate(request));
71
+    }
72
+
73
+    @Test
74
+    void testSimulateDefaultValues() {
75
+        when(simulationMapper.insert(any())).thenReturn(1);
76
+        when(simulationMapper.updateById(any())).thenReturn(1);
77
+
78
+        PipeBurstRequest request = new PipeBurstRequest();
79
+        request.setLongitude(120.0);
80
+        request.setLatitude(30.0);
81
+        // 不设置pipeDiameter和pipePressure,使用默认值
82
+
83
+        PipeBurstSimulation result = pipeBurstService.simulate(request);
84
+
85
+        assertNotNull(result);
86
+        assertEquals("COMPLETED", result.getStatus());
87
+        assertNotNull(result.getImpactRadius());
88
+    }
89
+
90
+    @Test
91
+    void testGetImpactAnalysis() {
92
+        PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED");
93
+        when(simulationMapper.selectById(1L)).thenReturn(sim);
94
+
95
+        Map<String, Object> analysis = pipeBurstService.getImpactAnalysis(1L);
96
+
97
+        assertNotNull(analysis);
98
+        assertEquals("PBS-TEST-001", analysis.get("simulationNo"));
99
+        assertTrue(analysis.containsKey("impactRadius"));
100
+        assertTrue(analysis.containsKey("affectedUsers"));
101
+        assertTrue(analysis.containsKey("valveShutdownPlan"));
102
+    }
103
+
104
+    @Test
105
+    void testGetValvePlan() {
106
+        PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED");
107
+        when(simulationMapper.selectById(1L)).thenReturn(sim);
108
+
109
+        Map<String, Object> plan = pipeBurstService.getValvePlan(1L);
110
+
111
+        assertNotNull(plan);
112
+        assertEquals("PBS-TEST-001", plan.get("pipeNo"));
113
+        assertTrue(plan.containsKey("valveShutdownPlan"));
114
+        assertTrue(plan.containsKey("recommendation"));
115
+    }
116
+
117
+    @Test
118
+    void testArchiveSimulation() {
119
+        PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED");
120
+        when(simulationMapper.selectById(1L)).thenReturn(sim);
121
+        when(simulationMapper.updateById(any())).thenReturn(1);
122
+
123
+        PipeBurstSimulation result = pipeBurstService.archive(1L);
124
+
125
+        assertEquals("ARCHIVED", result.getStatus());
126
+        verify(simulationMapper).updateById(any());
127
+    }
128
+
129
+    @Test
130
+    void testArchiveNonCompletedSimulation() {
131
+        PipeBurstSimulation sim = buildSimulation(1L, "RUNNING");
132
+        when(simulationMapper.selectById(1L)).thenReturn(sim);
133
+
134
+        assertThrows(BusinessException.class, () -> pipeBurstService.archive(1L));
135
+    }
136
+
137
+    @Test
138
+    void testGetByIdNotFound() {
139
+        when(simulationMapper.selectById(999L)).thenReturn(null);
140
+        assertThrows(BusinessException.class, () -> pipeBurstService.getById(999L));
141
+    }
142
+
143
+    private PipeBurstSimulation buildSimulation(Long id, String status) {
144
+        PipeBurstSimulation sim = new PipeBurstSimulation();
145
+        sim.setId(id);
146
+        sim.setSimulationNo("PBS-TEST-001");
147
+        sim.setPipeNo("PIPE-001");
148
+        sim.setLongitude(116.404);
149
+        sim.setLatitude(39.915);
150
+        sim.setLocation("测试位置");
151
+        sim.setPipeDiameter(300.0);
152
+        sim.setPipePressure(0.3);
153
+        sim.setImpactRadius(150.0);
154
+        sim.setImpactArea(70685.83);
155
+        sim.setAffectedUsers(354);
156
+        sim.setAffectedRegion("测试区域");
157
+        sim.setLeakageRate(100.0);
158
+        sim.setEstimatedRepairHours(6.0);
159
+        sim.setValveShutdownPlan("[{valveId: V-001}]");
160
+        sim.setValveAffectedUsers(637);
161
+        sim.setStatus(status);
162
+        return sim;
163
+    }
164
+}

+ 186
- 0
wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java 查看文件

@@ -0,0 +1,186 @@
1
+package com.water.dispatch.service;
2
+
3
+import com.water.common.core.exception.BusinessException;
4
+import com.water.dispatch.entity.WaterQualityIncident;
5
+import com.water.dispatch.entity.dto.WaterQualityRequest;
6
+import com.water.dispatch.mapper.EmergencyPlanMapper;
7
+import com.water.dispatch.mapper.WaterQualityIncidentMapper;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.InjectMocks;
11
+import org.mockito.Mock;
12
+import org.mockito.junit.jupiter.MockitoExtension;
13
+
14
+import java.time.LocalDateTime;
15
+import java.util.Collections;
16
+import java.util.Map;
17
+
18
+import static org.junit.jupiter.api.Assertions.*;
19
+import static org.mockito.ArgumentMatchers.any;
20
+import static org.mockito.Mockito.*;
21
+
22
+@ExtendWith(MockitoExtension.class)
23
+class WaterQualityServiceTest {
24
+
25
+    @Mock
26
+    private WaterQualityIncidentMapper incidentMapper;
27
+    @Mock
28
+    private EmergencyPlanMapper planMapper;
29
+
30
+    @InjectMocks
31
+    private WaterQualityService waterQualityService;
32
+
33
+    @Test
34
+    void testReportIncident() {
35
+        when(incidentMapper.insert(any())).thenAnswer(invocation -> {
36
+            WaterQualityIncident inc = invocation.getArgument(0);
37
+            inc.setId(1L);
38
+            return 1;
39
+        });
40
+        when(incidentMapper.updateById(any())).thenReturn(1);
41
+        when(planMapper.selectList(any())).thenReturn(Collections.emptyList());
42
+
43
+        WaterQualityRequest request = new WaterQualityRequest();
44
+        request.setTitle("末梢水浊度超标");
45
+        request.setDescription("某小区末梢水浊度异常");
46
+        request.setSourceType("END");
47
+        request.setAbnormalIndicator("TURBIDITY");
48
+        request.setDetectedValue(3.5);
49
+        request.setStandardValue(1.0);
50
+        request.setDetectedTime(LocalDateTime.now());
51
+        request.setCreatorId(1L);
52
+        request.setCreatorName("张三");
53
+
54
+        WaterQualityIncident result = waterQualityService.report(request);
55
+
56
+        assertNotNull(result.getIncidentNo());
57
+        assertTrue(result.getIncidentNo().startsWith("WQI-"));
58
+        assertEquals("DETECTED", result.getStatus());
59
+        assertEquals(3.5, result.getExceedMultiple());
60
+        assertNotNull(result.getSeverityLevel());
61
+        assertNotNull(result.getWarningMessage());
62
+
63
+        verify(incidentMapper).insert(any());
64
+    }
65
+
66
+    @Test
67
+    void testReportMissingIndicator() {
68
+        WaterQualityRequest request = new WaterQualityRequest();
69
+        request.setTitle("测试");
70
+
71
+        assertThrows(BusinessException.class, () -> waterQualityService.report(request));
72
+    }
73
+
74
+    @Test
75
+    void testConfirmIncident() {
76
+        WaterQualityIncident incident = buildIncident(1L, "DETECTED");
77
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
78
+        when(incidentMapper.updateById(any())).thenReturn(1);
79
+
80
+        WaterQualityIncident result = waterQualityService.confirm(1L);
81
+
82
+        assertEquals("CONFIRMED", result.getStatus());
83
+        assertNotNull(result.getConfirmedTime());
84
+    }
85
+
86
+    @Test
87
+    void testConfirmWrongStatus() {
88
+        WaterQualityIncident incident = buildIncident(1L, "HANDLING");
89
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
90
+
91
+        assertThrows(BusinessException.class, () -> waterQualityService.confirm(1L));
92
+    }
93
+
94
+    @Test
95
+    void testStartHandling() {
96
+        WaterQualityIncident incident = buildIncident(1L, "CONFIRMED");
97
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
98
+        when(incidentMapper.updateById(any())).thenReturn(1);
99
+        when(planMapper.selectList(any())).thenReturn(Collections.emptyList());
100
+
101
+        WaterQualityIncident result = waterQualityService.startHandling(1L, 2L, "李四");
102
+
103
+        assertEquals("HANDLING", result.getStatus());
104
+        assertEquals(2L, result.getHandlerId());
105
+        assertEquals("李四", result.getHandlerName());
106
+        assertNotNull(result.getHandlingStartTime());
107
+        assertNotNull(result.getHandlingMeasures());
108
+        assertEquals(10, result.getHandlingProgress());
109
+    }
110
+
111
+    @Test
112
+    void testUpdateProgress() {
113
+        WaterQualityIncident incident = buildIncident(1L, "HANDLING");
114
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
115
+        when(incidentMapper.updateById(any())).thenReturn(1);
116
+
117
+        WaterQualityIncident result = waterQualityService.updateProgress(1L, 50, "管网冲洗完成");
118
+
119
+        assertEquals(50, result.getHandlingProgress());
120
+        assertEquals("HANDLING", result.getStatus());
121
+    }
122
+
123
+    @Test
124
+    void testUpdateProgressToComplete() {
125
+        WaterQualityIncident incident = buildIncident(1L, "HANDLING");
126
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
127
+        when(incidentMapper.updateById(any())).thenReturn(1);
128
+
129
+        WaterQualityIncident result = waterQualityService.updateProgress(1L, 100, "全部完成");
130
+
131
+        assertEquals(100, result.getHandlingProgress());
132
+        assertEquals("RESOLVED", result.getStatus());
133
+        assertNotNull(result.getResolvedTime());
134
+    }
135
+
136
+    @Test
137
+    void testResolveIncident() {
138
+        WaterQualityIncident incident = buildIncident(1L, "HANDLING");
139
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
140
+        when(incidentMapper.updateById(any())).thenReturn(1);
141
+
142
+        WaterQualityIncident result = waterQualityService.resolve(1L, "水质恢复正常");
143
+
144
+        assertEquals("RESOLVED", result.getStatus());
145
+        assertNotNull(result.getResolvedTime());
146
+    }
147
+
148
+    @Test
149
+    void testResolveAlreadyResolved() {
150
+        WaterQualityIncident incident = buildIncident(1L, "RESOLVED");
151
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
152
+
153
+        assertThrows(BusinessException.class, () -> waterQualityService.resolve(1L, "test"));
154
+    }
155
+
156
+    @Test
157
+    void testGetHandlingTimeline() {
158
+        WaterQualityIncident incident = buildIncident(1L, "HANDLING");
159
+        incident.setDetectedTime(LocalDateTime.of(2024, 1, 1, 8, 0));
160
+        incident.setConfirmedTime(LocalDateTime.of(2024, 1, 1, 9, 0));
161
+        incident.setHandlingStartTime(LocalDateTime.of(2024, 1, 1, 10, 0));
162
+        when(incidentMapper.selectById(1L)).thenReturn(incident);
163
+
164
+        Map<String, Object> timeline = waterQualityService.getHandlingTimeline(1L);
165
+
166
+        assertNotNull(timeline);
167
+        assertEquals("WQI-TEST-001", timeline.get("incidentNo"));
168
+        assertEquals("HANDLING", timeline.get("status"));
169
+        assertNotNull(timeline.get("timeline"));
170
+    }
171
+
172
+    private WaterQualityIncident buildIncident(Long id, String status) {
173
+        WaterQualityIncident incident = new WaterQualityIncident();
174
+        incident.setId(id);
175
+        incident.setIncidentNo("WQI-TEST-001");
176
+        incident.setTitle("浊度超标");
177
+        incident.setAbnormalIndicator("TURBIDITY");
178
+        incident.setDetectedValue(3.5);
179
+        incident.setStandardValue(1.0);
180
+        incident.setExceedMultiple(3.5);
181
+        incident.setSeverityLevel("LEVEL_2");
182
+        incident.setStatus(status);
183
+        incident.setHandlingProgress(0);
184
+        return incident;
185
+    }
186
+}