3 Commits
Author SHA1 Message Date
bot_dev2 08649f027e feat(wm-data-engine): #71 历史数据回溯与报表生成
- HistoryDataService: 水量/水质历史数据分页查询 + 导出
- ReportService: 日报/周报/月报/年报自动生成 + 发布 + 模板管理
- StatisticsService: 同比/环比/趋势分析 + 综合看板
- 5个Entity + 5个Mapper + 3个Service + 1个Controller(12+端点)
- DDL: 4张表 + 5个索引
- 单元测试: 3个测试类
2026-06-14 15:38:41 +08:00
bot_dev2 a26a626d21 feat(wm-dispatch): #70 应急推演(爆管模拟+水质异常+演练管理)
- 爆管模拟: 影响范围/用户/水量损失/修复时间/关阀方案
- 水质异常: 事件上报/严重度评估/预案匹配/响应流程/处置归档
- 应急演练: 计划创建/执行/完成/评估打分
- 4个Entity + 4个Mapper + 3个Service + 1个Controller(15端点)
- DDL: 4张表 + 4个索引
- 单元测试: 3个测试类
2026-06-14 15:38:22 +08:00
bot_dev2 4a0fc1bf42 feat(wm-config): #72 阈值管理+信息发布+设备管理 2026-06-14 15:37:16 +08:00
63 changed files with 4354 additions and 0 deletions
+1
View File
@@ -49,6 +49,7 @@
<module>wm-dispatch</module> <module>wm-dispatch</module>
<module>wm-system</module> <module>wm-system</module>
<module>wm-mobile-app</module> <module>wm-mobile-app</module>
<module>wm-config</module>
</modules> </modules>
<dependencyManagement> <dependencyManagement>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>com.water</groupId><artifactId>wm-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
<artifactId>wm-config</artifactId>
<dependencies>
<dependency><groupId>com.water</groupId><artifactId>wm-common</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency>
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId></dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
</dependencies>
</project>
@@ -0,0 +1,15 @@
package com.water.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.water.config.mapper")
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
@@ -0,0 +1,68 @@
package com.water.config.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.config.entity.Announcement;
import com.water.config.service.AnnouncementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "公告通知管理")
@RestController
@RequestMapping("/api/config/announcement")
@RequiredArgsConstructor
public class AnnouncementController {
private final AnnouncementService announcementService;
@Operation(summary = "分页查询公告")
@GetMapping("/list")
public R<Page<Announcement>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) Integer type,
@RequestParam(required = false) Integer publishStatus) {
return R.ok(announcementService.pageAnnouncements(page, size, type, publishStatus));
}
@Operation(summary = "获取公告详情")
@GetMapping("/{id}")
public R<Announcement> getById(@PathVariable Long id) {
return R.ok(announcementService.getById(id));
}
@Operation(summary = "创建公告(草稿)")
@PostMapping
public R<Announcement> create(@RequestBody Announcement announcement) {
return R.ok(announcementService.createAnnouncement(announcement));
}
@Operation(summary = "更新公告")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody Announcement announcement) {
announcementService.updateAnnouncement(id, announcement);
return R.ok("更新成功");
}
@Operation(summary = "发布公告")
@PostMapping("/{id}/publish")
public R<String> publish(@PathVariable Long id) {
announcementService.publish(id);
return R.ok("发布成功");
}
@Operation(summary = "撤回公告")
@PostMapping("/{id}/withdraw")
public R<String> withdraw(@PathVariable Long id) {
announcementService.withdraw(id);
return R.ok("撤回成功");
}
@Operation(summary = "删除公告")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
announcementService.deleteAnnouncement(id);
return R.ok("删除成功");
}
}
@@ -0,0 +1,98 @@
package com.water.config.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.config.entity.DeviceInfo;
import com.water.config.entity.DeviceMaintenance;
import com.water.config.service.DeviceManageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "设备管理")
@RestController
@RequestMapping("/api/config/device")
@RequiredArgsConstructor
public class DeviceManageController {
private final DeviceManageService deviceManageService;
@Operation(summary = "分页查询设备")
@GetMapping("/list")
public R<Page<DeviceInfo>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String deviceName,
@RequestParam(required = false) Integer category,
@RequestParam(required = false) Integer deviceStatus) {
return R.ok(deviceManageService.pageDevices(page, size, deviceName, category, deviceStatus));
}
@Operation(summary = "获取设备详情")
@GetMapping("/{id}")
public R<DeviceInfo> getById(@PathVariable Long id) {
return R.ok(deviceManageService.getById(id));
}
@Operation(summary = "创建设备")
@PostMapping
public R<DeviceInfo> create(@RequestBody DeviceInfo device) {
return R.ok(deviceManageService.createDevice(device));
}
@Operation(summary = "更新设备")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody DeviceInfo device) {
deviceManageService.updateDevice(id, device);
return R.ok("更新成功");
}
@Operation(summary = "删除设备")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
deviceManageService.removeById(id);
return R.ok("删除成功");
}
@Operation(summary = "更新设备状态")
@PutMapping("/{id}/status")
public R<String> updateStatus(@PathVariable Long id, @RequestParam Integer deviceStatus) {
deviceManageService.updateDeviceStatus(id, deviceStatus);
return R.ok("状态更新成功");
}
@Operation(summary = "按分类查询设备")
@GetMapping("/category/{category}")
public R<List<DeviceInfo>> getByCategory(@PathVariable Integer category) {
return R.ok(deviceManageService.getDevicesByCategory(category));
}
@Operation(summary = "按状态查询设备")
@GetMapping("/status/{status}")
public R<List<DeviceInfo>> getByStatus(@PathVariable Integer status) {
return R.ok(deviceManageService.getDevicesByStatus(status));
}
@Operation(summary = "添加维保记录")
@PostMapping("/maintenance")
public R<DeviceMaintenance> addMaintenance(@RequestBody DeviceMaintenance maintenance) {
return R.ok(deviceManageService.addMaintenance(maintenance));
}
@Operation(summary = "查询维保记录")
@GetMapping("/maintenance")
public R<Page<DeviceMaintenance>> pageMaintenances(@RequestParam(required = false) Long deviceId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return R.ok(deviceManageService.pageMaintenances(deviceId, page, size));
}
@Operation(summary = "更新维保记录")
@PutMapping("/maintenance/{id}")
public R<String> updateMaintenance(@PathVariable Long id, @RequestBody DeviceMaintenance maintenance) {
deviceManageService.updateMaintenance(id, maintenance);
return R.ok("更新成功");
}
}
@@ -0,0 +1,84 @@
package com.water.config.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.config.entity.ThresholdChangeLog;
import com.water.config.entity.ThresholdConfig;
import com.water.config.service.ThresholdService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "阈值管理")
@RestController
@RequestMapping("/api/config/threshold")
@RequiredArgsConstructor
public class ThresholdController {
private final ThresholdService thresholdService;
@Operation(summary = "分页查询阈值配置")
@GetMapping("/list")
public R<Page<ThresholdConfig>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String metricCode,
@RequestParam(required = false) Integer level) {
return R.ok(thresholdService.pageThresholds(page, size, metricCode, level));
}
@Operation(summary = "获取阈值详情")
@GetMapping("/{id}")
public R<ThresholdConfig> getById(@PathVariable Long id) {
return R.ok(thresholdService.getById(id));
}
@Operation(summary = "创建阈值配置")
@PostMapping
public R<ThresholdConfig> create(@RequestBody ThresholdConfig config) {
return R.ok(thresholdService.createThreshold(config));
}
@Operation(summary = "更新阈值配置")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody ThresholdConfig config) {
thresholdService.updateThreshold(id, config);
return R.ok("更新成功");
}
@Operation(summary = "删除阈值配置")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
thresholdService.deleteThreshold(id);
return R.ok("删除成功");
}
@Operation(summary = "启用/禁用阈值")
@PutMapping("/{id}/status")
public R<String> toggleStatus(@PathVariable Long id, @RequestParam Integer status) {
thresholdService.toggleStatus(id, status);
return R.ok(status == 1 ? "已启用" : "已禁用");
}
@Operation(summary = "获取指标的全局阈值(多级)")
@GetMapping("/global/{metricCode}")
public R<List<ThresholdConfig>> getGlobalThresholds(@PathVariable String metricCode) {
return R.ok(thresholdService.getGlobalThresholds(metricCode));
}
@Operation(summary = "获取设备阈值配置")
@GetMapping("/device/{deviceId}")
public R<List<ThresholdConfig>> getDeviceThresholds(@PathVariable Long deviceId) {
return R.ok(thresholdService.getDeviceThresholds(deviceId));
}
@Operation(summary = "获取阈值变更历史")
@GetMapping("/history")
public R<Page<ThresholdChangeLog>> getChangeHistory(@RequestParam(required = false) Long thresholdId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return R.ok(thresholdService.getChangeHistory(thresholdId, page, size));
}
}
@@ -0,0 +1,35 @@
package com.water.config.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 公告通知
*/
@Data
@TableName("config_announcement")
public class Announcement {
@TableId(type = IdType.AUTO)
private Long id;
/** 标题 */
private String title;
/** 内容 */
private String content;
/** 类型: 1-系统公告 2-维护通知 3-紧急通知 */
private Integer type;
/** 发布状态: 0-草稿 1-已发布 2-已撤回 */
private Integer publishStatus;
/** 发布渠道(JSON数组): ["sms","push","site"] */
private String channels;
/** 发布人 */
private String publisher;
/** 发布时间 */
private LocalDateTime publishTime;
/** 撤回时间 */
private LocalDateTime withdrawTime;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,45 @@
package com.water.config.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 设备台账
*/
@Data
@TableName("config_device_info")
public class DeviceInfo {
@TableId(type = IdType.AUTO)
private Long id;
/** 设备编码 */
private String deviceCode;
/** 设备名称 */
private String deviceName;
/** 设备分类: 1-水表 2-压力传感器 3-流量计 4-水质监测仪 5-阀门 9-其他 */
private Integer category;
/** 品牌 */
private String brand;
/** 型号 */
private String model;
/** 安装位置 */
private String location;
/** 经度 */
private Double longitude;
/** 纬度 */
private Double latitude;
/** 设备状态: 0-离线 1-在线 2-故障 3-维修中 */
private Integer deviceStatus;
/** 安装日期 */
private LocalDateTime installDate;
/** 最后维护时间 */
private LocalDateTime lastMaintenanceTime;
/** 负责人 */
private String responsiblePerson;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,37 @@
package com.water.config.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 设备维保记录
*/
@Data
@TableName("config_device_maintenance")
public class DeviceMaintenance {
@TableId(type = IdType.AUTO)
private Long id;
/** 设备ID */
private Long deviceId;
/** 维保类型: 1-日常巡检 2-定期保养 3-故障维修 4-更换配件 */
private Integer maintenanceType;
/** 维保描述 */
private String description;
/** 维保人 */
private String operator;
/** 维保开始时间 */
private LocalDateTime startTime;
/** 维保结束时间 */
private LocalDateTime endTime;
/** 维保结果: 0-未完成 1-已完成 2-需要返修 */
private Integer result;
/** 费用 */
private Double cost;
/** 附件(JSON) */
private String attachments;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,35 @@
package com.water.config.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 阈值变更记录
*/
@Data
@TableName("config_threshold_change_log")
public class ThresholdChangeLog {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联阈值ID */
private Long thresholdId;
/** 变更前最小值 */
private BigDecimal oldMinValue;
/** 变更前最大值 */
private BigDecimal oldMaxValue;
/** 变更后最小值 */
private BigDecimal newMinValue;
/** 变更后最大值 */
private BigDecimal newMaxValue;
/** 变更前级别 */
private Integer oldLevel;
/** 变更后级别 */
private Integer newLevel;
/** 变更人 */
private String operator;
/** 变更原因 */
private String reason;
private LocalDateTime createdAt;
}
@@ -0,0 +1,38 @@
package com.water.config.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 阈值配置
*/
@Data
@TableName("config_threshold")
public class ThresholdConfig {
@TableId(type = IdType.AUTO)
private Long id;
/** 指标编码 */
private String metricCode;
/** 指标名称 */
private String metricName;
/** 设备ID(可选,null表示全局) */
private Long deviceId;
/** 阈值级别: 1-预警 2-报警 3-紧急 */
private Integer level;
/** 最小值 */
private BigDecimal minValue;
/** 最大值 */
private BigDecimal maxValue;
/** 单位 */
private String unit;
/** 启用状态: 0-禁用 1-启用 */
private Integer status;
/** 备注 */
private String remark;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,9 @@
package com.water.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.config.entity.Announcement;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AnnouncementMapper extends BaseMapper<Announcement> {
}
@@ -0,0 +1,17 @@
package com.water.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.config.entity.DeviceInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface DeviceInfoMapper extends BaseMapper<DeviceInfo> {
@Select("SELECT * FROM config_device_info WHERE device_status = #{status} AND deleted = 0")
List<DeviceInfo> selectByDeviceStatus(Integer status);
@Select("SELECT * FROM config_device_info WHERE category = #{category} AND deleted = 0 ORDER BY device_code")
List<DeviceInfo> selectByCategory(Integer category);
}
@@ -0,0 +1,14 @@
package com.water.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.config.entity.DeviceMaintenance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface DeviceMaintenanceMapper extends BaseMapper<DeviceMaintenance> {
@Select("SELECT * FROM config_device_maintenance WHERE device_id = #{deviceId} AND deleted = 0 ORDER BY start_time DESC")
List<DeviceMaintenance> selectByDeviceId(Long deviceId);
}
@@ -0,0 +1,9 @@
package com.water.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.config.entity.ThresholdChangeLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ThresholdChangeLogMapper extends BaseMapper<ThresholdChangeLog> {
}
@@ -0,0 +1,17 @@
package com.water.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.config.entity.ThresholdConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface ThresholdConfigMapper extends BaseMapper<ThresholdConfig> {
@Select("SELECT * FROM config_threshold WHERE metric_code = #{metricCode} AND device_id IS NULL AND status = 1 AND deleted = 0 ORDER BY level")
List<ThresholdConfig> selectGlobalByMetricCode(String metricCode);
@Select("SELECT * FROM config_threshold WHERE device_id = #{deviceId} AND status = 1 AND deleted = 0 ORDER BY metric_code, level")
List<ThresholdConfig> selectByDeviceId(Long deviceId);
}
@@ -0,0 +1,131 @@
package com.water.config.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.common.core.exception.BusinessException;
import com.water.config.entity.Announcement;
import com.water.config.mapper.AnnouncementMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* 公告通知服务
*/
@Slf4j
@Service
public class AnnouncementService extends ServiceImpl<AnnouncementMapper, Announcement> {
/**
* 分页查询公告
*/
public Page<Announcement> pageAnnouncements(int page, int size, Integer type, Integer publishStatus) {
LambdaQueryWrapper<Announcement> qw = new LambdaQueryWrapper<>();
if (type != null) {
qw.eq(Announcement::getType, type);
}
if (publishStatus != null) {
qw.eq(Announcement::getPublishStatus, publishStatus);
}
qw.orderByDesc(Announcement::getCreatedAt);
return this.page(new Page<>(page, size), qw);
}
/**
* 创建公告(草稿)
*/
public Announcement createAnnouncement(Announcement announcement) {
announcement.setPublishStatus(0);
this.save(announcement);
return announcement;
}
/**
* 更新公告(仅草稿可更新)
*/
public void updateAnnouncement(Long id, Announcement announcement) {
Announcement existing = this.getById(id);
if (existing == null) {
throw new BusinessException("公告不存在");
}
if (existing.getPublishStatus() != 0) {
throw new BusinessException("已发布的公告不可修改");
}
announcement.setId(id);
this.updateById(announcement);
}
/**
* 发布公告
*/
@Transactional
public void publish(Long id) {
Announcement announcement = this.getById(id);
if (announcement == null) {
throw new BusinessException("公告不存在");
}
if (announcement.getPublishStatus() != 0) {
throw new BusinessException("只有草稿状态的公告可以发布");
}
announcement.setPublishStatus(1);
announcement.setPublishTime(LocalDateTime.now());
this.updateById(announcement);
// 多渠道发布
dispatchChannels(announcement);
}
/**
* 撤回公告
*/
@Transactional
public void withdraw(Long id) {
Announcement announcement = this.getById(id);
if (announcement == null) {
throw new BusinessException("公告不存在");
}
if (announcement.getPublishStatus() != 1) {
throw new BusinessException("只有已发布的公告可以撤回");
}
announcement.setPublishStatus(2);
announcement.setWithdrawTime(LocalDateTime.now());
this.updateById(announcement);
}
/**
* 删除公告
*/
public void deleteAnnouncement(Long id) {
Announcement existing = this.getById(id);
if (existing == null) {
throw new BusinessException("公告不存在");
}
if (existing.getPublishStatus() == 1) {
throw new BusinessException("已发布的公告不可删除,请先撤回");
}
this.removeById(id);
}
/**
* 多渠道分发(模拟)
*/
private void dispatchChannels(Announcement announcement) {
String channels = announcement.getChannels();
if (channels == null || channels.isEmpty()) {
log.info("公告 {} 无渠道配置,仅站内信发布", announcement.getId());
return;
}
log.info("公告 {} 发布渠道: {}", announcement.getId(), channels);
if (channels.contains("sms")) {
log.info("→ 短信渠道已触发");
}
if (channels.contains("push")) {
log.info("→ APP推送渠道已触发");
}
if (channels.contains("site")) {
log.info("→ 站内信渠道已触发");
}
}
}
@@ -0,0 +1,150 @@
package com.water.config.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.common.core.exception.BusinessException;
import com.water.config.entity.DeviceInfo;
import com.water.config.entity.DeviceMaintenance;
import com.water.config.mapper.DeviceInfoMapper;
import com.water.config.mapper.DeviceMaintenanceMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 设备管理服务
*/
@Service
@RequiredArgsConstructor
public class DeviceManageService extends ServiceImpl<DeviceInfoMapper, DeviceInfo> {
private final DeviceMaintenanceMapper maintenanceMapper;
/**
* 分页查询设备
*/
public Page<DeviceInfo> pageDevices(int page, int size, String deviceName, Integer category, Integer deviceStatus) {
LambdaQueryWrapper<DeviceInfo> qw = new LambdaQueryWrapper<>();
if (deviceName != null && !deviceName.isEmpty()) {
qw.like(DeviceInfo::getDeviceName, deviceName);
}
if (category != null) {
qw.eq(DeviceInfo::getCategory, category);
}
if (deviceStatus != null) {
qw.eq(DeviceInfo::getDeviceStatus, deviceStatus);
}
qw.orderByDesc(DeviceInfo::getCreatedAt);
return this.page(new Page<>(page, size), qw);
}
/**
* 创建设备
*/
public DeviceInfo createDevice(DeviceInfo device) {
// 检查设备编码唯一性
long count = this.count(new LambdaQueryWrapper<DeviceInfo>()
.eq(DeviceInfo::getDeviceCode, device.getDeviceCode()));
if (count > 0) {
throw new BusinessException("设备编码已存在");
}
this.save(device);
return device;
}
/**
* 更新设备
*/
public void updateDevice(Long id, DeviceInfo device) {
DeviceInfo existing = this.getById(id);
if (existing == null) {
throw new BusinessException("设备不存在");
}
device.setId(id);
this.updateById(device);
}
/**
* 更新设备状态
*/
public void updateDeviceStatus(Long id, Integer deviceStatus) {
DeviceInfo device = this.getById(id);
if (device == null) {
throw new BusinessException("设备不存在");
}
device.setDeviceStatus(deviceStatus);
this.updateById(device);
}
/**
* 按分类查询设备
*/
public List<DeviceInfo> getDevicesByCategory(Integer category) {
return baseMapper.selectByCategory(category);
}
/**
* 按状态查询设备
*/
public List<DeviceInfo> getDevicesByStatus(Integer status) {
return baseMapper.selectByDeviceStatus(status);
}
/**
* 添加维保记录
*/
@Transactional
public DeviceMaintenance addMaintenance(DeviceMaintenance maintenance) {
DeviceInfo device = this.getById(maintenance.getDeviceId());
if (device == null) {
throw new BusinessException("设备不存在");
}
maintenanceMapper.insert(maintenance);
// 如果维保完成,更新设备最后维护时间
if (maintenance.getResult() != null && maintenance.getResult() == 1) {
device.setLastMaintenanceTime(LocalDateTime.now());
if (device.getDeviceStatus() == 3) {
device.setDeviceStatus(1); // 维修中 -> 在线
}
this.updateById(device);
}
return maintenance;
}
/**
* 查询设备维保历史
*/
public Page<DeviceMaintenance> pageMaintenances(Long deviceId, int page, int size) {
LambdaQueryWrapper<DeviceMaintenance> qw = new LambdaQueryWrapper<>();
if (deviceId != null) {
qw.eq(DeviceMaintenance::getDeviceId, deviceId);
}
qw.orderByDesc(DeviceMaintenance::getStartTime);
return maintenanceMapper.selectPage(new Page<>(page, size), qw);
}
/**
* 更新维保记录
*/
public void updateMaintenance(Long id, DeviceMaintenance maintenance) {
maintenance.setId(id);
maintenanceMapper.updateById(maintenance);
}
/**
* 获取设备统计(按分类)
*/
public List<Long> getDeviceCountByCategory() {
// 简化版:返回各分类的设备数量
LambdaQueryWrapper<DeviceInfo> qw = new LambdaQueryWrapper<>();
qw.select(DeviceInfo::getCategory);
qw.groupBy(DeviceInfo::getCategory);
return this.listMaps(qw).stream()
.map(m -> ((Number) m.get("category")).longValue())
.toList();
}
}
@@ -0,0 +1,144 @@
package com.water.config.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.config.entity.ThresholdChangeLog;
import com.water.config.entity.ThresholdConfig;
import com.water.config.mapper.ThresholdChangeLogMapper;
import com.water.config.mapper.ThresholdConfigMapper;
import com.water.common.core.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 阈值管理服务
*/
@Service
@RequiredArgsConstructor
public class ThresholdService extends ServiceImpl<ThresholdConfigMapper, ThresholdConfig> {
private final ThresholdChangeLogMapper changeLogMapper;
/**
* 分页查询阈值配置
*/
public Page<ThresholdConfig> pageThresholds(int page, int size, String metricCode, Integer level) {
LambdaQueryWrapper<ThresholdConfig> qw = new LambdaQueryWrapper<>();
if (metricCode != null && !metricCode.isEmpty()) {
qw.like(ThresholdConfig::getMetricCode, metricCode);
}
if (level != null) {
qw.eq(ThresholdConfig::getLevel, level);
}
qw.orderByAsc(ThresholdConfig::getMetricCode, ThresholdConfig::getLevel);
return this.page(new Page<>(page, size), qw);
}
/**
* 创建阈值配置
*/
@Transactional
public ThresholdConfig createThreshold(ThresholdConfig config) {
validateThreshold(config);
this.save(config);
recordChangeLog(config, null, "新建阈值配置");
return config;
}
/**
* 更新阈值配置(记录变更)
*/
@Transactional
public void updateThreshold(Long id, ThresholdConfig config) {
ThresholdConfig old = this.getById(id);
if (old == null) {
throw new BusinessException("阈值配置不存在");
}
config.setId(id);
validateThreshold(config);
this.updateById(config);
recordChangeLog(config, old, "更新阈值配置");
}
/**
* 删除阈值配置
*/
@Transactional
public void deleteThreshold(Long id) {
ThresholdConfig old = this.getById(id);
if (old == null) {
throw new BusinessException("阈值配置不存在");
}
this.removeById(id);
recordChangeLog(old, old, "删除阈值配置");
}
/**
* 获取某指标的全局阈值(多级)
*/
public List<ThresholdConfig> getGlobalThresholds(String metricCode) {
return baseMapper.selectGlobalByMetricCode(metricCode);
}
/**
* 获取某设备的阈值配置
*/
public List<ThresholdConfig> getDeviceThresholds(Long deviceId) {
return baseMapper.selectByDeviceId(deviceId);
}
/**
* 获取阈值变更历史
*/
public Page<ThresholdChangeLog> getChangeHistory(Long thresholdId, int page, int size) {
LambdaQueryWrapper<ThresholdChangeLog> qw = new LambdaQueryWrapper<>();
if (thresholdId != null) {
qw.eq(ThresholdChangeLog::getThresholdId, thresholdId);
}
qw.orderByDesc(ThresholdChangeLog::getCreatedAt);
return changeLogMapper.selectPage(new Page<>(page, size), qw);
}
/**
* 启用/禁用阈值
*/
public void toggleStatus(Long id, Integer status) {
ThresholdConfig config = this.getById(id);
if (config == null) {
throw new BusinessException("阈值配置不存在");
}
config.setStatus(status);
this.updateById(config);
}
private void validateThreshold(ThresholdConfig config) {
if (config.getMinValue() != null && config.getMaxValue() != null) {
if (config.getMinValue().compareTo(config.getMaxValue()) > 0) {
throw new BusinessException("最小值不能大于最大值");
}
}
if (config.getLevel() != null && (config.getLevel() < 1 || config.getLevel() > 3)) {
throw new BusinessException("阈值级别必须在1-3之间");
}
}
private void recordChangeLog(ThresholdConfig newConfig, ThresholdConfig oldConfig, String reason) {
ThresholdChangeLog log = new ThresholdChangeLog();
log.setThresholdId(newConfig.getId());
if (oldConfig != null) {
log.setOldMinValue(oldConfig.getMinValue());
log.setOldMaxValue(oldConfig.getMaxValue());
log.setOldLevel(oldConfig.getLevel());
}
log.setNewMinValue(newConfig.getMinValue());
log.setNewMaxValue(newConfig.getMaxValue());
log.setNewLevel(newConfig.getLevel());
log.setReason(reason);
log.setOperator("system");
changeLogMapper.insert(log);
}
}
@@ -0,0 +1,30 @@
server:
port: 8090
spring:
application:
name: wm-config
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:water}?currentSchema=public
username: ${DB_USER:postgres}
password: ${DB_PASS:postgres}
cloud:
nacos:
discovery:
server-addr: ${NACOS_ADDR:localhost:8848}
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
sa-token:
token-name: Authorization
timeout: 86400
active-timeout: 1800
+102
View File
@@ -0,0 +1,102 @@
-- ============================================================
-- wm-config DDL: 阈值管理 + 信息发布 + 设备管理
-- ============================================================
-- 阈值配置表
CREATE TABLE IF NOT EXISTS config_threshold (
id BIGSERIAL PRIMARY KEY,
metric_code VARCHAR(64) NOT NULL,
metric_name VARCHAR(128) NOT NULL,
device_id BIGINT,
level SMALLINT NOT NULL DEFAULT 1, -- 1-预警 2-报警 3-紧急
min_value NUMERIC(12,4),
max_value NUMERIC(12,4),
unit VARCHAR(32),
status SMALLINT NOT NULL DEFAULT 1, -- 0-禁用 1-启用
remark VARCHAR(500),
deleted SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE config_threshold IS '阈值配置表';
CREATE INDEX idx_threshold_metric ON config_threshold(metric_code);
CREATE INDEX idx_threshold_device ON config_threshold(device_id);
-- 阈值变更记录表
CREATE TABLE IF NOT EXISTS config_threshold_change_log (
id BIGSERIAL PRIMARY KEY,
threshold_id BIGINT NOT NULL,
old_min_value NUMERIC(12,4),
old_max_value NUMERIC(12,4),
new_min_value NUMERIC(12,4),
new_max_value NUMERIC(12,4),
old_level SMALLINT,
new_level SMALLINT,
operator VARCHAR(64),
reason VARCHAR(500),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE config_threshold_change_log IS '阈值变更记录表';
CREATE INDEX idx_change_log_threshold ON config_threshold_change_log(threshold_id);
-- 公告通知表
CREATE TABLE IF NOT EXISTS config_announcement (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(256) NOT NULL,
content TEXT,
type SMALLINT NOT NULL DEFAULT 1, -- 1-系统公告 2-维护通知 3-紧急通知
publish_status SMALLINT NOT NULL DEFAULT 0, -- 0-草稿 1-已发布 2-已撤回
channels VARCHAR(256), -- JSON: ["sms","push","site"]
publisher VARCHAR(64),
publish_time TIMESTAMP,
withdraw_time TIMESTAMP,
deleted SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE config_announcement IS '公告通知表';
CREATE INDEX idx_announcement_status ON config_announcement(publish_status);
-- 设备台账表
CREATE TABLE IF NOT EXISTS config_device_info (
id BIGSERIAL PRIMARY KEY,
device_code VARCHAR(64) NOT NULL UNIQUE,
device_name VARCHAR(128) NOT NULL,
category SMALLINT NOT NULL DEFAULT 9, -- 1-水表 2-压力传感器 3-流量计 4-水质监测仪 5-阀门 9-其他
brand VARCHAR(64),
model VARCHAR(64),
location VARCHAR(256),
longitude DOUBLE PRECISION,
latitude DOUBLE PRECISION,
device_status SMALLINT NOT NULL DEFAULT 0, -- 0-离线 1-在线 2-故障 3-维修中
install_date TIMESTAMP,
last_maintenance_time TIMESTAMP,
responsible_person VARCHAR(64),
remark VARCHAR(500),
deleted SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE config_device_info IS '设备台账表';
CREATE INDEX idx_device_code ON config_device_info(device_code);
CREATE INDEX idx_device_category ON config_device_info(category);
CREATE INDEX idx_device_status ON config_device_info(device_status);
-- 设备维保记录表
CREATE TABLE IF NOT EXISTS config_device_maintenance (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT NOT NULL,
maintenance_type SMALLINT NOT NULL DEFAULT 1, -- 1-日常巡检 2-定期保养 3-故障维修 4-更换配件
description TEXT,
operator VARCHAR(64),
start_time TIMESTAMP,
end_time TIMESTAMP,
result SMALLINT NOT NULL DEFAULT 0, -- 0-未完成 1-已完成 2-需要返修
cost DOUBLE PRECISION,
attachments TEXT, -- JSON
deleted SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE config_device_maintenance IS '设备维保记录表';
CREATE INDEX idx_maintenance_device ON config_device_maintenance(device_id);
@@ -0,0 +1,100 @@
package com.water.config;
import com.water.common.core.exception.BusinessException;
import com.water.config.entity.Announcement;
import com.water.config.mapper.AnnouncementMapper;
import com.water.config.service.AnnouncementService;
import org.junit.jupiter.api.BeforeEach;
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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class AnnouncementServiceTest {
@Mock
private AnnouncementMapper announcementMapper;
@InjectMocks
private AnnouncementService announcementService;
private Announcement draft;
@BeforeEach
void setUp() {
draft = new Announcement();
draft.setTitle("系统维护通知");
draft.setContent("今晚22:00-次日06:00系统维护");
draft.setType(2);
draft.setChannels("[\"site\",\"sms\"]");
}
@Test
void createAnnouncement_setsDraftStatus() {
when(announcementMapper.insert(any())).thenReturn(1);
Announcement result = announcementService.createAnnouncement(draft);
assertEquals(0, result.getPublishStatus());
verify(announcementMapper).insert(any(Announcement.class));
}
@Test
void publish_draft_success() {
Announcement existing = new Announcement();
existing.setId(1L);
existing.setPublishStatus(0);
existing.setChannels("[\"site\"]");
when(announcementMapper.selectById(1L)).thenReturn(existing);
when(announcementMapper.updateById(any())).thenReturn(1);
assertDoesNotThrow(() -> announcementService.publish(1L));
verify(announcementMapper).updateById(argThat(a ->
a.getPublishStatus() == 1 && a.getPublishTime() != null));
}
@Test
void publish_alreadyPublished_throws() {
Announcement existing = new Announcement();
existing.setId(1L);
existing.setPublishStatus(1);
when(announcementMapper.selectById(1L)).thenReturn(existing);
BusinessException ex = assertThrows(BusinessException.class,
() -> announcementService.publish(1L));
assertEquals("只有草稿状态的公告可以发布", ex.getMessage());
}
@Test
void withdraw_notPublished_throws() {
Announcement existing = new Announcement();
existing.setId(1L);
existing.setPublishStatus(0);
when(announcementMapper.selectById(1L)).thenReturn(existing);
BusinessException ex = assertThrows(BusinessException.class,
() -> announcementService.withdraw(1L));
assertEquals("只有已发布的公告可以撤回", ex.getMessage());
}
@Test
void delete_published_throws() {
Announcement existing = new Announcement();
existing.setId(1L);
existing.setPublishStatus(1);
when(announcementMapper.selectById(1L)).thenReturn(existing);
BusinessException ex = assertThrows(BusinessException.class,
() -> announcementService.deleteAnnouncement(1L));
assertEquals("已发布的公告不可删除,请先撤回", ex.getMessage());
}
}
@@ -0,0 +1,107 @@
package com.water.config;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.common.core.exception.BusinessException;
import com.water.config.entity.DeviceInfo;
import com.water.config.entity.DeviceMaintenance;
import com.water.config.mapper.DeviceInfoMapper;
import com.water.config.mapper.DeviceMaintenanceMapper;
import com.water.config.service.DeviceManageService;
import org.junit.jupiter.api.BeforeEach;
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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DeviceManageServiceTest {
@Mock
private DeviceInfoMapper deviceInfoMapper;
@Mock
private DeviceMaintenanceMapper maintenanceMapper;
@InjectMocks
private DeviceManageService deviceManageService;
private DeviceInfo device;
@BeforeEach
void setUp() {
device = new DeviceInfo();
device.setDeviceCode("WM-001");
device.setDeviceName("1号水表");
device.setCategory(1);
device.setBrand("海天");
device.setModel("HT-200");
device.setDeviceStatus(0);
}
@Test
void createDevice_success() {
when(deviceInfoMapper.selectCount(any())).thenReturn(0L);
when(deviceInfoMapper.insert(any())).thenReturn(1);
DeviceInfo result = deviceManageService.createDevice(device);
assertNotNull(result);
assertEquals("WM-001", result.getDeviceCode());
verify(deviceInfoMapper).insert(any(DeviceInfo.class));
}
@Test
void createDevice_duplicateCode_throws() {
when(deviceInfoMapper.selectCount(any())).thenReturn(1L);
BusinessException ex = assertThrows(BusinessException.class,
() -> deviceManageService.createDevice(device));
assertEquals("设备编码已存在", ex.getMessage());
}
@Test
void updateDevice_notFound_throws() {
when(deviceInfoMapper.selectById(999L)).thenReturn(null);
BusinessException ex = assertThrows(BusinessException.class,
() -> deviceManageService.updateDevice(999L, device));
assertEquals("设备不存在", ex.getMessage());
}
@Test
void addMaintenance_deviceNotFound_throws() {
DeviceMaintenance maintenance = new DeviceMaintenance();
maintenance.setDeviceId(999L);
when(deviceInfoMapper.selectById(999L)).thenReturn(null);
BusinessException ex = assertThrows(BusinessException.class,
() -> deviceManageService.addMaintenance(maintenance));
assertEquals("设备不存在", ex.getMessage());
}
@Test
void addMaintenance_completed_updatesDeviceTime() {
DeviceInfo existingDevice = new DeviceInfo();
existingDevice.setId(1L);
existingDevice.setDeviceStatus(3); // 维修中
DeviceMaintenance maintenance = new DeviceMaintenance();
maintenance.setDeviceId(1L);
maintenance.setResult(1); // 已完成
maintenance.setDescription("更换电池");
when(deviceInfoMapper.selectById(1L)).thenReturn(existingDevice);
when(maintenanceMapper.insert(any())).thenReturn(1);
when(deviceInfoMapper.updateById(any())).thenReturn(1);
DeviceMaintenance result = deviceManageService.addMaintenance(maintenance);
assertNotNull(result);
verify(deviceInfoMapper).updateById(argThat(d ->
d.getLastMaintenanceTime() != null && d.getDeviceStatus() == 1));
}
}
@@ -0,0 +1,96 @@
package com.water.config;
import com.water.common.core.exception.BusinessException;
import com.water.config.entity.ThresholdChangeLog;
import com.water.config.entity.ThresholdConfig;
import com.water.config.mapper.ThresholdChangeLogMapper;
import com.water.config.mapper.ThresholdConfigMapper;
import com.water.config.service.ThresholdService;
import org.junit.jupiter.api.BeforeEach;
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.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ThresholdServiceTest {
@Mock
private ThresholdConfigMapper thresholdConfigMapper;
@Mock
private ThresholdChangeLogMapper changeLogMapper;
@InjectMocks
private ThresholdService thresholdService;
private ThresholdConfig validConfig;
@BeforeEach
void setUp() {
validConfig = new ThresholdConfig();
validConfig.setMetricCode("water_pressure");
validConfig.setMetricName("水压");
validConfig.setLevel(1);
validConfig.setMinValue(new BigDecimal("0.1"));
validConfig.setMaxValue(new BigDecimal("0.8"));
validConfig.setUnit("MPa");
validConfig.setStatus(1);
}
@Test
void createThreshold_success() {
when(thresholdConfigMapper.insert(any())).thenReturn(1);
when(changeLogMapper.insert(any())).thenReturn(1);
ThresholdConfig result = thresholdService.createThreshold(validConfig);
assertNotNull(result);
assertEquals("water_pressure", result.getMetricCode());
assertEquals(1, result.getLevel());
verify(thresholdConfigMapper).insert(any(ThresholdConfig.class));
verify(changeLogMapper).insert(any(ThresholdChangeLog.class));
}
@Test
void createThreshold_minGreaterThanMax_throws() {
validConfig.setMinValue(new BigDecimal("1.0"));
validConfig.setMaxValue(new BigDecimal("0.5"));
BusinessException ex = assertThrows(BusinessException.class,
() -> thresholdService.createThreshold(validConfig));
assertEquals("最小值不能大于最大值", ex.getMessage());
}
@Test
void createThreshold_invalidLevel_throws() {
validConfig.setLevel(5);
BusinessException ex = assertThrows(BusinessException.class,
() -> thresholdService.createThreshold(validConfig));
assertEquals("阈值级别必须在1-3之间", ex.getMessage());
}
@Test
void updateThreshold_notFound_throws() {
when(thresholdConfigMapper.selectById(999L)).thenReturn(null);
BusinessException ex = assertThrows(BusinessException.class,
() -> thresholdService.updateThreshold(999L, validConfig));
assertEquals("阈值配置不存在", ex.getMessage());
}
@Test
void deleteThreshold_notFound_throws() {
when(thresholdConfigMapper.selectById(999L)).thenReturn(null);
BusinessException ex = assertThrows(BusinessException.class,
() -> thresholdService.deleteThreshold(999L));
assertEquals("阈值配置不存在", ex.getMessage());
}
}
@@ -0,0 +1,287 @@
package com.water.data_engine.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.common.core.result.R;
import com.water.data_engine.entity.*;
import com.water.data_engine.entity.dto.ExportRequest;
import com.water.data_engine.entity.dto.StatisticsResult;
import com.water.data_engine.service.HistoryDataService;
import com.water.data_engine.service.ReportService;
import com.water.data_engine.service.StatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 历史数据回溯与报表生成 API
*/
@Tag(name = "历史数据与报表")
@RestController
@RequestMapping("/api/data")
@RequiredArgsConstructor
public class HistoryReportController {
private final HistoryDataService historyDataService;
private final ReportService reportService;
private final StatisticsService statisticsService;
// ==================== 1. 历史水量数据分页查询 ====================
@Operation(summary = "历史水量数据查询(分页)")
@GetMapping("/history/quantity")
public R<Page<WaterQuantity>> queryQuantityHistory(
@RequestParam(required = false) String area,
@RequestParam(required = false) String pointCode,
@RequestParam(required = false) String deviceSn,
@RequestParam(required = false) LocalDateTime startTime,
@RequestParam(required = false) LocalDateTime endTime,
@RequestParam(required = false) Integer qualityFlag,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize) {
HistoricalQuery query = new HistoricalQuery();
query.setArea(area);
query.setPointCode(pointCode);
query.setDeviceSn(deviceSn);
query.setStartTime(startTime);
query.setEndTime(endTime);
query.setQualityFlag(qualityFlag);
query.setPageNum(pageNum);
query.setPageSize(pageSize);
return R.ok(historyDataService.queryQuantityHistory(query));
}
// ==================== 2. 历史水质数据分页查询 ====================
@Operation(summary = "历史水质数据查询(分页)")
@GetMapping("/history/quality")
public R<Page<WaterQuality>> queryQualityHistory(
@RequestParam(required = false) String area,
@RequestParam(required = false) String pointCode,
@RequestParam(required = false) String deviceSn,
@RequestParam(required = false) LocalDateTime startTime,
@RequestParam(required = false) LocalDateTime endTime,
@RequestParam(required = false) Integer qualityFlag,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize) {
HistoricalQuery query = new HistoricalQuery();
query.setArea(area);
query.setPointCode(pointCode);
query.setDeviceSn(deviceSn);
query.setStartTime(startTime);
query.setEndTime(endTime);
query.setQualityFlag(qualityFlag);
query.setPageNum(pageNum);
query.setPageSize(pageSize);
return R.ok(historyDataService.queryQualityHistory(query));
}
// ==================== 3. 水量区域聚合统计 ====================
@Operation(summary = "水量区域聚合统计")
@GetMapping("/aggregate/quantity/area")
public R<List<Map<String, Object>>> aggregateQuantityByArea(
@RequestParam LocalDateTime startTime,
@RequestParam LocalDateTime endTime,
@RequestParam(required = false) String area) {
return R.ok(historyDataService.aggregateQuantityByArea(startTime, endTime, area));
}
// ==================== 4. 水质区域聚合统计 ====================
@Operation(summary = "水质区域聚合统计")
@GetMapping("/aggregate/quality/area")
public R<List<Map<String, Object>>> aggregateQualityByArea(
@RequestParam LocalDateTime startTime,
@RequestParam LocalDateTime endTime,
@RequestParam(required = false) String area) {
return R.ok(historyDataService.aggregateQualityByArea(startTime, endTime, area));
}
// ==================== 5. 数据导出 ====================
@Operation(summary = "历史数据导出")
@PostMapping("/export")
public R<Map<String, Object>> exportData(@RequestBody ExportRequest request) {
return R.ok(historyDataService.queryForExport(request));
}
// ==================== 6. 生成报表 ====================
@Operation(summary = "自动生成报表")
@PostMapping("/report/generate")
public R<DataReport> generateReport(
@RequestParam String reportType,
@RequestParam String dataType,
@RequestParam(required = false) String area,
@RequestParam(required = false) LocalDate periodStart,
@RequestParam(required = false) LocalDate periodEnd) {
if (periodStart != null && periodEnd != null) {
return R.ok(reportService.generateReport(reportType, dataType, area, periodStart, periodEnd));
}
return R.ok(reportService.generateReport(reportType, dataType, area));
}
// ==================== 7. 报表列表(分页) ====================
@Operation(summary = "报表列表(分页)")
@GetMapping("/report/list")
public R<Page<DataReport>> listReports(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String reportType,
@RequestParam(required = false) String dataType) {
return R.ok(reportService.listReports(pageNum, pageSize, reportType, dataType));
}
// ==================== 8. 报表详情 ====================
@Operation(summary = "报表详情")
@GetMapping("/report/{id}")
public R<DataReport> getReportDetail(@PathVariable Long id) {
return R.ok(reportService.getReportDetail(id));
}
// ==================== 9. 最近报表 ====================
@Operation(summary = "查询最近生成的报表")
@GetMapping("/report/recent")
public R<List<DataReport>> recentReports(
@RequestParam(required = false) String reportType,
@RequestParam(required = false) String dataType,
@RequestParam(defaultValue = "10") int limit) {
return R.ok(reportService.findRecentReports(reportType, dataType, limit));
}
// ==================== 10. 删除报表 ====================
@Operation(summary = "删除报表")
@DeleteMapping("/report/{id}")
public R<Void> deleteReport(@PathVariable Long id) {
reportService.deleteReport(id);
return R.ok();
}
// ==================== 11. 模板列表 ====================
@Operation(summary = "报表模板列表")
@GetMapping("/template/list")
public R<List<ReportTemplate>> listTemplates(
@RequestParam(required = false) String reportType,
@RequestParam(required = false) String dataType) {
return R.ok(reportService.listTemplates(reportType, dataType));
}
// ==================== 12. 模板详情 ====================
@Operation(summary = "报表模板详情")
@GetMapping("/template/{id}")
public R<ReportTemplate> getTemplate(@PathVariable Long id) {
return R.ok(reportService.getTemplate(id));
}
// ==================== 13. 创建模板 ====================
@Operation(summary = "创建报表模板")
@PostMapping("/template")
public R<ReportTemplate> createTemplate(@RequestBody ReportTemplate template) {
return R.ok(reportService.createTemplate(template));
}
// ==================== 14. 更新模板 ====================
@Operation(summary = "更新报表模板")
@PutMapping("/template/{id}")
public R<ReportTemplate> updateTemplate(@PathVariable Long id, @RequestBody ReportTemplate template) {
template.setId(id);
return R.ok(reportService.updateTemplate(template));
}
// ==================== 15. 同比分析 ====================
@Operation(summary = "同比分析(水量/水质)")
@GetMapping("/statistics/yoy")
public R<StatisticsResult> yearOverYear(
@RequestParam String dataType,
@RequestParam(required = false) LocalDate date,
@RequestParam(required = false) String area) {
LocalDate targetDate = date != null ? date : LocalDate.now();
if ("quantity".equals(dataType)) {
return R.ok(statisticsService.quantityYearOverYear(targetDate, area));
} else {
return R.ok(statisticsService.qualityYearOverYear(targetDate, area));
}
}
// ==================== 16. 环比分析 ====================
@Operation(summary = "环比分析(水量/水质)")
@GetMapping("/statistics/mom")
public R<StatisticsResult> monthOverMonth(
@RequestParam String dataType,
@RequestParam(required = false) LocalDate date,
@RequestParam(required = false) String area) {
LocalDate targetDate = date != null ? date : LocalDate.now();
if ("quantity".equals(dataType)) {
return R.ok(statisticsService.quantityMonthOverMonth(targetDate, area));
} else {
return R.ok(statisticsService.qualityMonthOverMonth(targetDate, area));
}
}
// ==================== 17. 趋势分析 ====================
@Operation(summary = "趋势分析(日级)")
@GetMapping("/statistics/trend")
public R<StatisticsResult> trend(
@RequestParam String dataType,
@RequestParam LocalDate startDate,
@RequestParam LocalDate endDate,
@RequestParam(required = false) String area,
@RequestParam(required = false) String pointCode) {
if ("quantity".equals(dataType)) {
return R.ok(statisticsService.quantityTrend(startDate, endDate, area, pointCode));
} else {
return R.ok(statisticsService.qualityTrend(startDate, endDate, area, pointCode));
}
}
// ==================== 18. 月度趋势(年度) ====================
@Operation(summary = "月度趋势(年度报表)")
@GetMapping("/statistics/monthly-trend")
public R<StatisticsResult> monthlyTrend(
@RequestParam String dataType,
@RequestParam int year,
@RequestParam(required = false) String area) {
if ("quantity".equals(dataType)) {
return R.ok(statisticsService.quantityMonthlyTrend(year, area));
} else {
return R.ok(statisticsService.qualityMonthlyTrend(year, area));
}
}
// ==================== 19. 仪表板概览 ====================
@Operation(summary = "数据统计仪表板概览")
@GetMapping("/statistics/dashboard")
public R<Map<String, Object>> dashboard(
@RequestParam(required = false) LocalDate date,
@RequestParam(required = false) String area) {
LocalDate targetDate = date != null ? date : LocalDate.now();
return R.ok(statisticsService.dashboardOverview(targetDate, area));
}
// ==================== 20. 报表类型统计 ====================
@Operation(summary = "报表类型统计")
@GetMapping("/report/statistics")
public R<List<Map<String, Object>>> reportStatistics() {
return R.ok(reportService.countReportsByType());
}
}
@@ -0,0 +1,29 @@
package com.water.data_engine.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.water.common.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("de_data_report")
public class DataReport extends BaseEntity {
private String reportName;
private String reportCode;
private Long templateId;
private String reportType;
private String dataType;
private String area;
private LocalDate periodStart;
private LocalDate periodEnd;
@TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
private Object content;
@TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
private Object summary;
private String filePath;
private String status;
private String generatedBy;
}
@@ -0,0 +1,19 @@
package com.water.data_engine.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class HistoricalQuery {
private String dataType;
private String area;
private String pointCode;
private String deviceSn;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer qualityFlag;
private Integer pageNum = 1;
private Integer pageSize = 20;
private String orderBy;
private String orderDirection = "desc";
}
@@ -0,0 +1,21 @@
package com.water.data_engine.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.water.common.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("de_report_template")
public class ReportTemplate extends BaseEntity {
private String templateName;
private String templateCode;
private String reportType;
private String dataType;
private String description;
@TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
private Object config;
private String cronExpr;
private Integer enabled;
}
@@ -0,0 +1,31 @@
package com.water.data_engine.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.water.common.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("de_water_quality")
public class WaterQuality extends BaseEntity {
private String monitorPoint;
private String pointCode;
private String area;
private String deviceSn;
private BigDecimal ph;
private BigDecimal turbidity;
private BigDecimal residualChlorine;
private BigDecimal dissolvedOxygen;
private BigDecimal conductivity;
private BigDecimal temperature;
private BigDecimal cod;
private BigDecimal ammoniaNitrogen;
private Integer isQualified;
private LocalDateTime collectTime;
private String dataType;
private Integer quality;
}
@@ -0,0 +1,27 @@
package com.water.data_engine.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.water.common.core.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("de_water_quantity")
public class WaterQuantity extends BaseEntity {
private String monitorPoint;
private String pointCode;
private String area;
private String deviceSn;
private BigDecimal flowRate;
private BigDecimal totalFlow;
private BigDecimal pressure;
private BigDecimal waterLevel;
private BigDecimal velocity;
private LocalDateTime collectTime;
private String dataType;
private Integer quality;
}
@@ -0,0 +1,19 @@
package com.water.data_engine.entity.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Data
public class StatisticsResult {
private String type;
private String area;
private String dataType;
private BigDecimal currentValue;
private BigDecimal compareValue;
private BigDecimal changeAmount;
private BigDecimal changeRate;
private List<Map<String, Object>> trendData;
private Map<String, Object> extra;
}
@@ -0,0 +1,5 @@
package com.water.data_engine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.data_engine.entity.DataReport;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DataReportMapper extends BaseMapper<DataReport> {}
@@ -0,0 +1,5 @@
package com.water.data_engine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.data_engine.entity.HistoricalQuery;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface HistoricalQueryMapper extends BaseMapper<HistoricalQuery> {}
@@ -0,0 +1,5 @@
package com.water.data_engine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.data_engine.entity.ReportTemplate;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface ReportTemplateMapper extends BaseMapper<ReportTemplate> {}
@@ -0,0 +1,5 @@
package com.water.data_engine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.data_engine.entity.WaterQuality;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface WaterQualityMapper extends BaseMapper<WaterQuality> {}
@@ -0,0 +1,5 @@
package com.water.data_engine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.data_engine.entity.WaterQuantity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface WaterQuantityMapper extends BaseMapper<WaterQuantity> {}
@@ -0,0 +1,108 @@
package com.water.data_engine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.data_engine.entity.HistoricalQuery;
import com.water.data_engine.entity.WaterQuantity;
import com.water.data_engine.entity.WaterQuality;
import com.water.data_engine.mapper.WaterQuantityMapper;
import com.water.data_engine.mapper.WaterQualityMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
@Service
@RequiredArgsConstructor
public class HistoryDataService {
private final WaterQuantityMapper quantityMapper;
private final WaterQualityMapper qualityMapper;
/**
* 历史水量数据分页查询
*/
public Page<WaterQuantity> queryQuantityHistory(HistoricalQuery query) {
Page<WaterQuantity> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<WaterQuantity> wrapper = new LambdaQueryWrapper<>();
if (query.getArea() != null && !query.getArea().isBlank()) {
wrapper.eq(WaterQuantity::getArea, query.getArea());
}
if (query.getPointCode() != null && !query.getPointCode().isBlank()) {
wrapper.eq(WaterQuantity::getPointCode, query.getPointCode());
}
if (query.getStartTime() != null) {
wrapper.ge(WaterQuantity::getRecordTime, query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le(WaterQuantity::getRecordTime, query.getEndTime());
}
wrapper.orderByDesc(WaterQuantity::getRecordTime);
return quantityMapper.selectPage(page, wrapper);
}
/**
* 历史水质数据分页查询
*/
public Page<WaterQuality> queryQualityHistory(HistoricalQuery query) {
Page<WaterQuality> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<WaterQuality> wrapper = new LambdaQueryWrapper<>();
if (query.getArea() != null && !query.getArea().isBlank()) {
wrapper.eq(WaterQuality::getArea, query.getArea());
}
if (query.getPointCode() != null && !query.getPointCode().isBlank()) {
wrapper.eq(WaterQuality::getPointCode, query.getPointCode());
}
if (query.getStartTime() != null) {
wrapper.ge(WaterQuality::getRecordTime, query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le(WaterQuality::getRecordTime, query.getEndTime());
}
wrapper.orderByDesc(WaterQuality::getRecordTime);
return qualityMapper.selectPage(page, wrapper);
}
/**
* 导出历史数据
*/
public List<Map<String, Object>> exportHistory(String dataType, String area,
LocalDateTime start, LocalDateTime end) {
List<Map<String, Object>> result = new ArrayList<>();
if ("quantity".equals(dataType)) {
LambdaQueryWrapper<WaterQuantity> wrapper = new LambdaQueryWrapper<>();
if (area != null) wrapper.eq(WaterQuantity::getArea, area);
if (start != null) wrapper.ge(WaterQuantity::getRecordTime, start);
if (end != null) wrapper.le(WaterQuantity::getRecordTime, end);
List<WaterQuantity> records = quantityMapper.selectList(wrapper);
for (WaterQuantity r : records) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("区域", r.getArea());
row.put("监测点", r.getPointCode());
row.put("时间", r.getRecordTime());
row.put("水量(m³)", r.getQuantity());
row.put("单位", r.getUnit());
result.add(row);
}
} else if ("quality".equals(dataType)) {
LambdaQueryWrapper<WaterQuality> wrapper = new LambdaQueryWrapper<>();
if (area != null) wrapper.eq(WaterQuality::getArea, area);
if (start != null) wrapper.ge(WaterQuality::getRecordTime, start);
if (end != null) wrapper.le(WaterQuality::getRecordTime, end);
List<WaterQuality> records = qualityMapper.selectList(wrapper);
for (WaterQuality r : records) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("区域", r.getArea());
row.put("监测点", r.getPointCode());
row.put("时间", r.getRecordTime());
row.put("浊度(NTU)", r.getTurbidity());
row.put("pH", r.getPh());
row.put("余氯(mg/L)", r.getResidualChlorine());
row.put("结果", r.getResult());
result.add(row);
}
}
return result;
}
}
@@ -0,0 +1,111 @@
package com.water.data_engine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.data_engine.entity.DataReport;
import com.water.data_engine.entity.ReportTemplate;
import com.water.data_engine.mapper.DataReportMapper;
import com.water.data_engine.mapper.ReportTemplateMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
@Service
@RequiredArgsConstructor
public class ReportService {
private final DataReportMapper reportMapper;
private final ReportTemplateMapper templateMapper;
/**
* 自动生成报表
*/
public DataReport generateReport(String reportType, String period) {
DataReport report = new DataReport();
report.setReportNo("RPT-" + System.currentTimeMillis());
report.setReportType(reportType); // daily/weekly/monthly/yearly
report.setPeriod(period);
report.setTitle(reportType + " 报表 " + period);
// Generate content based on type
Map<String, Object> content = new LinkedHashMap<>();
content.put("generatedAt", LocalDateTime.now());
content.put("period", period);
switch (reportType) {
case "daily" -> {
content.put("totalSupply", 12500.0 + Math.random() * 2000);
content.put("totalConsumption", 11000.0 + Math.random() * 1500);
content.put("alertCount", (int)(Math.random() * 10));
content.put("waterQualityRate", 98.5 + Math.random() * 1.5);
}
case "weekly" -> {
content.put("avgDailySupply", 12000.0 + Math.random() * 1000);
content.put("peakDay", "周三");
content.put("totalAlerts", (int)(Math.random() * 50));
content.put("avgQualityRate", 98.0 + Math.random() * 2.0);
}
case "monthly" -> {
content.put("totalSupply", 380000.0 + Math.random() * 50000);
content.put("totalConsumption", 350000.0 + Math.random() * 40000);
content.put("leakageRate", 8.0 + Math.random() * 4);
content.put("complaints", (int)(Math.random() * 30));
}
case "yearly" -> {
content.put("totalSupply", 4500000.0 + Math.random() * 500000);
content.put("yoyGrowth", -5.0 + Math.random() * 15);
content.put("infrastructureInvestment", 2500000.0);
content.put("serviceCoverage", 95.0 + Math.random() * 5);
}
}
report.setContent(content.toString());
report.setStatus("GENERATED");
report.setCreatedTime(LocalDateTime.now());
reportMapper.insert(report);
return report;
}
/**
* 获取报表列表
*/
public List<DataReport> listReports(String reportType, String status) {
LambdaQueryWrapper<DataReport> wrapper = new LambdaQueryWrapper<>();
if (reportType != null && !reportType.isBlank()) wrapper.eq(DataReport::getReportType, reportType);
if (status != null && !status.isBlank()) wrapper.eq(DataReport::getStatus, status);
return reportMapper.selectList(wrapper.orderByDesc(DataReport::getCreatedTime));
}
/**
* 获取报表详情
*/
public DataReport getReport(Long id) {
return reportMapper.selectById(id);
}
/**
* 发布报表
*/
public void publishReport(Long id) {
DataReport report = reportMapper.selectById(id);
if (report == null) throw new RuntimeException("报表不存在");
report.setStatus("PUBLISHED");
report.setPublishedTime(LocalDateTime.now());
reportMapper.updateById(report);
}
/**
* 模板管理
*/
public List<ReportTemplate> listTemplates() {
return templateMapper.selectList(null);
}
public ReportTemplate createTemplate(ReportTemplate template) {
template.setCreatedTime(LocalDateTime.now());
templateMapper.insert(template);
return template;
}
}
@@ -0,0 +1,106 @@
package com.water.data_engine.service;
import com.water.data_engine.entity.dto.StatisticsResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@RequiredArgsConstructor
public class StatisticsService {
/**
* 同比分析
*/
public StatisticsResult yearOverYear(String metric, String period) {
StatisticsResult result = new StatisticsResult();
result.setMetric(metric);
result.setPeriod(period);
result.setType("YOY");
// Simulated data
double current = 1000 + Math.random() * 5000;
double previous = 1000 + Math.random() * 5000;
result.setCurrentValue(current);
result.setPreviousValue(previous);
result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0);
result.setTrend(current > previous ? "上升" : "下降");
return result;
}
/**
* 环比分析
*/
public StatisticsResult monthOverMonth(String metric, String period) {
StatisticsResult result = new StatisticsResult();
result.setMetric(metric);
result.setPeriod(period);
result.setType("MOM");
double current = 500 + Math.random() * 2000;
double previous = 500 + Math.random() * 2000;
result.setCurrentValue(current);
result.setPreviousValue(previous);
result.setChangeRate(previous > 0 ? (current - previous) / previous * 100 : 0);
result.setTrend(current > previous ? "上升" : "下降");
return result;
}
/**
* 趋势分析
*/
public Map<String, Object> trendAnalysis(String metric, String area, String startPeriod, String endPeriod) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("metric", metric);
result.put("area", area);
result.put("startPeriod", startPeriod);
result.put("endPeriod", endPeriod);
// Generate trend data points
List<Map<String, Object>> dataPoints = new ArrayList<>();
double base = 1000 + Math.random() * 2000;
for (int i = 0; i < 12; i++) {
Map<String, Object> point = new LinkedHashMap<>();
point.put("period", "2025-" + String.format("%02d", i + 1));
point.put("value", base + Math.random() * 500 - 250);
dataPoints.add(point);
}
result.put("dataPoints", dataPoints);
// Summary
double avg = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).average().orElse(0);
double max = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).max().orElse(0);
double min = dataPoints.stream().mapToDouble(p -> (Double) p.get("value")).min().orElse(0);
result.put("average", avg);
result.put("max", max);
result.put("min", min);
result.put("overallTrend", dataPoints.get(dataPoints.size() - 1).get("value")
.compareTo(dataPoints.get(0).get("value")) > 0 ? "上升" : "下降");
return result;
}
/**
* 综合看板
*/
public Map<String, Object> dashboard() {
Map<String, Object> dashboard = new LinkedHashMap<>();
dashboard.put("todaySupply", 12500 + Math.random() * 2000);
dashboard.put("todayAlerts", (int)(Math.random() * 10));
dashboard.put("deviceOnlineRate", 0.92 + Math.random() * 0.08);
dashboard.put("waterQualityRate", 97 + Math.random() * 3);
dashboard.put("activeWorkOrders", (int)(Math.random() * 20));
dashboard.put("monthlyConsumption", 350000 + Math.random() * 50000);
// Top alerts
List<Map<String, Object>> topAlerts = new ArrayList<>();
topAlerts.add(Map.of("area", "A区主管", "type", "压力异常", "level", "重要"));
topAlerts.add(Map.of("area", "B区支管", "type", "流量偏低", "level", "一般"));
dashboard.put("topAlerts", topAlerts);
return dashboard;
}
}
@@ -0,0 +1,56 @@
-- History Data & Report DDL
CREATE TABLE IF NOT EXISTS de_water_quantity (
id BIGSERIAL PRIMARY KEY,
area VARCHAR(100),
point_code VARCHAR(50),
device_sn VARCHAR(50),
quantity DOUBLE PRECISION,
unit VARCHAR(20),
quality_flag INT DEFAULT 0,
record_time TIMESTAMP,
created_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS de_water_quality (
id BIGSERIAL PRIMARY KEY,
area VARCHAR(100),
point_code VARCHAR(50),
device_sn VARCHAR(50),
turbidity DOUBLE PRECISION,
ph DOUBLE PRECISION,
residual_chlorine DOUBLE PRECISION,
color DOUBLE PRECISION,
odor DOUBLE PRECISION,
result VARCHAR(20),
quality_flag INT DEFAULT 0,
record_time TIMESTAMP,
created_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS de_data_report (
id BIGSERIAL PRIMARY KEY,
report_no VARCHAR(50) UNIQUE,
report_type VARCHAR(20),
period VARCHAR(50),
title VARCHAR(200),
content TEXT,
status VARCHAR(20) DEFAULT 'GENERATED',
published_time TIMESTAMP,
created_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS de_report_template (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200),
report_type VARCHAR(20),
template_content TEXT,
description TEXT,
status INT DEFAULT 1,
created_time TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wq_area_time ON de_water_quantity(area, record_time);
CREATE INDEX IF NOT EXISTS idx_wq_point ON de_water_quantity(point_code);
CREATE INDEX IF NOT EXISTS idx_qual_area_time ON de_water_quality(area, record_time);
CREATE INDEX IF NOT EXISTS idx_rpt_type ON de_data_report(report_type);
CREATE INDEX IF NOT EXISTS idx_rpt_status ON de_data_report(status);
@@ -0,0 +1,185 @@
package com.water.data_engine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.data_engine.entity.*;
import com.water.data_engine.entity.dto.ExportRequest;
import com.water.data_engine.mapper.WaterQuantityMapper;
import com.water.data_engine.mapper.WaterQualityMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 历史数据服务测试
*/
@ExtendWith(MockitoExtension.class)
class HistoryDataServiceTest {
@Mock
private WaterQuantityMapper waterQuantityMapper;
@Mock
private WaterQualityMapper waterQualityMapper;
private HistoryDataService historyDataService;
@BeforeEach
void setUp() {
historyDataService = new HistoryDataService(waterQuantityMapper, waterQualityMapper);
}
@Test
@DisplayName("分页查询水量历史数据-按区域和时间范围")
void testQueryQuantityHistory() {
// Given
HistoricalQuery query = new HistoricalQuery();
query.setArea("城东");
query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
query.setPageNum(1);
query.setPageSize(10);
WaterQuantity wq = new WaterQuantity();
wq.setId(1L);
wq.setMonitorPoint("城东水厂出口");
wq.setArea("城东");
wq.setFlowRate(new BigDecimal("120.5"));
wq.setPressure(new BigDecimal("0.35"));
wq.setCollectTime(LocalDateTime.of(2026, 1, 15, 10, 0));
Page<WaterQuantity> mockPage = new Page<>(1, 10);
mockPage.setRecords(List.of(wq));
mockPage.setTotal(1);
when(waterQuantityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
.thenReturn(mockPage);
// When
Page<WaterQuantity> result = historyDataService.queryQuantityHistory(query);
// Then
assertNotNull(result);
assertEquals(1, result.getRecords().size());
assertEquals("城东", result.getRecords().get(0).getArea());
verify(waterQuantityMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
}
@Test
@DisplayName("分页查询水质历史数据-按监测点")
void testQueryQualityHistory() {
// Given
HistoricalQuery query = new HistoricalQuery();
query.setPointCode("WQ001");
query.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
query.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
query.setPageNum(1);
query.setPageSize(20);
WaterQuality wq = new WaterQuality();
wq.setId(1L);
wq.setMonitorPoint("水厂出口");
wq.setPointCode("WQ001");
wq.setPh(new BigDecimal("7.2"));
wq.setTurbidity(new BigDecimal("0.5"));
wq.setIsQualified(1);
wq.setCollectTime(LocalDateTime.of(2026, 1, 10, 8, 0));
Page<WaterQuality> mockPage = new Page<>(1, 20);
mockPage.setRecords(List.of(wq));
mockPage.setTotal(1);
when(waterQualityMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
.thenReturn(mockPage);
// When
Page<WaterQuality> result = historyDataService.queryQualityHistory(query);
// Then
assertNotNull(result);
assertEquals(1, result.getRecords().size());
assertEquals("WQ001", result.getRecords().get(0).getPointCode());
}
@Test
@DisplayName("水量区域聚合统计")
void testAggregateQuantityByArea() {
LocalDateTime start = LocalDateTime.of(2026, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 1, 31, 23, 59);
Map<String, Object> stat = new LinkedHashMap<>();
stat.put("area", "城东");
stat.put("avg_flow_rate", new BigDecimal("120.5"));
stat.put("record_count", 1000L);
when(waterQuantityMapper.aggregateByArea(eq(start), eq(end), isNull()))
.thenReturn(List.of(stat));
List<Map<String, Object>> result = historyDataService.aggregateQuantityByArea(start, end, null);
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("城东", result.get(0).get("area"));
}
@Test
@DisplayName("导出水量数据-生成导出结构")
void testQueryForExport_Quantity() {
ExportRequest request = new ExportRequest();
request.setDataType("quantity");
request.setArea("城东");
request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
request.setFormat("excel");
WaterQuantity wq = new WaterQuantity();
wq.setMonitorPoint("城东水厂");
wq.setArea("城东");
wq.setFlowRate(new BigDecimal("120.5"));
when(waterQuantityMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(wq));
Map<String, Object> result = historyDataService.queryForExport(request);
assertNotNull(result);
assertEquals("quantity", result.get("dataType"));
assertEquals("城东", result.get("area"));
assertNotNull(result.get("headers"));
assertNotNull(result.get("data"));
}
@Test
@DisplayName("导出水质数据-生成导出结构")
void testQueryForExport_Quality() {
ExportRequest request = new ExportRequest();
request.setDataType("quality");
request.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
request.setEndTime(LocalDateTime.of(2026, 1, 31, 23, 59));
WaterQuality wq = new WaterQuality();
wq.setMonitorPoint("水厂出口");
wq.setPh(new BigDecimal("7.2"));
wq.setIsQualified(1);
when(waterQualityMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(wq));
Map<String, Object> result = historyDataService.queryForExport(request);
assertNotNull(result);
assertEquals("quality", result.get("dataType"));
assertEquals(1, ((List<?>) result.get("data")).size());
}
}
@@ -0,0 +1,158 @@
package com.water.data_engine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.data_engine.entity.*;
import com.water.data_engine.mapper.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDate;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 报表服务测试
*/
@ExtendWith(MockitoExtension.class)
class ReportServiceTest {
@Mock
private DataReportMapper dataReportMapper;
@Mock
private ReportTemplateMapper reportTemplateMapper;
@Mock
private WaterQuantityMapper waterQuantityMapper;
@Mock
private WaterQualityMapper waterQualityMapper;
private ReportService reportService;
@BeforeEach
void setUp() {
reportService = new ReportService(dataReportMapper, reportTemplateMapper,
waterQuantityMapper, waterQualityMapper);
}
@Test
@DisplayName("生成日报-水量")
void testGenerateDailyReport_Quantity() {
// Given
ReportTemplate template = new ReportTemplate();
template.setId(1L);
template.setTemplateCode("TPL-QTY-DAY");
template.setReportType("daily");
template.setDataType("quantity");
when(reportTemplateMapper.findByType("daily", "quantity")).thenReturn(List.of(template));
when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of());
when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1);
// When
DataReport report = reportService.generateReport("daily", "quantity", null);
// Then
assertNotNull(report);
assertEquals("daily", report.getReportType());
assertEquals("quantity", report.getDataType());
assertEquals("generated", report.getStatus());
assertNotNull(report.getReportCode());
assertTrue(report.getReportCode().startsWith("RPT-"));
verify(dataReportMapper).insert(any(DataReport.class));
}
@Test
@DisplayName("生成月报-水质(指定时间段)")
void testGenerateMonthlyReport_Quality_WithPeriod() {
when(reportTemplateMapper.findByType("monthly", "quality")).thenReturn(List.of());
when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
when(waterQualityMapper.aggregateDaily(any(), any(), any(), any())).thenReturn(List.of());
when(dataReportMapper.insert(any(DataReport.class))).thenReturn(1);
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 1, 31);
DataReport report = reportService.generateReport("monthly", "quality", "城东", start, end);
assertNotNull(report);
assertEquals("monthly", report.getReportType());
assertEquals("quality", report.getDataType());
assertEquals("城东", report.getArea());
assertEquals(start, report.getPeriodStart());
assertEquals(end, report.getPeriodEnd());
}
@Test
@DisplayName("生成报表-不支持的类型抛异常")
void testGenerateReport_InvalidType() {
assertThrows(IllegalArgumentException.class, () ->
reportService.generateReport("invalid", "quantity", null)
);
}
@Test
@DisplayName("查询报表列表-分页")
void testListReports() {
Page<DataReport> mockPage = new Page<>(1, 10);
mockPage.setRecords(List.of());
mockPage.setTotal(0);
when(dataReportMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
.thenReturn(mockPage);
Page<DataReport> result = reportService.listReports(1, 10, "daily", null);
assertNotNull(result);
verify(dataReportMapper).selectPage(any(Page.class), any(LambdaQueryWrapper.class));
}
@Test
@DisplayName("模板CRUD操作")
void testTemplateOperations() {
// Create
ReportTemplate template = new ReportTemplate();
template.setTemplateName("测试模板");
template.setTemplateCode("TPL-TEST");
template.setReportType("daily");
template.setDataType("quantity");
when(reportTemplateMapper.insert(any(ReportTemplate.class))).thenReturn(1);
ReportTemplate created = reportService.createTemplate(template);
assertEquals("测试模板", created.getTemplateName());
// Get
when(reportTemplateMapper.selectById(1L)).thenReturn(template);
ReportTemplate found = reportService.getTemplate(1L);
assertNotNull(found);
// Not found
when(reportTemplateMapper.selectById(999L)).thenReturn(null);
assertThrows(RuntimeException.class, () -> reportService.getTemplate(999L));
}
@Test
@DisplayName("查询最近报表和统计")
void testRecentAndStatistics() {
when(dataReportMapper.findRecent(any(), any(), eq(5))).thenReturn(List.of());
when(dataReportMapper.countByType()).thenReturn(List.of());
List<DataReport> recent = reportService.findRecentReports(null, null, 5);
List<Map<String, Object>> stats = reportService.countReportsByType();
assertNotNull(recent);
assertNotNull(stats);
verify(dataReportMapper).findRecent(any(), any(), eq(5));
verify(dataReportMapper).countByType();
}
}
@@ -0,0 +1,180 @@
package com.water.data_engine.service;
import com.water.data_engine.entity.dto.StatisticsResult;
import com.water.data_engine.mapper.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 统计分析服务测试
*/
@ExtendWith(MockitoExtension.class)
class StatisticsServiceTest {
@Mock
private WaterQuantityMapper waterQuantityMapper;
@Mock
private WaterQualityMapper waterQualityMapper;
@Mock
private StatQuantityDailyMapper statQuantityDailyMapper;
@Mock
private StatQualityDailyMapper statQualityDailyMapper;
private StatisticsService statisticsService;
@BeforeEach
void setUp() {
statisticsService = new StatisticsService(
waterQuantityMapper, waterQualityMapper,
statQuantityDailyMapper, statQualityDailyMapper
);
}
@Test
@DisplayName("水量同比分析-有数据")
void testQuantityYearOverYear() {
// Given
LocalDate date = LocalDate.of(2026, 6, 14);
Map<String, Object> currentData = new LinkedHashMap<>();
currentData.put("area", "城东");
currentData.put("sum_total_flow", 15000.0);
Map<String, Object> compareData = new LinkedHashMap<>();
compareData.put("area", "城东");
compareData.put("sum_total_flow", 12000.0);
when(statQuantityDailyMapper.sumByArea(any(), any(), isNull()))
.thenReturn(List.of(currentData))
.thenReturn(List.of(compareData));
// When
StatisticsResult result = statisticsService.quantityYearOverYear(date, null);
// Then
assertNotNull(result);
assertEquals("yoy", result.getType());
assertEquals("quantity", result.getDataType());
assertEquals(0, new BigDecimal("15000").compareTo(result.getCurrentValue()));
assertEquals(0, new BigDecimal("12000").compareTo(result.getCompareValue()));
assertTrue(result.getChangeRate().compareTo(BigDecimal.ZERO) > 0);
}
@Test
@DisplayName("水量环比分析-对比期为零")
void testQuantityMonthOverMonth_ZeroCompare() {
LocalDate date = LocalDate.of(2026, 3, 15);
Map<String, Object> currentData = new LinkedHashMap<>();
currentData.put("area", "城南");
currentData.put("sum_total_flow", 5000.0);
when(statQuantityDailyMapper.sumByArea(any(), any(), isNull()))
.thenReturn(List.of(currentData))
.thenReturn(List.of());
StatisticsResult result = statisticsService.quantityMonthOverMonth(date, null);
assertNotNull(result);
assertEquals("mom", result.getType());
assertEquals(0, new BigDecimal("100").compareTo(result.getChangeRate()));
}
@Test
@DisplayName("水量日趋势分析")
void testQuantityTrend() {
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 1, 7);
Map<String, Object> day1 = new LinkedHashMap<>();
day1.put("stat_date", "2026-01-01");
day1.put("avg_flow_rate", new BigDecimal("120.5"));
day1.put("daily_flow", new BigDecimal("2892"));
day1.put("avg_pressure", new BigDecimal("0.35"));
Map<String, Object> day2 = new LinkedHashMap<>();
day2.put("stat_date", "2026-01-02");
day2.put("avg_flow_rate", new BigDecimal("118.3"));
day2.put("daily_flow", new BigDecimal("2839"));
day2.put("avg_pressure", new BigDecimal("0.34"));
when(waterQuantityMapper.aggregateDaily(any(), any(), any(), any()))
.thenReturn(List.of(day1, day2));
StatisticsResult result = statisticsService.quantityTrend(start, end, null, null);
assertNotNull(result);
assertEquals("trend", result.getType());
assertEquals("quantity", result.getDataType());
assertEquals(2, result.getTrendData().size());
assertEquals("2026-01-01", result.getTrendData().get(0).get("date"));
}
@Test
@DisplayName("水质日趋势分析")
void testQualityTrend() {
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 1, 3);
Map<String, Object> day1 = new LinkedHashMap<>();
day1.put("stat_date", "2026-01-01");
day1.put("avg_turbidity", new BigDecimal("0.5"));
day1.put("qualified_rate", new BigDecimal("98.5"));
when(waterQualityMapper.aggregateDaily(any(), any(), any(), any()))
.thenReturn(List.of(day1));
StatisticsResult result = statisticsService.qualityTrend(start, end, "城东", null);
assertNotNull(result);
assertEquals("trend", result.getType());
assertEquals("quality", result.getDataType());
assertEquals(1, result.getTrendData().size());
}
@Test
@DisplayName("仪表板概览-综合统计")
void testDashboardOverview() {
LocalDate date = LocalDate.of(2026, 6, 14);
when(waterQuantityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
when(waterQualityMapper.aggregateByArea(any(), any(), any())).thenReturn(List.of());
Map<String, Object> result = statisticsService.dashboardOverview(date, null);
assertNotNull(result);
assertEquals(date.toString(), result.get("date"));
assertTrue(result.containsKey("quantityStats"));
assertTrue(result.containsKey("qualityStats"));
}
@Test
@DisplayName("水质同比分析-无数据返回零")
void testQualityYearOverYear_Empty() {
when(statQualityDailyMapper.sumByArea(any(), any(), any()))
.thenReturn(List.of())
.thenReturn(List.of());
StatisticsResult result = statisticsService.qualityYearOverYear(LocalDate.now(), null);
assertNotNull(result);
assertEquals("yoy", result.getType());
assertEquals(0, BigDecimal.ZERO.compareTo(result.getCurrentValue()));
assertEquals(0, BigDecimal.ZERO.compareTo(result.getChangeRate()));
}
}
@@ -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;
}
}