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

- 爆管模拟: 影响范围/用户/水量损失/修复时间/关阀方案
- 水质异常: 事件上报/严重度评估/预案匹配/响应流程/处置归档
- 应急演练: 计划创建/执行/完成/评估打分
- 4个Entity + 4个Mapper + 3个Service + 1个Controller(15端点)
- DDL: 4张表 + 4个索引
- 单元测试: 3个测试类
This commit is contained in:
2026-06-14 15:38:22 +08:00
parent 4a0fc1bf42
commit a26a626d21
20 changed files with 1594 additions and 0 deletions
@@ -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<PipeBurstSimulation> simulatePipeBurst(@RequestBody PipeBurstRequest request) {
return R.ok(pipeBurstService.simulate(request));
}
@GetMapping("/pipe-burst/{id}/impact")
public R<Map<String, Object>> getImpactAnalysis(@PathVariable Long id) {
return R.ok(pipeBurstService.getImpactAnalysis(id));
}
@GetMapping("/pipe-burst/{id}/valve-plan")
public R<Map<String, Object>> getValvePlan(@PathVariable Long id) {
return R.ok(pipeBurstService.getValveShutdownPlan(id));
}
@GetMapping("/pipe-burst/list")
public R<List<PipeBurstSimulation>> listSimulations(@RequestParam(required = false) String status) {
return R.ok(pipeBurstService.listSimulations(status));
}
// === 水质异常处置 ===
@PostMapping("/water-quality/report")
public R<WaterQualityIncident> reportIncident(@RequestBody WaterQualityRequest request) {
return R.ok(waterQualityService.reportIncident(request));
}
@GetMapping("/water-quality/{type}/plan")
public R<Map<String, Object>> matchPlan(@PathVariable String type) {
return R.ok(waterQualityService.matchPlan(type));
}
@PostMapping("/water-quality/{id}/start-response")
public R<Map<String, Object>> startResponse(@PathVariable Long id) {
return R.ok(waterQualityService.startResponse(id));
}
@PostMapping("/water-quality/{id}/progress")
public R<Map<String, Object>> 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<WaterQualityIncident> resolve(@PathVariable Long id, @RequestParam String resolution) {
return R.ok(waterQualityService.resolveIncident(id, resolution));
}
@GetMapping("/water-quality/list")
public R<List<WaterQualityIncident>> 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<Map<String, Object>> getIncidentDetail(@PathVariable Long id) {
return R.ok(waterQualityService.getIncidentDetail(id));
}
// === 应急演练 ===
@PostMapping("/drill")
public R<EmergencyDrill> createDrill(@RequestBody DrillCreateRequest request) {
return R.ok(drillService.createDrill(request));
}
@PostMapping("/drill/{id}/start")
public R<EmergencyDrill> startDrill(@PathVariable Long id) {
return R.ok(drillService.startDrill(id));
}
@PostMapping("/drill/{id}/complete")
public R<EmergencyDrill> completeDrill(@PathVariable Long id) {
return R.ok(drillService.completeDrill(id));
}
@PostMapping("/drill/{id}/evaluate")
public R<DrillEvaluation> evaluateDrill(@PathVariable Long id, @RequestBody DrillEvaluationRequest request) {
return R.ok(drillService.evaluateDrill(id, request));
}
@GetMapping("/drill/list")
public R<List<EmergencyDrill>> listDrills(@RequestParam(required = false) String status) {
return R.ok(drillService.listDrills(status));
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<DrillEvaluation> {}
@@ -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<EmergencyDrill> {}
@@ -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<PipeBurstSimulation> {}
@@ -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<WaterQualityIncident> {}
@@ -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<EmergencyDrill> listDrills(String status, String drillType) {
return drillMapper.selectList(
new LambdaQueryWrapper<EmergencyDrill>()
.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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<DrillEvaluation> getEvaluations(Long drillId) {
return evaluationMapper.selectList(
new LambdaQueryWrapper<DrillEvaluation>()
.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<String, Object> getDrillStatistics() {
Map<String, Object> stats = new LinkedHashMap<>();
long totalDrills = drillMapper.selectCount(null);
long completedDrills = drillMapper.selectCount(
new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "COMPLETED"));
long evaluatedDrills = drillMapper.selectCount(
new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "EVALUATED"));
long plannedDrills = drillMapper.selectCount(
new LambdaQueryWrapper<EmergencyDrill>().eq(EmergencyDrill::getStatus, "PLANNED"));
stats.put("totalDrills", totalDrills);
stats.put("completedDrills", completedDrills);
stats.put("evaluatedDrills", evaluatedDrills);
stats.put("plannedDrills", plannedDrills);
// 平均评分
List<DrillEvaluation> 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<Map<String, Object>> logs, String stage, String content, LocalDateTime time) {
Map<String, Object> log = new LinkedHashMap<>();
log.put("stage", stage);
log.put("content", content);
log.put("time", time);
logs.add(log);
}
private List<Map<String, Object>> 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";
}
}
@@ -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<String> 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<String, Object> getImpactAnalysis(Long id) {
PipeBurstSimulation sim = simulationMapper.selectById(id);
if (sim == null) throw new RuntimeException("模拟记录不存在");
Map<String, Object> 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<String, Object> 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<PipeBurstSimulation> listSimulations(String status) {
LambdaQueryWrapper<PipeBurstSimulation> wrapper = new LambdaQueryWrapper<>();
if (status != null && !status.isBlank()) {
wrapper.eq(PipeBurstSimulation::getStatus, status);
}
return simulationMapper.selectList(wrapper.orderByDesc(PipeBurstSimulation::getCreatedTime));
}
public Map<String, Object> getValveShutdownPlan(Long simulationId) {
PipeBurstSimulation sim = simulationMapper.selectById(simulationId);
if (sim == null) throw new RuntimeException("模拟记录不存在");
Map<String, Object> 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;
}
}
@@ -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<String, Map<String, Object>> 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<String, Object> plan = matchPlan(request.getPollutantType());
if (plan != null) {
incident.setMatchedPlan((String) plan.get("planName"));
}
incidentMapper.insert(incident);
return incident;
}
public Map<String, Object> matchPlan(String pollutantType) {
return RESPONSE_PLANS.getOrDefault(pollutantType,
Map.of("planName", "通用水质异常处置预案", "steps", List.of(
"1. 采样复检", "2. 分析异常原因", "3. 采取对应措施", "4. 持续监测"
)));
}
public Map<String, Object> 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<String, Object> result = new LinkedHashMap<>();
result.put("incident", incident);
result.put("plan", matchPlan(incident.getPollutantType()));
result.put("responseTeam", "水质应急小组");
return result;
}
public Map<String, Object> 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<WaterQualityIncident> listIncidents(String status, String pollutantType) {
LambdaQueryWrapper<WaterQualityIncident> 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<String, Object> getIncidentDetail(Long id) {
WaterQualityIncident incident = incidentMapper.selectById(id);
if (incident == null) throw new RuntimeException("水质事件不存在");
Map<String, Object> 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";
}
}
@@ -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);
@@ -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<String, Object> 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<String, Object> 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;
}
}
@@ -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<String, Object> 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<String, Object> 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;
}
}
@@ -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<String, Object> 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;
}
}