diff --git a/pom.xml b/pom.xml
index 8dae7470..2cb55ca7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,7 @@
wm-dispatch
wm-system
wm-mobile-app
+ wm-config
diff --git a/wm-config/pom.xml b/wm-config/pom.xml
new file mode 100644
index 00000000..db5f2c1f
--- /dev/null
+++ b/wm-config/pom.xml
@@ -0,0 +1,16 @@
+
+
+ 4.0.0
+ com.waterwm-parent1.0.0-SNAPSHOT
+ wm-config
+
+ com.waterwm-common
+ org.springframework.bootspring-boot-starter-web
+ com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery
+ com.baomidoumybatis-plus-spring-boot3-starter
+ cn.dev33sa-token-spring-boot3-starter
+ org.postgresqlpostgresql
+
+
diff --git a/wm-config/src/main/java/com/water/config/ConfigApplication.java b/wm-config/src/main/java/com/water/config/ConfigApplication.java
new file mode 100644
index 00000000..26db7723
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/ConfigApplication.java
@@ -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);
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java b/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java
new file mode 100644
index 00000000..9cbeaee4
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/controller/AnnouncementController.java
@@ -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> 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 getById(@PathVariable Long id) {
+ return R.ok(announcementService.getById(id));
+ }
+
+ @Operation(summary = "创建公告(草稿)")
+ @PostMapping
+ public R create(@RequestBody Announcement announcement) {
+ return R.ok(announcementService.createAnnouncement(announcement));
+ }
+
+ @Operation(summary = "更新公告")
+ @PutMapping("/{id}")
+ public R update(@PathVariable Long id, @RequestBody Announcement announcement) {
+ announcementService.updateAnnouncement(id, announcement);
+ return R.ok("更新成功");
+ }
+
+ @Operation(summary = "发布公告")
+ @PostMapping("/{id}/publish")
+ public R publish(@PathVariable Long id) {
+ announcementService.publish(id);
+ return R.ok("发布成功");
+ }
+
+ @Operation(summary = "撤回公告")
+ @PostMapping("/{id}/withdraw")
+ public R withdraw(@PathVariable Long id) {
+ announcementService.withdraw(id);
+ return R.ok("撤回成功");
+ }
+
+ @Operation(summary = "删除公告")
+ @DeleteMapping("/{id}")
+ public R delete(@PathVariable Long id) {
+ announcementService.deleteAnnouncement(id);
+ return R.ok("删除成功");
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java b/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java
new file mode 100644
index 00000000..707978d0
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/controller/DeviceManageController.java
@@ -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> 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 getById(@PathVariable Long id) {
+ return R.ok(deviceManageService.getById(id));
+ }
+
+ @Operation(summary = "创建设备")
+ @PostMapping
+ public R create(@RequestBody DeviceInfo device) {
+ return R.ok(deviceManageService.createDevice(device));
+ }
+
+ @Operation(summary = "更新设备")
+ @PutMapping("/{id}")
+ public R update(@PathVariable Long id, @RequestBody DeviceInfo device) {
+ deviceManageService.updateDevice(id, device);
+ return R.ok("更新成功");
+ }
+
+ @Operation(summary = "删除设备")
+ @DeleteMapping("/{id}")
+ public R delete(@PathVariable Long id) {
+ deviceManageService.removeById(id);
+ return R.ok("删除成功");
+ }
+
+ @Operation(summary = "更新设备状态")
+ @PutMapping("/{id}/status")
+ public R updateStatus(@PathVariable Long id, @RequestParam Integer deviceStatus) {
+ deviceManageService.updateDeviceStatus(id, deviceStatus);
+ return R.ok("状态更新成功");
+ }
+
+ @Operation(summary = "按分类查询设备")
+ @GetMapping("/category/{category}")
+ public R> getByCategory(@PathVariable Integer category) {
+ return R.ok(deviceManageService.getDevicesByCategory(category));
+ }
+
+ @Operation(summary = "按状态查询设备")
+ @GetMapping("/status/{status}")
+ public R> getByStatus(@PathVariable Integer status) {
+ return R.ok(deviceManageService.getDevicesByStatus(status));
+ }
+
+ @Operation(summary = "添加维保记录")
+ @PostMapping("/maintenance")
+ public R addMaintenance(@RequestBody DeviceMaintenance maintenance) {
+ return R.ok(deviceManageService.addMaintenance(maintenance));
+ }
+
+ @Operation(summary = "查询维保记录")
+ @GetMapping("/maintenance")
+ public R> 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 updateMaintenance(@PathVariable Long id, @RequestBody DeviceMaintenance maintenance) {
+ deviceManageService.updateMaintenance(id, maintenance);
+ return R.ok("更新成功");
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/controller/ThresholdController.java b/wm-config/src/main/java/com/water/config/controller/ThresholdController.java
new file mode 100644
index 00000000..a5507b29
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/controller/ThresholdController.java
@@ -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> 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 getById(@PathVariable Long id) {
+ return R.ok(thresholdService.getById(id));
+ }
+
+ @Operation(summary = "创建阈值配置")
+ @PostMapping
+ public R create(@RequestBody ThresholdConfig config) {
+ return R.ok(thresholdService.createThreshold(config));
+ }
+
+ @Operation(summary = "更新阈值配置")
+ @PutMapping("/{id}")
+ public R update(@PathVariable Long id, @RequestBody ThresholdConfig config) {
+ thresholdService.updateThreshold(id, config);
+ return R.ok("更新成功");
+ }
+
+ @Operation(summary = "删除阈值配置")
+ @DeleteMapping("/{id}")
+ public R delete(@PathVariable Long id) {
+ thresholdService.deleteThreshold(id);
+ return R.ok("删除成功");
+ }
+
+ @Operation(summary = "启用/禁用阈值")
+ @PutMapping("/{id}/status")
+ public R toggleStatus(@PathVariable Long id, @RequestParam Integer status) {
+ thresholdService.toggleStatus(id, status);
+ return R.ok(status == 1 ? "已启用" : "已禁用");
+ }
+
+ @Operation(summary = "获取指标的全局阈值(多级)")
+ @GetMapping("/global/{metricCode}")
+ public R> getGlobalThresholds(@PathVariable String metricCode) {
+ return R.ok(thresholdService.getGlobalThresholds(metricCode));
+ }
+
+ @Operation(summary = "获取设备阈值配置")
+ @GetMapping("/device/{deviceId}")
+ public R> getDeviceThresholds(@PathVariable Long deviceId) {
+ return R.ok(thresholdService.getDeviceThresholds(deviceId));
+ }
+
+ @Operation(summary = "获取阈值变更历史")
+ @GetMapping("/history")
+ public R> getChangeHistory(@RequestParam(required = false) Long thresholdId,
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "10") int size) {
+ return R.ok(thresholdService.getChangeHistory(thresholdId, page, size));
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/entity/Announcement.java b/wm-config/src/main/java/com/water/config/entity/Announcement.java
new file mode 100644
index 00000000..4a1ce99d
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/entity/Announcement.java
@@ -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;
+}
diff --git a/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java b/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java
new file mode 100644
index 00000000..76d84794
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/entity/DeviceInfo.java
@@ -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;
+}
diff --git a/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java b/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java
new file mode 100644
index 00000000..d566af13
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/entity/DeviceMaintenance.java
@@ -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;
+}
diff --git a/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java b/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java
new file mode 100644
index 00000000..cadd348b
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/entity/ThresholdChangeLog.java
@@ -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;
+}
diff --git a/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java b/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java
new file mode 100644
index 00000000..3eae99f7
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/entity/ThresholdConfig.java
@@ -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;
+}
diff --git a/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java b/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java
new file mode 100644
index 00000000..95d92801
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/mapper/AnnouncementMapper.java
@@ -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 {
+}
diff --git a/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java b/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java
new file mode 100644
index 00000000..63ad028c
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/mapper/DeviceInfoMapper.java
@@ -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 {
+
+ @Select("SELECT * FROM config_device_info WHERE device_status = #{status} AND deleted = 0")
+ List selectByDeviceStatus(Integer status);
+
+ @Select("SELECT * FROM config_device_info WHERE category = #{category} AND deleted = 0 ORDER BY device_code")
+ List selectByCategory(Integer category);
+}
diff --git a/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java b/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java
new file mode 100644
index 00000000..45a39323
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/mapper/DeviceMaintenanceMapper.java
@@ -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 {
+
+ @Select("SELECT * FROM config_device_maintenance WHERE device_id = #{deviceId} AND deleted = 0 ORDER BY start_time DESC")
+ List selectByDeviceId(Long deviceId);
+}
diff --git a/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java b/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java
new file mode 100644
index 00000000..9d37f034
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/mapper/ThresholdChangeLogMapper.java
@@ -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 {
+}
diff --git a/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java b/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java
new file mode 100644
index 00000000..8e57c1f1
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/mapper/ThresholdConfigMapper.java
@@ -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 {
+
+ @Select("SELECT * FROM config_threshold WHERE metric_code = #{metricCode} AND device_id IS NULL AND status = 1 AND deleted = 0 ORDER BY level")
+ List selectGlobalByMetricCode(String metricCode);
+
+ @Select("SELECT * FROM config_threshold WHERE device_id = #{deviceId} AND status = 1 AND deleted = 0 ORDER BY metric_code, level")
+ List selectByDeviceId(Long deviceId);
+}
diff --git a/wm-config/src/main/java/com/water/config/service/AnnouncementService.java b/wm-config/src/main/java/com/water/config/service/AnnouncementService.java
new file mode 100644
index 00000000..6091d726
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/service/AnnouncementService.java
@@ -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 {
+
+ /**
+ * 分页查询公告
+ */
+ public Page pageAnnouncements(int page, int size, Integer type, Integer publishStatus) {
+ LambdaQueryWrapper 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("→ 站内信渠道已触发");
+ }
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/service/DeviceManageService.java b/wm-config/src/main/java/com/water/config/service/DeviceManageService.java
new file mode 100644
index 00000000..839f8733
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/service/DeviceManageService.java
@@ -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 {
+
+ private final DeviceMaintenanceMapper maintenanceMapper;
+
+ /**
+ * 分页查询设备
+ */
+ public Page pageDevices(int page, int size, String deviceName, Integer category, Integer deviceStatus) {
+ LambdaQueryWrapper 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()
+ .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 getDevicesByCategory(Integer category) {
+ return baseMapper.selectByCategory(category);
+ }
+
+ /**
+ * 按状态查询设备
+ */
+ public List 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 pageMaintenances(Long deviceId, int page, int size) {
+ LambdaQueryWrapper 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 getDeviceCountByCategory() {
+ // 简化版:返回各分类的设备数量
+ LambdaQueryWrapper qw = new LambdaQueryWrapper<>();
+ qw.select(DeviceInfo::getCategory);
+ qw.groupBy(DeviceInfo::getCategory);
+ return this.listMaps(qw).stream()
+ .map(m -> ((Number) m.get("category")).longValue())
+ .toList();
+ }
+}
diff --git a/wm-config/src/main/java/com/water/config/service/ThresholdService.java b/wm-config/src/main/java/com/water/config/service/ThresholdService.java
new file mode 100644
index 00000000..550940b6
--- /dev/null
+++ b/wm-config/src/main/java/com/water/config/service/ThresholdService.java
@@ -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 {
+
+ private final ThresholdChangeLogMapper changeLogMapper;
+
+ /**
+ * 分页查询阈值配置
+ */
+ public Page pageThresholds(int page, int size, String metricCode, Integer level) {
+ LambdaQueryWrapper 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 getGlobalThresholds(String metricCode) {
+ return baseMapper.selectGlobalByMetricCode(metricCode);
+ }
+
+ /**
+ * 获取某设备的阈值配置
+ */
+ public List getDeviceThresholds(Long deviceId) {
+ return baseMapper.selectByDeviceId(deviceId);
+ }
+
+ /**
+ * 获取阈值变更历史
+ */
+ public Page getChangeHistory(Long thresholdId, int page, int size) {
+ LambdaQueryWrapper 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);
+ }
+}
diff --git a/wm-config/src/main/resources/application.yml b/wm-config/src/main/resources/application.yml
new file mode 100644
index 00000000..f1cbf385
--- /dev/null
+++ b/wm-config/src/main/resources/application.yml
@@ -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
diff --git a/wm-config/src/main/resources/db/schema.sql b/wm-config/src/main/resources/db/schema.sql
new file mode 100644
index 00000000..f58cf900
--- /dev/null
+++ b/wm-config/src/main/resources/db/schema.sql
@@ -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);
diff --git a/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java b/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java
new file mode 100644
index 00000000..85481eae
--- /dev/null
+++ b/wm-config/src/test/java/com/water/config/AnnouncementServiceTest.java
@@ -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());
+ }
+}
diff --git a/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java b/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java
new file mode 100644
index 00000000..d62c5340
--- /dev/null
+++ b/wm-config/src/test/java/com/water/config/DeviceManageServiceTest.java
@@ -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));
+ }
+}
diff --git a/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java b/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java
new file mode 100644
index 00000000..6042f259
--- /dev/null
+++ b/wm-config/src/test/java/com/water/config/ThresholdServiceTest.java
@@ -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());
+ }
+}