feat(wm-config): #72 阈值管理+信息发布+设备管理

This commit is contained in:
2026-06-14 15:37:16 +08:00
parent 1870171e45
commit 4a0fc1bf42
24 changed files with 1398 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());
}
}