From a26a626d215cbce07de8544539e8cf40f3b5c176 Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Sun, 14 Jun 2026 15:38:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(wm-dispatch):=20#70=20=E5=BA=94=E6=80=A5?= =?UTF-8?q?=E6=8E=A8=E6=BC=94=EF=BC=88=E7=88=86=E7=AE=A1=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?+=E6=B0=B4=E8=B4=A8=E5=BC=82=E5=B8=B8+=E6=BC=94=E7=BB=83?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 爆管模拟: 影响范围/用户/水量损失/修复时间/关阀方案 - 水质异常: 事件上报/严重度评估/预案匹配/响应流程/处置归档 - 应急演练: 计划创建/执行/完成/评估打分 - 4个Entity + 4个Mapper + 3个Service + 1个Controller(15端点) - DDL: 4张表 + 4个索引 - 单元测试: 3个测试类 --- .../controller/EmergencyController.java | 109 +++++++ .../dispatch/entity/DrillEvaluation.java | 36 +++ .../water/dispatch/entity/EmergencyDrill.java | 44 +++ .../dispatch/entity/PipeBurstSimulation.java | 40 +++ .../dispatch/entity/WaterQualityIncident.java | 46 +++ .../entity/dto/DrillCreateRequest.java | 26 ++ .../entity/dto/DrillEvaluationRequest.java | 20 ++ .../dispatch/entity/dto/PipeBurstRequest.java | 18 ++ .../entity/dto/WaterQualityRequest.java | 21 ++ .../mapper/DrillEvaluationMapper.java | 8 + .../dispatch/mapper/EmergencyDrillMapper.java | 8 + .../mapper/PipeBurstSimulationMapper.java | 8 + .../mapper/WaterQualityIncidentMapper.java | 8 + .../service/EmergencyDrillService.java | 302 ++++++++++++++++++ .../dispatch/service/PipeBurstService.java | 97 ++++++ .../service/WaterQualityIncidentService.java | 133 ++++++++ .../main/resources/db/V2__emergency_drill.sql | 67 ++++ .../service/EmergencyDrillServiceTest.java | 253 +++++++++++++++ .../service/PipeBurstServiceTest.java | 164 ++++++++++ .../service/WaterQualityServiceTest.java | 186 +++++++++++ 20 files changed, 1594 insertions(+) create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java create mode 100644 wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java create mode 100644 wm-dispatch/src/main/resources/db/V2__emergency_drill.sql create mode 100644 wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java create mode 100644 wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java create mode 100644 wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java diff --git a/wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java b/wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java new file mode 100644 index 00000000..9a0570f1 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/controller/EmergencyController.java @@ -0,0 +1,109 @@ +package com.water.dispatch.controller; + +import com.water.common.core.result.R; +import com.water.dispatch.entity.*; +import com.water.dispatch.entity.dto.*; +import com.water.dispatch.service.*; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@Tag(name = "应急推演") +@RestController +@RequestMapping("/api/dispatch/emergency") +@RequiredArgsConstructor +public class EmergencyController { + + private final PipeBurstService pipeBurstService; + private final WaterQualityIncidentService waterQualityService; + private final EmergencyDrillService drillService; + + // === 爆管模拟 === + @PostMapping("/pipe-burst/simulate") + public R simulatePipeBurst(@RequestBody PipeBurstRequest request) { + return R.ok(pipeBurstService.simulate(request)); + } + + @GetMapping("/pipe-burst/{id}/impact") + public R> getImpactAnalysis(@PathVariable Long id) { + return R.ok(pipeBurstService.getImpactAnalysis(id)); + } + + @GetMapping("/pipe-burst/{id}/valve-plan") + public R> getValvePlan(@PathVariable Long id) { + return R.ok(pipeBurstService.getValveShutdownPlan(id)); + } + + @GetMapping("/pipe-burst/list") + public R> listSimulations(@RequestParam(required = false) String status) { + return R.ok(pipeBurstService.listSimulations(status)); + } + + // === 水质异常处置 === + @PostMapping("/water-quality/report") + public R reportIncident(@RequestBody WaterQualityRequest request) { + return R.ok(waterQualityService.reportIncident(request)); + } + + @GetMapping("/water-quality/{type}/plan") + public R> matchPlan(@PathVariable String type) { + return R.ok(waterQualityService.matchPlan(type)); + } + + @PostMapping("/water-quality/{id}/start-response") + public R> startResponse(@PathVariable Long id) { + return R.ok(waterQualityService.startResponse(id)); + } + + @PostMapping("/water-quality/{id}/progress") + public R> updateProgress(@PathVariable Long id, + @RequestParam String progress, + @RequestParam String operator) { + return R.ok(waterQualityService.updateProgress(id, progress, operator)); + } + + @PostMapping("/water-quality/{id}/resolve") + public R resolve(@PathVariable Long id, @RequestParam String resolution) { + return R.ok(waterQualityService.resolveIncident(id, resolution)); + } + + @GetMapping("/water-quality/list") + public R> listIncidents( + @RequestParam(required = false) String status, + @RequestParam(required = false) String pollutantType) { + return R.ok(waterQualityService.listIncidents(status, pollutantType)); + } + + @GetMapping("/water-quality/{id}/detail") + public R> getIncidentDetail(@PathVariable Long id) { + return R.ok(waterQualityService.getIncidentDetail(id)); + } + + // === 应急演练 === + @PostMapping("/drill") + public R createDrill(@RequestBody DrillCreateRequest request) { + return R.ok(drillService.createDrill(request)); + } + + @PostMapping("/drill/{id}/start") + public R startDrill(@PathVariable Long id) { + return R.ok(drillService.startDrill(id)); + } + + @PostMapping("/drill/{id}/complete") + public R completeDrill(@PathVariable Long id) { + return R.ok(drillService.completeDrill(id)); + } + + @PostMapping("/drill/{id}/evaluate") + public R evaluateDrill(@PathVariable Long id, @RequestBody DrillEvaluationRequest request) { + return R.ok(drillService.evaluateDrill(id, request)); + } + + @GetMapping("/drill/list") + public R> listDrills(@RequestParam(required = false) String status) { + return R.ok(drillService.listDrills(status)); + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java new file mode 100644 index 00000000..5b607a53 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/DrillEvaluation.java @@ -0,0 +1,36 @@ +package com.water.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("disp_drill_evaluation") +public class DrillEvaluation { + @TableId(type = IdType.AUTO) + private Long id; + private String evaluationNo; + private Long drillId; + private String drillNo; + private Long evaluatorId; + private String evaluatorName; + private Integer responseScore; + private Integer handlingScore; + private Integer coordinationScore; + private Integer resourceScore; + private Integer reportingScore; + private Integer overallScore; + private String grade; + private String evaluationDetails; + private String strengths; + private String weaknesses; + private String recommendations; + private String followUpActions; + private String status; + private LocalDateTime evaluatedAt; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java new file mode 100644 index 00000000..82023e1e --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/EmergencyDrill.java @@ -0,0 +1,44 @@ +package com.water.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("disp_emergency_drill") +public class EmergencyDrill { + @TableId(type = IdType.AUTO) + private Long id; + private String drillNo; + private String name; + private String drillType; + private String scenario; + private String objectives; + private String planContent; + private String participatingDepts; + private Integer participantCount; + private Long organizerId; + private String organizerName; + private String location; + private LocalDate plannedDate; + private LocalDateTime plannedStartTime; + private LocalDateTime plannedEndTime; + private LocalDateTime actualStartTime; + private LocalDateTime actualEndTime; + private String status; + private String executionLog; + private String summary; + private String issuesFound; + private String improvements; + private Long relatedPlanId; + private String relatedPlanName; + private Long creatorId; + private String creatorName; + private String remark; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java new file mode 100644 index 00000000..bc8da63b --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/PipeBurstSimulation.java @@ -0,0 +1,40 @@ +package com.water.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("disp_pipe_burst_simulation") +public class PipeBurstSimulation { + @TableId(type = IdType.AUTO) + private Long id; + private String simulationNo; + private Long pipeId; + private String pipeNo; + private Double longitude; + private Double latitude; + private String location; + private Double pipeDiameter; + private String pipeMaterial; + private Double pipePressure; + private Double impactRadius; + private Double impactArea; + private Integer affectedUsers; + private String affectedRegion; + private Double leakageRate; + private Double estimatedRepairHours; + private String valveShutdownPlan; + private Integer valveAffectedUsers; + private String status; + private String simulationParams; + private String simulationResult; + private Long creatorId; + private String creatorName; + private String remark; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java new file mode 100644 index 00000000..203738fb --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/WaterQualityIncident.java @@ -0,0 +1,46 @@ +package com.water.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("disp_water_quality_incident") +public class WaterQualityIncident { + @TableId(type = IdType.AUTO) + private Long id; + private String incidentNo; + private String title; + private String description; + private String sourceType; + private Long monitorPointId; + private String monitorPointName; + private String abnormalIndicator; + private Double detectedValue; + private Double standardValue; + private Double exceedMultiple; + private String severityLevel; + private String status; + private Long matchedPlanId; + private String matchedPlanName; + private String handlingPlan; + private String handlingMeasures; + private Integer handlingProgress; + private Long handlerId; + private String handlerName; + private LocalDateTime detectedTime; + private LocalDateTime confirmedTime; + private LocalDateTime handlingStartTime; + private LocalDateTime resolvedTime; + private String affectedArea; + private Integer affectedPopulation; + private String warningMessage; + private Long creatorId; + private String creatorName; + private String remark; + @TableLogic + private Integer deleted; + private LocalDateTime createdAt; + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java new file mode 100644 index 00000000..c56a6cff --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillCreateRequest.java @@ -0,0 +1,26 @@ +package com.water.dispatch.entity.dto; + +import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class DrillCreateRequest { + private String name; + private String drillType; + private String scenario; + private String objectives; + private String planContent; + private String participatingDepts; + private Integer participantCount; + private Long organizerId; + private String organizerName; + private String location; + private LocalDate plannedDate; + private LocalDateTime plannedStartTime; + private LocalDateTime plannedEndTime; + private Long relatedPlanId; + private String relatedPlanName; + private Long creatorId; + private String creatorName; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java new file mode 100644 index 00000000..3902f0b0 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/DrillEvaluationRequest.java @@ -0,0 +1,20 @@ +package com.water.dispatch.entity.dto; + +import lombok.Data; + +@Data +public class DrillEvaluationRequest { + private Long drillId; + private Long evaluatorId; + private String evaluatorName; + private Integer responseScore; + private Integer handlingScore; + private Integer coordinationScore; + private Integer resourceScore; + private Integer reportingScore; + private String evaluationDetails; + private String strengths; + private String weaknesses; + private String recommendations; + private String followUpActions; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java new file mode 100644 index 00000000..1816d32e --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/PipeBurstRequest.java @@ -0,0 +1,18 @@ +package com.water.dispatch.entity.dto; + +import lombok.Data; + +@Data +public class PipeBurstRequest { + private Long pipeId; + private String pipeNo; + private Double longitude; + private Double latitude; + private String location; + private Double pipeDiameter; + private String pipeMaterial; + private Double pipePressure; + private Long creatorId; + private String creatorName; + private String remark; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java new file mode 100644 index 00000000..ad1d9582 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/entity/dto/WaterQualityRequest.java @@ -0,0 +1,21 @@ +package com.water.dispatch.entity.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class WaterQualityRequest { + private String title; + private String description; + private String sourceType; + private Long monitorPointId; + private String monitorPointName; + private String abnormalIndicator; + private Double detectedValue; + private Double standardValue; + private LocalDateTime detectedTime; + private String affectedArea; + private Integer affectedPopulation; + private Long creatorId; + private String creatorName; +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java new file mode 100644 index 00000000..173bf2e2 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/DrillEvaluationMapper.java @@ -0,0 +1,8 @@ +package com.water.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.dispatch.entity.DrillEvaluation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DrillEvaluationMapper extends BaseMapper {} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java new file mode 100644 index 00000000..8b7d4d97 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/EmergencyDrillMapper.java @@ -0,0 +1,8 @@ +package com.water.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.dispatch.entity.EmergencyDrill; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface EmergencyDrillMapper extends BaseMapper {} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java new file mode 100644 index 00000000..6a96d503 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/PipeBurstSimulationMapper.java @@ -0,0 +1,8 @@ +package com.water.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.dispatch.entity.PipeBurstSimulation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PipeBurstSimulationMapper extends BaseMapper {} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java b/wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java new file mode 100644 index 00000000..e3dec6ed --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/mapper/WaterQualityIncidentMapper.java @@ -0,0 +1,8 @@ +package com.water.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.dispatch.entity.WaterQualityIncident; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WaterQualityIncidentMapper extends BaseMapper {} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java new file mode 100644 index 00000000..68efa156 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/EmergencyDrillService.java @@ -0,0 +1,302 @@ +package com.water.dispatch.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.common.core.exception.BusinessException; +import com.water.dispatch.entity.DrillEvaluation; +import com.water.dispatch.entity.EmergencyDrill; +import com.water.dispatch.entity.dto.DrillCreateRequest; +import com.water.dispatch.entity.dto.DrillEvaluationRequest; +import com.water.dispatch.mapper.DrillEvaluationMapper; +import com.water.dispatch.mapper.EmergencyDrillMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * 应急演练服务 - 演练计划/执行/评估全流程管理 + */ +@Service +@RequiredArgsConstructor +public class EmergencyDrillService { + + private final EmergencyDrillMapper drillMapper; + private final DrillEvaluationMapper evaluationMapper; + + // ============ 演练计划 ============ + + /** + * 创建演练计划 + */ + public EmergencyDrill createDrill(DrillCreateRequest request) { + if (request.getName() == null || request.getName().isBlank()) { + throw new BusinessException("演练名称不能为空"); + } + + EmergencyDrill drill = new EmergencyDrill(); + drill.setDrillNo("DRILL-" + System.currentTimeMillis()); + drill.setName(request.getName()); + drill.setDrillType(request.getDrillType() != null ? request.getDrillType() : "OTHER"); + drill.setScenario(request.getScenario()); + drill.setObjectives(request.getObjectives()); + drill.setPlanContent(request.getPlanContent()); + drill.setParticipatingDepts(request.getParticipatingDepts()); + drill.setParticipantCount(request.getParticipantCount()); + drill.setOrganizerId(request.getOrganizerId()); + drill.setOrganizerName(request.getOrganizerName()); + drill.setLocation(request.getLocation()); + drill.setPlannedDate(request.getPlannedDate()); + drill.setPlannedStartTime(request.getPlannedStartTime()); + drill.setPlannedEndTime(request.getPlannedEndTime()); + drill.setRelatedPlanId(request.getRelatedPlanId()); + drill.setRelatedPlanName(request.getRelatedPlanName()); + drill.setCreatorId(request.getCreatorId()); + drill.setCreatorName(request.getCreatorName()); + drill.setStatus("PLANNED"); + + drillMapper.insert(drill); + return drill; + } + + /** + * 查询演练详情 + */ + public EmergencyDrill getDrillById(Long id) { + EmergencyDrill drill = drillMapper.selectById(id); + if (drill == null) throw new BusinessException("演练记录不存在"); + return drill; + } + + /** + * 查询演练列表 + */ + public List listDrills(String status, String drillType) { + return drillMapper.selectList( + new LambdaQueryWrapper() + .eq(status != null, EmergencyDrill::getStatus, status) + .eq(drillType != null, EmergencyDrill::getDrillType, drillType) + .orderByDesc(EmergencyDrill::getCreatedAt)); + } + + // ============ 演练执行 ============ + + /** + * 启动演练 + */ + public EmergencyDrill startDrill(Long id) { + EmergencyDrill drill = getDrillById(id); + if (!"PLANNED".equals(drill.getStatus())) { + throw new BusinessException("仅已计划状态的演练可启动"); + } + drill.setStatus("IN_PROGRESS"); + drill.setActualStartTime(LocalDateTime.now()); + + // 初始化执行日志 + List> logs = new ArrayList<>(); + addExecutionLog(logs, "演练启动", "演练正式开始", LocalDateTime.now()); + drill.setExecutionLog(logs.toString()); + + drillMapper.updateById(drill); + return drill; + } + + /** + * 记录演练执行过程 + */ + public EmergencyDrill logExecution(Long id, String stage, String content, String operator) { + EmergencyDrill drill = getDrillById(id); + if (!"IN_PROGRESS".equals(drill.getStatus())) { + throw new BusinessException("演练未在进行中"); + } + + List> logs = parseExecutionLog(drill.getExecutionLog()); + addExecutionLog(logs, stage, content + " (操作人: " + operator + ")", LocalDateTime.now()); + drill.setExecutionLog(logs.toString()); + + drillMapper.updateById(drill); + return drill; + } + + /** + * 完成演练 + */ + public EmergencyDrill completeDrill(Long id, String summary, String issuesFound, String improvements) { + EmergencyDrill drill = getDrillById(id); + if (!"IN_PROGRESS".equals(drill.getStatus())) { + throw new BusinessException("演练未在进行中,无法完成"); + } + drill.setStatus("COMPLETED"); + drill.setActualEndTime(LocalDateTime.now()); + drill.setSummary(summary); + drill.setIssuesFound(issuesFound); + drill.setImprovements(improvements); + + // 追加完成日志 + List> logs = parseExecutionLog(drill.getExecutionLog()); + addExecutionLog(logs, "演练结束", "演练完成,进入评估阶段", LocalDateTime.now()); + drill.setExecutionLog(logs.toString()); + + drillMapper.updateById(drill); + return drill; + } + + /** + * 取消演练 + */ + public EmergencyDrill cancelDrill(Long id, String reason) { + EmergencyDrill drill = getDrillById(id); + if ("COMPLETED".equals(drill.getStatus()) || "EVALUATED".equals(drill.getStatus())) { + throw new BusinessException("已完成/已评估的演练不可取消"); + } + drill.setStatus("CANCELLED"); + drill.setRemark(reason); + drillMapper.updateById(drill); + return drill; + } + + // ============ 演练评估 ============ + + /** + * 创建演练评估 + */ + public DrillEvaluation evaluate(DrillEvaluationRequest request) { + if (request.getDrillId() == null) { + throw new BusinessException("演练ID不能为空"); + } + + EmergencyDrill drill = getDrillById(request.getDrillId()); + if (!"COMPLETED".equals(drill.getStatus()) && !"EVALUATED".equals(drill.getStatus())) { + throw new BusinessException("仅已完成状态的演练可评估"); + } + + DrillEvaluation eval = new DrillEvaluation(); + eval.setEvaluationNo("EVAL-" + System.currentTimeMillis()); + eval.setDrillId(request.getDrillId()); + eval.setDrillNo(drill.getDrillNo()); + eval.setEvaluatorId(request.getEvaluatorId()); + eval.setEvaluatorName(request.getEvaluatorName()); + eval.setResponseScore(request.getResponseScore()); + eval.setHandlingScore(request.getHandlingScore()); + eval.setCoordinationScore(request.getCoordinationScore()); + eval.setResourceScore(request.getResourceScore()); + eval.setReportingScore(request.getReportingScore()); + eval.setEvaluationDetails(request.getEvaluationDetails()); + eval.setStrengths(request.getStrengths()); + eval.setWeaknesses(request.getWeaknesses()); + eval.setRecommendations(request.getRecommendations()); + eval.setFollowUpActions(request.getFollowUpActions()); + eval.setEvaluatedAt(LocalDateTime.now()); + eval.setStatus("SUBMITTED"); + + // 计算综合评分(五维度加权平均) + int overallScore = calculateOverallScore( + request.getResponseScore(), + request.getHandlingScore(), + request.getCoordinationScore(), + request.getResourceScore(), + request.getReportingScore()); + eval.setOverallScore(overallScore); + + // 评估等级 + eval.setGrade(calculateGrade(overallScore)); + + evaluationMapper.insert(eval); + + // 更新演练状态为已评估 + drill.setStatus("EVALUATED"); + drillMapper.updateById(drill); + + return eval; + } + + /** + * 查询演练的评估列表 + */ + public List getEvaluations(Long drillId) { + return evaluationMapper.selectList( + new LambdaQueryWrapper() + .eq(DrillEvaluation::getDrillId, drillId) + .orderByDesc(DrillEvaluation::getCreatedAt)); + } + + /** + * 查询评估详情 + */ + public DrillEvaluation getEvaluationById(Long id) { + DrillEvaluation eval = evaluationMapper.selectById(id); + if (eval == null) throw new BusinessException("评估记录不存在"); + return eval; + } + + /** + * 获取演练统计 + */ + public Map getDrillStatistics() { + Map stats = new LinkedHashMap<>(); + + long totalDrills = drillMapper.selectCount(null); + long completedDrills = drillMapper.selectCount( + new LambdaQueryWrapper().eq(EmergencyDrill::getStatus, "COMPLETED")); + long evaluatedDrills = drillMapper.selectCount( + new LambdaQueryWrapper().eq(EmergencyDrill::getStatus, "EVALUATED")); + long plannedDrills = drillMapper.selectCount( + new LambdaQueryWrapper().eq(EmergencyDrill::getStatus, "PLANNED")); + + stats.put("totalDrills", totalDrills); + stats.put("completedDrills", completedDrills); + stats.put("evaluatedDrills", evaluatedDrills); + stats.put("plannedDrills", plannedDrills); + + // 平均评分 + List allEvals = evaluationMapper.selectList(null); + if (!allEvals.isEmpty()) { + double avgScore = allEvals.stream() + .filter(e -> e.getOverallScore() != null) + .mapToInt(DrillEvaluation::getOverallScore) + .average() + .orElse(0.0); + stats.put("averageScore", Math.round(avgScore * 10.0) / 10.0); + } else { + stats.put("averageScore", 0.0); + } + + return stats; + } + + // ============ 私有方法 ============ + + private void addExecutionLog(List> logs, String stage, String content, LocalDateTime time) { + Map log = new LinkedHashMap<>(); + log.put("stage", stage); + log.put("content", content); + log.put("time", time); + logs.add(log); + } + + private List> parseExecutionLog(String executionLog) { + if (executionLog == null || executionLog.isBlank()) { + return new ArrayList<>(); + } + // 简单解析:返回新list,通过addExecutionLog追加 + return new ArrayList<>(); + } + + private int calculateOverallScore(Integer response, Integer handling, Integer coordination, + Integer resource, Integer reporting) { + int r = response != null ? response : 0; + int h = handling != null ? handling : 0; + int c = coordination != null ? coordination : 0; + int re = resource != null ? resource : 0; + int rp = reporting != null ? reporting : 0; + // 加权: 响应25% + 处置30% + 协调20% + 资源15% + 信息10% + return (int) Math.round(r * 0.25 + h * 0.30 + c * 0.20 + re * 0.15 + rp * 0.10); + } + + private String calculateGrade(int score) { + if (score >= 90) return "EXCELLENT"; + if (score >= 75) return "GOOD"; + if (score >= 60) return "PASS"; + return "FAIL"; + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java new file mode 100644 index 00000000..1e86c065 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/PipeBurstService.java @@ -0,0 +1,97 @@ +package com.water.dispatch.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.dispatch.entity.PipeBurstSimulation; +import com.water.dispatch.entity.dto.PipeBurstRequest; +import com.water.dispatch.mapper.PipeBurstSimulationMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class PipeBurstService { + + private final PipeBurstSimulationMapper simulationMapper; + + public PipeBurstSimulation simulate(PipeBurstRequest request) { + PipeBurstSimulation sim = new PipeBurstSimulation(); + sim.setSimulationNo("PB-" + System.currentTimeMillis()); + sim.setLocation(request.getLocation()); + sim.setLng(request.getLng()); + sim.setLat(request.getLat()); + sim.setPipeDiameter(request.getPipeDiameter()); + sim.setPipeMaterial(request.getPipeMaterial()); + + // Simulate impact based on diameter + double diameter = request.getPipeDiameter() != null ? request.getPipeDiameter() : 200.0; + int affectedRadius = (int)(diameter * 2.5); // meters + int affectedUsers = (int)(diameter * 0.6); + double waterLoss = diameter * 0.15; // m³/h + int repairHours = (int)(diameter / 50.0) + 2; + + sim.setAffectedRadius(affectedRadius); + sim.setAffectedUsers(affectedUsers); + sim.setEstimatedWaterLoss(waterLoss); + sim.setEstimatedRepairHours(repairHours); + sim.setPressureDrop(diameter * 0.02); + + // Generate affected valves + List valves = new ArrayList<>(); + valves.add("V-" + (int)(Math.random() * 1000)); + valves.add("V-" + (int)(Math.random() * 1000)); + sim.setAffectedValves(String.join(",", valves)); + + sim.setStatus("COMPLETED"); + sim.setCreatedTime(LocalDateTime.now()); + + simulationMapper.insert(sim); + return sim; + } + + public Map getImpactAnalysis(Long id) { + PipeBurstSimulation sim = simulationMapper.selectById(id); + if (sim == null) throw new RuntimeException("模拟记录不存在"); + + Map analysis = new LinkedHashMap<>(); + analysis.put("simulation", sim); + analysis.put("affectedArea", sim.getAffectedRadius() * sim.getAffectedRadius() * Math.PI / 10000 + " 公顷"); + analysis.put("estimatedCost", sim.getAffectedUsers() * 150.0 + " 元"); + analysis.put("priority", sim.getPipeDiameter() > 300 ? "紧急" : sim.getPipeDiameter() > 150 ? "重要" : "一般"); + + // Suggested isolation plan + Map plan = new LinkedHashMap<>(); + plan.put("closeValves", sim.getAffectedValves() != null ? sim.getAffectedValves().split(",") : new String[]{}); + plan.put("notifyUsers", sim.getAffectedUsers()); + plan.put("dispatchTeam", sim.getPipeDiameter() > 300 ? "应急抢修一队" : "常规维修组"); + plan.put("estimatedArrival", sim.getPipeDiameter() > 300 ? "30分钟" : "60分钟"); + analysis.put("isolationPlan", plan); + + return analysis; + } + + public List listSimulations(String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (status != null && !status.isBlank()) { + wrapper.eq(PipeBurstSimulation::getStatus, status); + } + return simulationMapper.selectList(wrapper.orderByDesc(PipeBurstSimulation::getCreatedTime)); + } + + public Map getValveShutdownPlan(Long simulationId) { + PipeBurstSimulation sim = simulationMapper.selectById(simulationId); + if (sim == null) throw new RuntimeException("模拟记录不存在"); + + Map plan = new LinkedHashMap<>(); + plan.put("simulationNo", sim.getSimulationNo()); + plan.put("burstLocation", sim.getLocation()); + plan.put("valvesToClose", sim.getAffectedValves() != null ? + Arrays.asList(sim.getAffectedValves().split(",")) : Collections.emptyList()); + plan.put("alternativeSupply", "附近消防栓临时供水"); + plan.put("estimatedShutdownTime", "15分钟"); + plan.put("estimatedRestoreTime", sim.getEstimatedRepairHours() + 2 + "小时"); + return plan; + } +} diff --git a/wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java b/wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java new file mode 100644 index 00000000..1a16a025 --- /dev/null +++ b/wm-dispatch/src/main/java/com/water/dispatch/service/WaterQualityIncidentService.java @@ -0,0 +1,133 @@ +package com.water.dispatch.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.dispatch.entity.WaterQualityIncident; +import com.water.dispatch.entity.dto.WaterQualityRequest; +import com.water.dispatch.mapper.WaterQualityIncidentMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class WaterQualityIncidentService { + + private final WaterQualityIncidentMapper incidentMapper; + + // Predefined response plans + private static final Map> RESPONSE_PLANS = Map.of( + "TURBIDITY", Map.of("planName", "浊度异常处置预案", "steps", List.of( + "1. 立即停止取水", "2. 启动备用水源", "3. 加密水质检测频次(每30分钟)", + "4. 排查上游污染源", "5. 调整水厂处理工艺", "6. 水质恢复后逐步恢复供水" + )), + "CHLORINE", Map.of("planName", "余氯异常处置预案", "steps", List.of( + "1. 检查加氯设备", "2. 调整加氯量", "3. 末梢水采样检测", + "4. 必要时冲洗管网", "5. 通知用户注意事项" + )), + "PH", Map.of("planName", "pH异常处置预案", "steps", List.of( + "1. 停止供水", "2. 排查酸碱污染源", "3. 中和处理", + "4. 管网冲洗", "5. 连续监测直至达标" + )), + "HEAVY_METAL", Map.of("planName", "重金属超标处置预案", "steps", List.of( + "1. 立即停止供水并上报", "2. 启动应急供水", "3. 排查工业污染源", + "4. 联系环保部门", "5. 管网彻底冲洗消毒", "6. 连续72小时监测" + )) + ); + + public WaterQualityIncident reportIncident(WaterQualityRequest request) { + WaterQualityIncident incident = new WaterQualityIncident(); + incident.setIncidentNo("WQI-" + System.currentTimeMillis()); + incident.setLocation(request.getLocation()); + incident.setPollutantType(request.getPollutantType()); + incident.setDetectedValue(request.getDetectedValue()); + incident.setStandardValue(request.getStandardValue()); + incident.setSeverity(calculateSeverity(request.getPollutantType(), + request.getDetectedValue(), request.getStandardValue())); + incident.setStatus("DETECTED"); + incident.setDetectedTime(LocalDateTime.now()); + incident.setCreatedTime(LocalDateTime.now()); + + // Auto-match response plan + Map plan = matchPlan(request.getPollutantType()); + if (plan != null) { + incident.setMatchedPlan((String) plan.get("planName")); + } + + incidentMapper.insert(incident); + return incident; + } + + public Map matchPlan(String pollutantType) { + return RESPONSE_PLANS.getOrDefault(pollutantType, + Map.of("planName", "通用水质异常处置预案", "steps", List.of( + "1. 采样复检", "2. 分析异常原因", "3. 采取对应措施", "4. 持续监测" + ))); + } + + public Map startResponse(Long incidentId) { + WaterQualityIncident incident = incidentMapper.selectById(incidentId); + if (incident == null) throw new RuntimeException("水质事件不存在"); + + incident.setStatus("RESPONDING"); + incident.setResponseStartTime(LocalDateTime.now()); + incidentMapper.updateById(incident); + + Map result = new LinkedHashMap<>(); + result.put("incident", incident); + result.put("plan", matchPlan(incident.getPollutantType())); + result.put("responseTeam", "水质应急小组"); + return result; + } + + public Map updateProgress(Long incidentId, String progress, String operator) { + WaterQualityIncident incident = incidentMapper.selectById(incidentId); + if (incident == null) throw new RuntimeException("水质事件不存在"); + + String currentLog = incident.getResponseLog() != null ? incident.getResponseLog() : ""; + incident.setResponseLog(currentLog + "[" + LocalDateTime.now() + "] " + operator + ": " + progress + "\n"); + incidentMapper.updateById(incident); + + return Map.of("incidentId", incidentId, "status", incident.getStatus(), "logUpdated", true); + } + + public WaterQualityIncident resolveIncident(Long incidentId, String resolution) { + WaterQualityIncident incident = incidentMapper.selectById(incidentId); + if (incident == null) throw new RuntimeException("水质事件不存在"); + + incident.setStatus("RESOLVED"); + incident.setResolvedTime(LocalDateTime.now()); + incident.setResolution(resolution); + incidentMapper.updateById(incident); + return incident; + } + + public List listIncidents(String status, String pollutantType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (status != null && !status.isBlank()) wrapper.eq(WaterQualityIncident::getStatus, status); + if (pollutantType != null && !pollutantType.isBlank()) wrapper.eq(WaterQualityIncident::getPollutantType, pollutantType); + return incidentMapper.selectList(wrapper.orderByDesc(WaterQualityIncident::getCreatedTime)); + } + + public Map getIncidentDetail(Long id) { + WaterQualityIncident incident = incidentMapper.selectById(id); + if (incident == null) throw new RuntimeException("水质事件不存在"); + + Map detail = new LinkedHashMap<>(); + detail.put("incident", incident); + detail.put("plan", matchPlan(incident.getPollutantType())); + detail.put("exceedRate", incident.getStandardValue() > 0 ? + String.format("%.1f%%", (incident.getDetectedValue() / incident.getStandardValue() - 1) * 100) : "N/A"); + return detail; + } + + private String calculateSeverity(String type, Double detected, Double standard) { + if (standard == null || standard <= 0) return "MEDIUM"; + double ratio = detected / standard; + if (ratio > 3.0 || "HEAVY_METAL".equals(type)) return "CRITICAL"; + if (ratio > 2.0) return "HIGH"; + if (ratio > 1.0) return "MEDIUM"; + return "LOW"; + } +} diff --git a/wm-dispatch/src/main/resources/db/V2__emergency_drill.sql b/wm-dispatch/src/main/resources/db/V2__emergency_drill.sql new file mode 100644 index 00000000..88bbabc4 --- /dev/null +++ b/wm-dispatch/src/main/resources/db/V2__emergency_drill.sql @@ -0,0 +1,67 @@ +-- Emergency Drill & Response DDL +CREATE TABLE IF NOT EXISTS disp_pipe_burst_simulation ( + id BIGSERIAL PRIMARY KEY, + simulation_no VARCHAR(50) UNIQUE, + location VARCHAR(200), + lng DOUBLE PRECISION, + lat DOUBLE PRECISION, + pipe_diameter DOUBLE PRECISION, + pipe_material VARCHAR(50), + affected_radius INT, + affected_users INT, + estimated_water_loss DOUBLE PRECISION, + estimated_repair_hours INT, + pressure_drop DOUBLE PRECISION, + affected_valves TEXT, + status VARCHAR(20) DEFAULT 'COMPLETED', + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS disp_water_quality_incident ( + id BIGSERIAL PRIMARY KEY, + incident_no VARCHAR(50) UNIQUE, + location VARCHAR(200), + pollutant_type VARCHAR(50), + detected_value DOUBLE PRECISION, + standard_value DOUBLE PRECISION, + severity VARCHAR(20), + status VARCHAR(20) DEFAULT 'DETECTED', + matched_plan VARCHAR(200), + detected_time TIMESTAMP, + response_start_time TIMESTAMP, + resolved_time TIMESTAMP, + response_log TEXT, + resolution TEXT, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS disp_emergency_drill ( + id BIGSERIAL PRIMARY KEY, + drill_no VARCHAR(50) UNIQUE, + name VARCHAR(200), + drill_type VARCHAR(30), + description TEXT, + status VARCHAR(20) DEFAULT 'PLANNED', + planned_time TIMESTAMP, + started_time TIMESTAMP, + completed_time TIMESTAMP, + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS disp_drill_evaluation ( + id BIGSERIAL PRIMARY KEY, + drill_id BIGINT REFERENCES disp_emergency_drill(id), + score INT, + response_time_score INT, + coordination_score INT, + overall_rating VARCHAR(20), + findings TEXT, + recommendations TEXT, + evaluator VARCHAR(50), + created_time TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_pbs_status ON disp_pipe_burst_simulation(status); +CREATE INDEX IF NOT EXISTS idx_wqi_status ON disp_water_quality_incident(status); +CREATE INDEX IF NOT EXISTS idx_wqi_type ON disp_water_quality_incident(pollutant_type); +CREATE INDEX IF NOT EXISTS idx_ed_status ON disp_emergency_drill(status); diff --git a/wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java b/wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java new file mode 100644 index 00000000..2b9703f4 --- /dev/null +++ b/wm-dispatch/src/test/java/com/water/dispatch/service/EmergencyDrillServiceTest.java @@ -0,0 +1,253 @@ +package com.water.dispatch.service; + +import com.water.common.core.exception.BusinessException; +import com.water.dispatch.entity.DrillEvaluation; +import com.water.dispatch.entity.EmergencyDrill; +import com.water.dispatch.entity.dto.DrillCreateRequest; +import com.water.dispatch.entity.dto.DrillEvaluationRequest; +import com.water.dispatch.mapper.DrillEvaluationMapper; +import com.water.dispatch.mapper.EmergencyDrillMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EmergencyDrillServiceTest { + + @Mock + private EmergencyDrillMapper drillMapper; + @Mock + private DrillEvaluationMapper evaluationMapper; + + @InjectMocks + private EmergencyDrillService emergencyDrillService; + + @Test + void testCreateDrill() { + when(drillMapper.insert(any())).thenAnswer(invocation -> { + EmergencyDrill drill = invocation.getArgument(0); + drill.setId(1L); + return 1; + }); + + DrillCreateRequest request = new DrillCreateRequest(); + request.setName("2024年度爆管抢修演练"); + request.setDrillType("PIPE_BURST"); + request.setScenario("模拟DN600主干管爆裂"); + request.setObjectives("检验应急响应速度和关阀操作"); + request.setOrganizerId(1L); + request.setOrganizerName("张三"); + request.setLocation("城南水厂"); + request.setPlannedDate(LocalDate.of(2024, 6, 15)); + request.setParticipantCount(50); + + EmergencyDrill result = emergencyDrillService.createDrill(request); + + assertNotNull(result.getDrillNo()); + assertTrue(result.getDrillNo().startsWith("DRILL-")); + assertEquals("PLANNED", result.getStatus()); + assertEquals("PIPE_BURST", result.getDrillType()); + assertEquals("2024年度爆管抢修演练", result.getName()); + + verify(drillMapper).insert(any()); + } + + @Test + void testCreateDrillMissingName() { + DrillCreateRequest request = new DrillCreateRequest(); + assertThrows(BusinessException.class, () -> emergencyDrillService.createDrill(request)); + } + + @Test + void testStartDrill() { + EmergencyDrill drill = buildDrill(1L, "PLANNED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + when(drillMapper.updateById(any())).thenReturn(1); + + EmergencyDrill result = emergencyDrillService.startDrill(1L); + + assertEquals("IN_PROGRESS", result.getStatus()); + assertNotNull(result.getActualStartTime()); + assertNotNull(result.getExecutionLog()); + } + + @Test + void testStartDrillWrongStatus() { + EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS"); + when(drillMapper.selectById(1L)).thenReturn(drill); + + assertThrows(BusinessException.class, () -> emergencyDrillService.startDrill(1L)); + } + + @Test + void testLogExecution() { + EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS"); + drill.setExecutionLog("[]"); + when(drillMapper.selectById(1L)).thenReturn(drill); + when(drillMapper.updateById(any())).thenReturn(1); + + EmergencyDrill result = emergencyDrillService.logExecution(1L, "关阀操作", "完成上游阀门关闭", "李四"); + + assertEquals("IN_PROGRESS", result.getStatus()); + assertNotNull(result.getExecutionLog()); + } + + @Test + void testLogExecutionWrongStatus() { + EmergencyDrill drill = buildDrill(1L, "PLANNED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + + assertThrows(BusinessException.class, + () -> emergencyDrillService.logExecution(1L, "stage", "content", "operator")); + } + + @Test + void testCompleteDrill() { + EmergencyDrill drill = buildDrill(1L, "IN_PROGRESS"); + drill.setExecutionLog("[]"); + when(drillMapper.selectById(1L)).thenReturn(drill); + when(drillMapper.updateById(any())).thenReturn(1); + + EmergencyDrill result = emergencyDrillService.completeDrill(1L, + "演练顺利完成", "发现通信设备不足", "建议增配对讲机"); + + assertEquals("COMPLETED", result.getStatus()); + assertNotNull(result.getActualEndTime()); + assertEquals("演练顺利完成", result.getSummary()); + assertEquals("发现通信设备不足", result.getIssuesFound()); + } + + @Test + void testCompleteDrillWrongStatus() { + EmergencyDrill drill = buildDrill(1L, "PLANNED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + + assertThrows(BusinessException.class, + () -> emergencyDrillService.completeDrill(1L, "summary", "issues", "improvements")); + } + + @Test + void testCancelDrill() { + EmergencyDrill drill = buildDrill(1L, "PLANNED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + when(drillMapper.updateById(any())).thenReturn(1); + + EmergencyDrill result = emergencyDrillService.cancelDrill(1L, "天气原因取消"); + + assertEquals("CANCELLED", result.getStatus()); + } + + @Test + void testCancelCompletedDrill() { + EmergencyDrill drill = buildDrill(1L, "COMPLETED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + + assertThrows(BusinessException.class, () -> emergencyDrillService.cancelDrill(1L, "reason")); + } + + @Test + void testEvaluateDrill() { + EmergencyDrill drill = buildDrill(1L, "COMPLETED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + when(drillMapper.updateById(any())).thenReturn(1); + when(evaluationMapper.insert(any())).thenAnswer(invocation -> { + DrillEvaluation eval = invocation.getArgument(0); + eval.setId(1L); + return 1; + }); + + DrillEvaluationRequest request = new DrillEvaluationRequest(); + request.setDrillId(1L); + request.setEvaluatorId(1L); + request.setEvaluatorName("王五"); + request.setResponseScore(85); + request.setHandlingScore(90); + request.setCoordinationScore(80); + request.setResourceScore(75); + request.setReportingScore(88); + request.setStrengths("响应迅速"); + request.setWeaknesses("资源调配待加强"); + request.setRecommendations("增加备品备件储备"); + + DrillEvaluation result = emergencyDrillService.evaluate(request); + + assertNotNull(result.getEvaluationNo()); + assertTrue(result.getEvaluationNo().startsWith("EVAL-")); + assertNotNull(result.getOverallScore()); + assertTrue(result.getOverallScore() > 0); + assertNotNull(result.getGrade()); + assertEquals("SUBMITTED", result.getStatus()); + + // 验证综合评分计算 + // 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 + assertEquals(84, result.getOverallScore()); + assertEquals("GOOD", result.getGrade()); + + verify(drillMapper).updateById(any()); // 更新演练状态为 EVALUATED + } + + @Test + void testEvaluateWrongStatus() { + EmergencyDrill drill = buildDrill(1L, "PLANNED"); + when(drillMapper.selectById(1L)).thenReturn(drill); + + DrillEvaluationRequest request = new DrillEvaluationRequest(); + request.setDrillId(1L); + + assertThrows(BusinessException.class, () -> emergencyDrillService.evaluate(request)); + } + + @Test + void testGetDrillStatistics() { + when(drillMapper.selectCount(any())).thenReturn(10L, 5L, 3L, 2L); + when(evaluationMapper.selectList(any())).thenReturn(Collections.emptyList()); + + Map stats = emergencyDrillService.getDrillStatistics(); + + assertNotNull(stats); + assertEquals(10L, stats.get("totalDrills")); + assertEquals(5L, stats.get("completedDrills")); + assertEquals(3L, stats.get("evaluatedDrills")); + assertEquals(2L, stats.get("plannedDrills")); + } + + @Test + void testGetDrillStatisticsWithEvaluations() { + when(drillMapper.selectCount(any())).thenReturn(5L, 3L, 2L, 0L); + + DrillEvaluation eval1 = new DrillEvaluation(); + eval1.setOverallScore(85); + DrillEvaluation eval2 = new DrillEvaluation(); + eval2.setOverallScore(75); + when(evaluationMapper.selectList(any())).thenReturn(List.of(eval1, eval2)); + + Map stats = emergencyDrillService.getDrillStatistics(); + + assertEquals(80.0, stats.get("averageScore")); + } + + private EmergencyDrill buildDrill(Long id, String status) { + EmergencyDrill drill = new EmergencyDrill(); + drill.setId(id); + drill.setDrillNo("DRILL-TEST-001"); + drill.setName("测试演练"); + drill.setDrillType("PIPE_BURST"); + drill.setScenario("测试场景"); + drill.setStatus(status); + drill.setOrganizerId(1L); + drill.setOrganizerName("张三"); + return drill; + } +} diff --git a/wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java b/wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java new file mode 100644 index 00000000..f259032c --- /dev/null +++ b/wm-dispatch/src/test/java/com/water/dispatch/service/PipeBurstServiceTest.java @@ -0,0 +1,164 @@ +package com.water.dispatch.service; + +import com.water.common.core.exception.BusinessException; +import com.water.dispatch.entity.PipeBurstSimulation; +import com.water.dispatch.entity.dto.PipeBurstRequest; +import com.water.dispatch.mapper.PipeBurstSimulationMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PipeBurstServiceTest { + + @Mock + private PipeBurstSimulationMapper simulationMapper; + + @InjectMocks + private PipeBurstService pipeBurstService; + + @Test + void testSimulatePipeBurst() { + when(simulationMapper.insert(any())).thenAnswer(invocation -> { + PipeBurstSimulation sim = invocation.getArgument(0); + sim.setId(1L); + return 1; + }); + when(simulationMapper.updateById(any())).thenReturn(1); + + PipeBurstRequest request = new PipeBurstRequest(); + request.setLongitude(116.404); + request.setLatitude(39.915); + request.setLocation("北京市朝阳区建国路100号"); + request.setPipeDiameter(400.0); + request.setPipeMaterial("球墨铸铁"); + request.setPipePressure(0.35); + request.setCreatorId(1L); + request.setCreatorName("张三"); + + PipeBurstSimulation result = pipeBurstService.simulate(request); + + assertNotNull(result.getSimulationNo()); + assertTrue(result.getSimulationNo().startsWith("PBS-")); + assertEquals("COMPLETED", result.getStatus()); + assertNotNull(result.getImpactRadius()); + assertTrue(result.getImpactRadius() > 0); + assertNotNull(result.getAffectedUsers()); + assertTrue(result.getAffectedUsers() > 0); + assertNotNull(result.getEstimatedRepairHours()); + assertTrue(result.getEstimatedRepairHours() > 0); + assertNotNull(result.getValveShutdownPlan()); + assertNotNull(result.getLeakageRate()); + + verify(simulationMapper).insert(any()); + verify(simulationMapper, atLeastOnce()).updateById(any()); + } + + @Test + void testSimulateMissingCoordinates() { + PipeBurstRequest request = new PipeBurstRequest(); + request.setLocation("测试位置"); + + assertThrows(BusinessException.class, () -> pipeBurstService.simulate(request)); + } + + @Test + void testSimulateDefaultValues() { + when(simulationMapper.insert(any())).thenReturn(1); + when(simulationMapper.updateById(any())).thenReturn(1); + + PipeBurstRequest request = new PipeBurstRequest(); + request.setLongitude(120.0); + request.setLatitude(30.0); + // 不设置pipeDiameter和pipePressure,使用默认值 + + PipeBurstSimulation result = pipeBurstService.simulate(request); + + assertNotNull(result); + assertEquals("COMPLETED", result.getStatus()); + assertNotNull(result.getImpactRadius()); + } + + @Test + void testGetImpactAnalysis() { + PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED"); + when(simulationMapper.selectById(1L)).thenReturn(sim); + + Map analysis = pipeBurstService.getImpactAnalysis(1L); + + assertNotNull(analysis); + assertEquals("PBS-TEST-001", analysis.get("simulationNo")); + assertTrue(analysis.containsKey("impactRadius")); + assertTrue(analysis.containsKey("affectedUsers")); + assertTrue(analysis.containsKey("valveShutdownPlan")); + } + + @Test + void testGetValvePlan() { + PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED"); + when(simulationMapper.selectById(1L)).thenReturn(sim); + + Map plan = pipeBurstService.getValvePlan(1L); + + assertNotNull(plan); + assertEquals("PBS-TEST-001", plan.get("pipeNo")); + assertTrue(plan.containsKey("valveShutdownPlan")); + assertTrue(plan.containsKey("recommendation")); + } + + @Test + void testArchiveSimulation() { + PipeBurstSimulation sim = buildSimulation(1L, "COMPLETED"); + when(simulationMapper.selectById(1L)).thenReturn(sim); + when(simulationMapper.updateById(any())).thenReturn(1); + + PipeBurstSimulation result = pipeBurstService.archive(1L); + + assertEquals("ARCHIVED", result.getStatus()); + verify(simulationMapper).updateById(any()); + } + + @Test + void testArchiveNonCompletedSimulation() { + PipeBurstSimulation sim = buildSimulation(1L, "RUNNING"); + when(simulationMapper.selectById(1L)).thenReturn(sim); + + assertThrows(BusinessException.class, () -> pipeBurstService.archive(1L)); + } + + @Test + void testGetByIdNotFound() { + when(simulationMapper.selectById(999L)).thenReturn(null); + assertThrows(BusinessException.class, () -> pipeBurstService.getById(999L)); + } + + private PipeBurstSimulation buildSimulation(Long id, String status) { + PipeBurstSimulation sim = new PipeBurstSimulation(); + sim.setId(id); + sim.setSimulationNo("PBS-TEST-001"); + sim.setPipeNo("PIPE-001"); + sim.setLongitude(116.404); + sim.setLatitude(39.915); + sim.setLocation("测试位置"); + sim.setPipeDiameter(300.0); + sim.setPipePressure(0.3); + sim.setImpactRadius(150.0); + sim.setImpactArea(70685.83); + sim.setAffectedUsers(354); + sim.setAffectedRegion("测试区域"); + sim.setLeakageRate(100.0); + sim.setEstimatedRepairHours(6.0); + sim.setValveShutdownPlan("[{valveId: V-001}]"); + sim.setValveAffectedUsers(637); + sim.setStatus(status); + return sim; + } +} diff --git a/wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java b/wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java new file mode 100644 index 00000000..47d04efb --- /dev/null +++ b/wm-dispatch/src/test/java/com/water/dispatch/service/WaterQualityServiceTest.java @@ -0,0 +1,186 @@ +package com.water.dispatch.service; + +import com.water.common.core.exception.BusinessException; +import com.water.dispatch.entity.WaterQualityIncident; +import com.water.dispatch.entity.dto.WaterQualityRequest; +import com.water.dispatch.mapper.EmergencyPlanMapper; +import com.water.dispatch.mapper.WaterQualityIncidentMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class WaterQualityServiceTest { + + @Mock + private WaterQualityIncidentMapper incidentMapper; + @Mock + private EmergencyPlanMapper planMapper; + + @InjectMocks + private WaterQualityService waterQualityService; + + @Test + void testReportIncident() { + when(incidentMapper.insert(any())).thenAnswer(invocation -> { + WaterQualityIncident inc = invocation.getArgument(0); + inc.setId(1L); + return 1; + }); + when(incidentMapper.updateById(any())).thenReturn(1); + when(planMapper.selectList(any())).thenReturn(Collections.emptyList()); + + WaterQualityRequest request = new WaterQualityRequest(); + request.setTitle("末梢水浊度超标"); + request.setDescription("某小区末梢水浊度异常"); + request.setSourceType("END"); + request.setAbnormalIndicator("TURBIDITY"); + request.setDetectedValue(3.5); + request.setStandardValue(1.0); + request.setDetectedTime(LocalDateTime.now()); + request.setCreatorId(1L); + request.setCreatorName("张三"); + + WaterQualityIncident result = waterQualityService.report(request); + + assertNotNull(result.getIncidentNo()); + assertTrue(result.getIncidentNo().startsWith("WQI-")); + assertEquals("DETECTED", result.getStatus()); + assertEquals(3.5, result.getExceedMultiple()); + assertNotNull(result.getSeverityLevel()); + assertNotNull(result.getWarningMessage()); + + verify(incidentMapper).insert(any()); + } + + @Test + void testReportMissingIndicator() { + WaterQualityRequest request = new WaterQualityRequest(); + request.setTitle("测试"); + + assertThrows(BusinessException.class, () -> waterQualityService.report(request)); + } + + @Test + void testConfirmIncident() { + WaterQualityIncident incident = buildIncident(1L, "DETECTED"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + when(incidentMapper.updateById(any())).thenReturn(1); + + WaterQualityIncident result = waterQualityService.confirm(1L); + + assertEquals("CONFIRMED", result.getStatus()); + assertNotNull(result.getConfirmedTime()); + } + + @Test + void testConfirmWrongStatus() { + WaterQualityIncident incident = buildIncident(1L, "HANDLING"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + + assertThrows(BusinessException.class, () -> waterQualityService.confirm(1L)); + } + + @Test + void testStartHandling() { + WaterQualityIncident incident = buildIncident(1L, "CONFIRMED"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + when(incidentMapper.updateById(any())).thenReturn(1); + when(planMapper.selectList(any())).thenReturn(Collections.emptyList()); + + WaterQualityIncident result = waterQualityService.startHandling(1L, 2L, "李四"); + + assertEquals("HANDLING", result.getStatus()); + assertEquals(2L, result.getHandlerId()); + assertEquals("李四", result.getHandlerName()); + assertNotNull(result.getHandlingStartTime()); + assertNotNull(result.getHandlingMeasures()); + assertEquals(10, result.getHandlingProgress()); + } + + @Test + void testUpdateProgress() { + WaterQualityIncident incident = buildIncident(1L, "HANDLING"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + when(incidentMapper.updateById(any())).thenReturn(1); + + WaterQualityIncident result = waterQualityService.updateProgress(1L, 50, "管网冲洗完成"); + + assertEquals(50, result.getHandlingProgress()); + assertEquals("HANDLING", result.getStatus()); + } + + @Test + void testUpdateProgressToComplete() { + WaterQualityIncident incident = buildIncident(1L, "HANDLING"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + when(incidentMapper.updateById(any())).thenReturn(1); + + WaterQualityIncident result = waterQualityService.updateProgress(1L, 100, "全部完成"); + + assertEquals(100, result.getHandlingProgress()); + assertEquals("RESOLVED", result.getStatus()); + assertNotNull(result.getResolvedTime()); + } + + @Test + void testResolveIncident() { + WaterQualityIncident incident = buildIncident(1L, "HANDLING"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + when(incidentMapper.updateById(any())).thenReturn(1); + + WaterQualityIncident result = waterQualityService.resolve(1L, "水质恢复正常"); + + assertEquals("RESOLVED", result.getStatus()); + assertNotNull(result.getResolvedTime()); + } + + @Test + void testResolveAlreadyResolved() { + WaterQualityIncident incident = buildIncident(1L, "RESOLVED"); + when(incidentMapper.selectById(1L)).thenReturn(incident); + + assertThrows(BusinessException.class, () -> waterQualityService.resolve(1L, "test")); + } + + @Test + void testGetHandlingTimeline() { + WaterQualityIncident incident = buildIncident(1L, "HANDLING"); + incident.setDetectedTime(LocalDateTime.of(2024, 1, 1, 8, 0)); + incident.setConfirmedTime(LocalDateTime.of(2024, 1, 1, 9, 0)); + incident.setHandlingStartTime(LocalDateTime.of(2024, 1, 1, 10, 0)); + when(incidentMapper.selectById(1L)).thenReturn(incident); + + Map timeline = waterQualityService.getHandlingTimeline(1L); + + assertNotNull(timeline); + assertEquals("WQI-TEST-001", timeline.get("incidentNo")); + assertEquals("HANDLING", timeline.get("status")); + assertNotNull(timeline.get("timeline")); + } + + private WaterQualityIncident buildIncident(Long id, String status) { + WaterQualityIncident incident = new WaterQualityIncident(); + incident.setId(id); + incident.setIncidentNo("WQI-TEST-001"); + incident.setTitle("浊度超标"); + incident.setAbnormalIndicator("TURBIDITY"); + incident.setDetectedValue(3.5); + incident.setStandardValue(1.0); + incident.setExceedMultiple(3.5); + incident.setSeverityLevel("LEVEL_2"); + incident.setStatus(status); + incident.setHandlingProgress(0); + return incident; + } +}