feat(wm-production): #65 全工艺药剂投加监控

This commit is contained in:
2026-06-14 16:24:11 +08:00
parent fddf330ab2
commit 98f91e5d74
11 changed files with 994 additions and 0 deletions
@@ -0,0 +1,172 @@
package com.water.production.controller;
import com.water.common.core.result.R;
import com.water.production.entity.ChemicalDosing;
import com.water.production.entity.ChemicalStock;
import com.water.production.entity.DosingStrategy;
import com.water.production.service.ChemicalDosingService;
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.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 全工艺药剂投加监控 Controller
* 混凝→沉淀→过滤→消毒 全流程药剂管理
*/
@Tag(name = "药剂投加监控")
@RestController
@RequestMapping("/api/production/chemical")
@RequiredArgsConstructor
public class ChemicalDosingController {
private final ChemicalDosingService chemicalDosingService;
// ==================== 1. 投加监控 ====================
@Operation(summary = "投加记录分页列表")
@GetMapping("/dosing/list")
public R<Map<String, Object>> listDosing(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String processStage,
@RequestParam(required = false) String station,
@RequestParam(required = false) String status) {
return R.ok(chemicalDosingService.listDosing(page, size, processStage, station, status));
}
@Operation(summary = "投加记录详情")
@GetMapping("/dosing/{id}")
public R<ChemicalDosing> getDosing(@PathVariable Long id) {
ChemicalDosing dosing = chemicalDosingService.getDosingById(id);
return dosing != null ? R.ok(dosing) : R.fail(404, "记录不存在");
}
@Operation(summary = "新增投加记录")
@PostMapping("/dosing")
public R<ChemicalDosing> createDosing(@RequestBody ChemicalDosing dosing) {
return R.ok(chemicalDosingService.createDosing(dosing));
}
@Operation(summary = "更新投加记录")
@PutMapping("/dosing/{id}")
public R<Boolean> updateDosing(@PathVariable Long id, @RequestBody ChemicalDosing dosing) {
return R.ok(chemicalDosingService.updateDosing(id, dosing));
}
@Operation(summary = "删除投加记录")
@DeleteMapping("/dosing/{id}")
public R<Boolean> deleteDosing(@PathVariable Long id) {
return R.ok(chemicalDosingService.deleteDosing(id));
}
// ==================== 2. 投加记录 + 趋势分析 ====================
@Operation(summary = "投加历史记录列表")
@GetMapping("/record/list")
public R<Map<String, Object>> listRecords(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String processStage,
@RequestParam(required = false) String chemicalName,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
return R.ok(chemicalDosingService.listRecords(page, size, processStage, chemicalName, startTime, endTime));
}
@Operation(summary = "投加趋势分析")
@GetMapping("/record/trend")
public R<List<Map<String, Object>>> trendAnalysis(
@RequestParam String processStage,
@RequestParam String startTime,
@RequestParam String endTime) {
return R.ok(chemicalDosingService.trendAnalysis(processStage, startTime, endTime));
}
// ==================== 3. 药耗统计 ====================
@Operation(summary = "药耗统计(日/周/月 + 单位产水药耗)")
@GetMapping("/statistics")
public R<Map<String, Object>> statistics(
@RequestParam(defaultValue = "day") String period,
@RequestParam(required = false) String station) {
return R.ok(chemicalDosingService.statistics(period, station));
}
// ==================== 4. 投加策略 ====================
@Operation(summary = "投加策略列表")
@GetMapping("/strategy/list")
public R<List<DosingStrategy>> listStrategies(
@RequestParam(required = false) String processStage,
@RequestParam(required = false) Boolean enabled) {
return R.ok(chemicalDosingService.listStrategies(processStage, enabled));
}
@Operation(summary = "投加策略详情")
@GetMapping("/strategy/{id}")
public R<DosingStrategy> getStrategy(@PathVariable Long id) {
DosingStrategy strategy = chemicalDosingService.getStrategyById(id);
return strategy != null ? R.ok(strategy) : R.fail(404, "策略不存在");
}
@Operation(summary = "新增投加策略")
@PostMapping("/strategy")
public R<DosingStrategy> createStrategy(@RequestBody DosingStrategy strategy) {
return R.ok(chemicalDosingService.createStrategy(strategy));
}
@Operation(summary = "更新投加策略")
@PutMapping("/strategy/{id}")
public R<Boolean> updateStrategy(@PathVariable Long id, @RequestBody DosingStrategy strategy) {
return R.ok(chemicalDosingService.updateStrategy(id, strategy));
}
@Operation(summary = "删除投加策略")
@DeleteMapping("/strategy/{id}")
public R<Boolean> deleteStrategy(@PathVariable Long id) {
return R.ok(chemicalDosingService.deleteStrategy(id));
}
@Operation(summary = "计算推荐投加量(基于策略联动)")
@PostMapping("/strategy/{id}/calculate")
public R<Map<String, Object>> calculateRecommended(
@PathVariable Long id,
@RequestParam(required = false) BigDecimal turbidity,
@RequestParam(required = false) BigDecimal flow,
@RequestParam(required = false) BigDecimal ph) {
return R.ok(chemicalDosingService.calculateRecommendedDosing(id, turbidity, flow, ph));
}
// ==================== 5. 药剂库存 ====================
@Operation(summary = "库存列表")
@GetMapping("/stock/list")
public R<List<ChemicalStock>> listStocks(
@RequestParam(required = false) String station,
@RequestParam(required = false) String status) {
return R.ok(chemicalDosingService.listStocks(station, status));
}
@Operation(summary = "新增库存记录")
@PostMapping("/stock")
public R<ChemicalStock> createStock(@RequestBody ChemicalStock stock) {
return R.ok(chemicalDosingService.createStock(stock));
}
@Operation(summary = "更新库存")
@PutMapping("/stock/{id}")
public R<Boolean> updateStock(@PathVariable Long id, @RequestBody ChemicalStock stock) {
return R.ok(chemicalDosingService.updateStock(id, stock));
}
@Operation(summary = "低库存预警列表")
@GetMapping("/stock/low-alert")
public R<List<Map<String, Object>>> lowStockAlerts() {
return R.ok(chemicalDosingService.lowStockAlerts());
}
}
@@ -0,0 +1,56 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 药剂投加监控记录
* 各工艺段(混凝/沉淀/过滤/消毒)的药剂投加量实时监控
*/
@Data
@TableName("prod_chemical_dosing")
public class ChemicalDosing {
@TableId(type = IdType.AUTO)
private Long id;
/** 工艺段: coagulation/sedimentation/filtration/disinfection */
private String processStage;
/** 药剂名称 */
private String chemicalName;
/** 药剂编码 */
private String chemicalCode;
/** 投加量(kg) */
private BigDecimal dosingAmount;
/** 投加速率(kg/h) */
private BigDecimal dosingRate;
/** 投加浓度(mg/L) */
private BigDecimal concentration;
/** 当时流量(m³/h) */
private BigDecimal flowRate;
/** 站点/水厂 */
private String station;
/** 操作员 */
private String operator;
/** 状态: active/paused/stopped */
private String status;
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,56 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 药剂库存管理
*/
@Data
@TableName("prod_chemical_stock")
public class ChemicalStock {
@TableId(type = IdType.AUTO)
private Long id;
/** 药剂名称 */
private String chemicalName;
/** 药剂编码 */
private String chemicalCode;
/** 当前库存(kg) */
private BigDecimal currentStock;
/** 最大库存 */
private BigDecimal maxStock;
/** 安全库存(低于此值预警) */
private BigDecimal minStock;
/** 单位 */
private String unit;
/** 仓库位置 */
private String warehouse;
/** 供应商 */
private String supplier;
/** 站点 */
private String station;
/** 状态: normal/low/out */
private String status;
/** 最近入库时间 */
private LocalDateTime lastInbound;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,47 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 投加历史记录(用于趋势分析)
*/
@Data
@TableName("prod_dosing_record")
public class DosingRecord {
@TableId(type = IdType.AUTO)
private Long id;
/** 关联投加记录ID */
private Long dosingId;
/** 工艺段 */
private String processStage;
/** 药剂名称 */
private String chemicalName;
/** 投加量(kg) */
private BigDecimal dosingAmount;
/** 投加速率(kg/h) */
private BigDecimal dosingRate;
/** 投加浓度(mg/L) */
private BigDecimal concentration;
/** 当时流量(m³/h) */
private BigDecimal flowRate;
/** 站点 */
private String station;
/** 记录时间 */
private LocalDateTime recordTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
}
@@ -0,0 +1,67 @@
package com.water.production.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 自动投加策略配置(基于原水水质/流量联动)
*/
@Data
@TableName("prod_dosing_strategy")
public class DosingStrategy {
@TableId(type = IdType.AUTO)
private Long id;
/** 策略名称 */
private String strategyName;
/** 工艺段 */
private String processStage;
/** 药剂名称 */
private String chemicalName;
/** 策略类型: auto/manual/semi-auto */
private String strategyType;
/** 基础投加速率 */
private BigDecimal baseDosingRate;
/** 最小投加速率 */
private BigDecimal minDosingRate;
/** 最大投加速率 */
private BigDecimal maxDosingRate;
/** 浊度阈值联动 */
private BigDecimal turbidityThreshold;
/** 流量阈值联动 */
private BigDecimal flowThreshold;
/** pH下限 */
private BigDecimal phThresholdMin;
/** pH上限 */
private BigDecimal phThresholdMax;
/** 投加公式 */
private String formula;
/** 是否启用 */
private Boolean enabled;
/** 站点 */
private String station;
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}
@@ -0,0 +1,28 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.ChemicalDosing;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface ChemicalDosingMapper extends BaseMapper<ChemicalDosing> {
/** 按工艺段统计今日投加量 */
@Select("SELECT process_stage, chemical_name, SUM(dosing_amount) as total_amount " +
"FROM prod_chemical_dosing WHERE DATE(created_time) = CURRENT_DATE " +
"GROUP BY process_stage, chemical_name")
List<Map<String, Object>> dailyStatistics();
/** 按时间段统计药耗 */
@Select("SELECT process_stage, chemical_name, SUM(dosing_amount) as total_amount, " +
"COUNT(*) as record_count FROM prod_chemical_dosing " +
"WHERE created_time BETWEEN #{startTime} AND #{endTime} " +
"GROUP BY process_stage, chemical_name ORDER BY total_amount DESC")
List<Map<String, Object>> statisticsByPeriod(@Param("startTime") String startTime,
@Param("endTime") String endTime);
}
@@ -0,0 +1,9 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.ChemicalStock;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChemicalStockMapper extends BaseMapper<ChemicalStock> {
}
@@ -0,0 +1,24 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.DosingRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface DosingRecordMapper extends BaseMapper<DosingRecord> {
/** 趋势分析:按小时聚合投加量 */
@Select("SELECT DATE_TRUNC('hour', record_time) as time_bucket, " +
"AVG(dosing_rate) as avg_rate, SUM(dosing_amount) as total_amount " +
"FROM prod_dosing_record WHERE process_stage = #{stage} " +
"AND record_time BETWEEN #{startTime} AND #{endTime} " +
"GROUP BY time_bucket ORDER BY time_bucket")
List<Map<String, Object>> trendByHour(@Param("stage") String stage,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
}
@@ -0,0 +1,9 @@
package com.water.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.production.entity.DosingStrategy;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DosingStrategyMapper extends BaseMapper<DosingStrategy> {
}
@@ -0,0 +1,308 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.production.entity.ChemicalDosing;
import com.water.production.entity.ChemicalStock;
import com.water.production.entity.DosingRecord;
import com.water.production.entity.DosingStrategy;
import com.water.production.mapper.ChemicalDosingMapper;
import com.water.production.mapper.ChemicalStockMapper;
import com.water.production.mapper.DosingRecordMapper;
import com.water.production.mapper.DosingStrategyMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 全工艺药剂投加监控服务
* 涵盖混凝→沉淀→过滤→消毒全流程
*/
@Service
@RequiredArgsConstructor
public class ChemicalDosingService {
private final ChemicalDosingMapper dosingMapper;
private final DosingRecordMapper recordMapper;
private final ChemicalStockMapper stockMapper;
private final DosingStrategyMapper strategyMapper;
// ==================== 药剂投加监控 ====================
/** 分页查询投加记录 */
public Map<String, Object> listDosing(int page, int size, String processStage, String station, String status) {
LambdaQueryWrapper<ChemicalDosing> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(processStage), ChemicalDosing::getProcessStage, processStage)
.eq(StringUtils.hasText(station), ChemicalDosing::getStation, station)
.eq(StringUtils.hasText(status), ChemicalDosing::getStatus, status)
.orderByDesc(ChemicalDosing::getCreatedTime);
Page<ChemicalDosing> result = dosingMapper.selectPage(new Page<>(page, size), wrapper);
Map<String, Object> data = new HashMap<>();
data.put("records", result.getRecords());
data.put("total", result.getTotal());
data.put("pages", result.getPages());
return data;
}
/** 获取投加详情 */
public ChemicalDosing getDosingById(Long id) {
return dosingMapper.selectById(id);
}
/** 新增投加记录 */
public ChemicalDosing createDosing(ChemicalDosing dosing) {
if (dosing.getStatus() == null) {
dosing.setStatus("active");
}
dosing.setCreatedTime(LocalDateTime.now());
dosing.setUpdatedTime(LocalDateTime.now());
dosingMapper.insert(dosing);
saveDosingRecord(dosing);
return dosing;
}
/** 更新投加记录 */
public boolean updateDosing(Long id, ChemicalDosing dosing) {
dosing.setId(id);
dosing.setUpdatedTime(LocalDateTime.now());
int rows = dosingMapper.updateById(dosing);
if (rows > 0) {
saveDosingRecord(dosing);
}
return rows > 0;
}
/** 删除投加记录 */
public boolean deleteDosing(Long id) {
return dosingMapper.deleteById(id) > 0;
}
// ==================== 投加记录 + 趋势分析 ====================
/** 投加记录列表 */
public Map<String, Object> listRecords(int page, int size, String processStage, String chemicalName,
String startTime, String endTime) {
LambdaQueryWrapper<DosingRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(processStage), DosingRecord::getProcessStage, processStage)
.eq(StringUtils.hasText(chemicalName), DosingRecord::getChemicalName, chemicalName);
if (StringUtils.hasText(startTime)) {
wrapper.ge(DosingRecord::getRecordTime, LocalDateTime.parse(startTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
if (StringUtils.hasText(endTime)) {
wrapper.le(DosingRecord::getRecordTime, LocalDateTime.parse(endTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
wrapper.orderByDesc(DosingRecord::getRecordTime);
Page<DosingRecord> result = recordMapper.selectPage(new Page<>(page, size), wrapper);
Map<String, Object> data = new HashMap<>();
data.put("records", result.getRecords());
data.put("total", result.getTotal());
return data;
}
/** 趋势分析 */
public List<Map<String, Object>> trendAnalysis(String processStage, String startTime, String endTime) {
return recordMapper.trendByHour(processStage, startTime, endTime);
}
// ==================== 药耗统计 ====================
/** 药耗统计:日/周/月 + 单位产水药耗 */
public Map<String, Object> statistics(String period, String station) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime start;
LocalDateTime end = now;
switch (period != null ? period : "day") {
case "week":
start = now.minusWeeks(1);
break;
case "month":
start = now.minusMonths(1);
break;
default:
start = now.minusDays(1);
}
String startTime = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String endTime = end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
List<Map<String, Object>> rawStats = dosingMapper.statisticsByPeriod(startTime, endTime);
if (StringUtils.hasText(station)) {
rawStats = rawStats.stream()
.filter(m -> station.equals(m.get("station")))
.collect(Collectors.toList());
}
BigDecimal totalAmount = rawStats.stream()
.map(m -> m.get("total_amount") != null ? new BigDecimal(m.get("total_amount").toString()) : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Object> result = new HashMap<>();
result.put("period", period);
result.put("details", rawStats);
result.put("totalAmount", totalAmount.setScale(4, RoundingMode.HALF_UP));
result.put("unitConsumption", calculateUnitConsumption(totalAmount, start, end));
return result;
}
private BigDecimal calculateUnitConsumption(BigDecimal totalAmount, LocalDateTime start, LocalDateTime end) {
long hours = java.time.Duration.between(start, end).toHours();
if (hours <= 0) hours = 1;
BigDecimal totalWater = BigDecimal.valueOf(hours * 100L);
return totalAmount.divide(totalWater, 6, RoundingMode.HALF_UP);
}
// ==================== 投加策略 ====================
public List<DosingStrategy> listStrategies(String processStage, Boolean enabled) {
LambdaQueryWrapper<DosingStrategy> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(processStage), DosingStrategy::getProcessStage, processStage)
.eq(enabled != null, DosingStrategy::getEnabled, enabled)
.orderByAsc(DosingStrategy::getProcessStage);
return strategyMapper.selectList(wrapper);
}
public DosingStrategy getStrategyById(Long id) {
return strategyMapper.selectById(id);
}
public DosingStrategy createStrategy(DosingStrategy strategy) {
strategy.setCreatedTime(LocalDateTime.now());
strategy.setUpdatedTime(LocalDateTime.now());
if (strategy.getEnabled() == null) strategy.setEnabled(true);
strategyMapper.insert(strategy);
return strategy;
}
public boolean updateStrategy(Long id, DosingStrategy strategy) {
strategy.setId(id);
strategy.setUpdatedTime(LocalDateTime.now());
return strategyMapper.updateById(strategy) > 0;
}
public boolean deleteStrategy(Long id) {
return strategyMapper.deleteById(id) > 0;
}
/** 根据策略计算推荐投加量 */
public Map<String, Object> calculateRecommendedDosing(Long strategyId, BigDecimal currentTurbidity,
BigDecimal currentFlow, BigDecimal currentPh) {
DosingStrategy strategy = strategyMapper.selectById(strategyId);
if (strategy == null || !Boolean.TRUE.equals(strategy.getEnabled())) {
return Map.of("error", "策略不存在或未启用");
}
BigDecimal recommendedRate = strategy.getBaseDosingRate() != null ? strategy.getBaseDosingRate() : BigDecimal.ZERO;
if (currentTurbidity != null && strategy.getTurbidityThreshold() != null) {
if (currentTurbidity.compareTo(strategy.getTurbidityThreshold()) > 0) {
BigDecimal factor = currentTurbidity.divide(strategy.getTurbidityThreshold(), 4, RoundingMode.HALF_UP);
recommendedRate = recommendedRate.multiply(factor);
}
}
if (currentFlow != null && strategy.getFlowThreshold() != null) {
if (currentFlow.compareTo(strategy.getFlowThreshold()) > 0) {
BigDecimal flowFactor = currentFlow.divide(strategy.getFlowThreshold(), 4, RoundingMode.HALF_UP);
recommendedRate = recommendedRate.multiply(flowFactor);
}
}
if (strategy.getMinDosingRate() != null) {
recommendedRate = recommendedRate.max(strategy.getMinDosingRate());
}
if (strategy.getMaxDosingRate() != null) {
recommendedRate = recommendedRate.min(strategy.getMaxDosingRate());
}
Map<String, Object> result = new HashMap<>();
result.put("strategyId", strategyId);
result.put("strategyName", strategy.getStrategyName());
result.put("recommendedRate", recommendedRate.setScale(4, RoundingMode.HALF_UP));
result.put("chemicalName", strategy.getChemicalName());
result.put("processStage", strategy.getProcessStage());
return result;
}
// ==================== 药剂库存 ====================
public List<ChemicalStock> listStocks(String station, String status) {
LambdaQueryWrapper<ChemicalStock> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(station), ChemicalStock::getStation, station)
.eq(StringUtils.hasText(status), ChemicalStock::getStatus, status)
.orderByAsc(ChemicalStock::getStatus);
return stockMapper.selectList(wrapper);
}
public ChemicalStock createStock(ChemicalStock stock) {
stock.setCreatedTime(LocalDateTime.now());
stock.setUpdatedTime(LocalDateTime.now());
updateStockStatus(stock);
stockMapper.insert(stock);
return stock;
}
public boolean updateStock(Long id, ChemicalStock stock) {
stock.setId(id);
stock.setUpdatedTime(LocalDateTime.now());
updateStockStatus(stock);
return stockMapper.updateById(stock) > 0;
}
public List<Map<String, Object>> lowStockAlerts() {
LambdaQueryWrapper<ChemicalStock> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ChemicalStock::getStatus, "low").or().eq(ChemicalStock::getStatus, "out");
List<ChemicalStock> lowStocks = stockMapper.selectList(wrapper);
return lowStocks.stream().map(s -> {
Map<String, Object> alert = new HashMap<>();
alert.put("id", s.getId());
alert.put("chemicalName", s.getChemicalName());
alert.put("currentStock", s.getCurrentStock());
alert.put("minStock", s.getMinStock());
alert.put("status", s.getStatus());
alert.put("station", s.getStation());
alert.put("warehouse", s.getWarehouse());
BigDecimal deficit = s.getMinStock() != null && s.getCurrentStock() != null
? s.getMinStock().subtract(s.getCurrentStock()) : BigDecimal.ZERO;
alert.put("deficit", deficit.max(BigDecimal.ZERO));
return alert;
}).collect(Collectors.toList());
}
private void updateStockStatus(ChemicalStock stock) {
if (stock.getCurrentStock() == null || stock.getMinStock() == null) {
stock.setStatus("normal");
return;
}
if (stock.getCurrentStock().compareTo(BigDecimal.ZERO) <= 0) {
stock.setStatus("out");
} else if (stock.getCurrentStock().compareTo(stock.getMinStock()) < 0) {
stock.setStatus("low");
} else {
stock.setStatus("normal");
}
}
private void saveDosingRecord(ChemicalDosing dosing) {
DosingRecord record = new DosingRecord();
record.setDosingId(dosing.getId());
record.setProcessStage(dosing.getProcessStage());
record.setChemicalName(dosing.getChemicalName());
record.setDosingAmount(dosing.getDosingAmount());
record.setDosingRate(dosing.getDosingRate());
record.setConcentration(dosing.getConcentration());
record.setFlowRate(dosing.getFlowRate());
record.setStation(dosing.getStation());
record.setRecordTime(LocalDateTime.now());
record.setCreatedTime(LocalDateTime.now());
recordMapper.insert(record);
}
}
@@ -0,0 +1,218 @@
package com.water.production.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.production.entity.ChemicalDosing;
import com.water.production.entity.ChemicalStock;
import com.water.production.entity.DosingStrategy;
import com.water.production.mapper.ChemicalDosingMapper;
import com.water.production.mapper.ChemicalStockMapper;
import com.water.production.mapper.DosingRecordMapper;
import com.water.production.mapper.DosingStrategyMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* ChemicalDosingService 单元测试
* 测试药剂投加监控、库存管理、投加策略计算等核心逻辑
*/
class ChemicalDosingServiceTest {
private ChemicalDosingService service;
private ChemicalDosingMapper dosingMapper;
private DosingRecordMapper recordMapper;
private ChemicalStockMapper stockMapper;
private DosingStrategyMapper strategyMapper;
@BeforeEach
void setUp() {
dosingMapper = mock(ChemicalDosingMapper.class);
recordMapper = mock(DosingRecordMapper.class);
stockMapper = mock(ChemicalStockMapper.class);
strategyMapper = mock(DosingStrategyMapper.class);
service = new ChemicalDosingService(dosingMapper, recordMapper, stockMapper, strategyMapper);
}
@Test
@DisplayName("测试新增投加记录 - 自动同步历史记录")
void testCreateDosing_autoSyncRecord() {
ChemicalDosing dosing = new ChemicalDosing();
dosing.setProcessStage("coagulation");
dosing.setChemicalName("聚合氯化铝");
dosing.setDosingAmount(new BigDecimal("10.5"));
dosing.setDosingRate(new BigDecimal("2.5"));
dosing.setStation("一水厂");
when(dosingMapper.insert(any())).thenReturn(1);
when(recordMapper.insert(any())).thenReturn(1);
ChemicalDosing result = service.createDosing(dosing);
assertNotNull(result);
assertEquals("active", result.getStatus());
assertNotNull(result.getCreatedTime());
verify(dosingMapper).insert(dosing);
verify(recordMapper).insert(any());
}
@Test
@DisplayName("测试删除投加记录")
void testDeleteDosing() {
when(dosingMapper.deleteById(1L)).thenReturn(1);
when(dosingMapper.deleteById(999L)).thenReturn(0);
assertTrue(service.deleteDosing(1L));
assertFalse(service.deleteDosing(999L));
}
@Test
@DisplayName("测试库存状态自动更新 - 低库存预警")
void testStockStatusAutoUpdate_low() {
ChemicalStock stock = new ChemicalStock();
stock.setChemicalName("次氯酸钠");
stock.setCurrentStock(new BigDecimal("50"));
stock.setMinStock(new BigDecimal("100"));
stock.setStation("一水厂");
when(stockMapper.insert(any())).thenReturn(1);
ChemicalStock result = service.createStock(stock);
assertEquals("low", result.getStatus());
verify(stockMapper).insert(stock);
}
@Test
@DisplayName("测试库存状态自动更新 - 缺货")
void testStockStatusAutoUpdate_out() {
ChemicalStock stock = new ChemicalStock();
stock.setChemicalName("聚合氯化铝");
stock.setCurrentStock(BigDecimal.ZERO);
stock.setMinStock(new BigDecimal("100"));
when(stockMapper.insert(any())).thenReturn(1);
ChemicalStock result = service.createStock(stock);
assertEquals("out", result.getStatus());
}
@Test
@DisplayName("测试库存状态自动更新 - 正常")
void testStockStatusAutoUpdate_normal() {
ChemicalStock stock = new ChemicalStock();
stock.setChemicalName("聚丙烯酰胺");
stock.setCurrentStock(new BigDecimal("500"));
stock.setMinStock(new BigDecimal("100"));
when(stockMapper.insert(any())).thenReturn(1);
ChemicalStock result = service.createStock(stock);
assertEquals("normal", result.getStatus());
}
@Test
@DisplayName("测试策略推荐投加量计算 - 浊度联动")
void testCalculateRecommended_turbidityLinkage() {
DosingStrategy strategy = new DosingStrategy();
strategy.setId(1L);
strategy.setStrategyName("混凝自动投加");
strategy.setProcessStage("coagulation");
strategy.setChemicalName("聚合氯化铝");
strategy.setEnabled(true);
strategy.setBaseDosingRate(new BigDecimal("10.0"));
strategy.setTurbidityThreshold(new BigDecimal("5.0"));
strategy.setMinDosingRate(new BigDecimal("5.0"));
strategy.setMaxDosingRate(new BigDecimal("50.0"));
when(strategyMapper.selectById(1L)).thenReturn(strategy);
// 浊度=10 > 阈值5, 因子=2, 推荐=10*2=20
Map<String, Object> result = service.calculateRecommendedDosing(
1L, new BigDecimal("10.0"), null, null);
assertNotNull(result);
assertEquals("聚合氯化铝", result.get("chemicalName"));
BigDecimal rate = (BigDecimal) result.get("recommendedRate");
assertEquals(0, rate.compareTo(new BigDecimal("20.0000")));
}
@Test
@DisplayName("测试策略推荐投加量计算 - 超出最大值被限制")
void testCalculateRecommended_cappedAtMax() {
DosingStrategy strategy = new DosingStrategy();
strategy.setId(2L);
strategy.setStrategyName("消毒投加");
strategy.setProcessStage("disinfection");
strategy.setChemicalName("次氯酸钠");
strategy.setEnabled(true);
strategy.setBaseDosingRate(new BigDecimal("10.0"));
strategy.setTurbidityThreshold(new BigDecimal("1.0"));
strategy.setMaxDosingRate(new BigDecimal("15.0"));
strategy.setMinDosingRate(new BigDecimal("2.0"));
when(strategyMapper.selectById(2L)).thenReturn(strategy);
// 浊度=100, 因子=100, 推荐=1000 → 被max限制为15
Map<String, Object> result = service.calculateRecommendedDosing(
2L, new BigDecimal("100.0"), null, null);
BigDecimal rate = (BigDecimal) result.get("recommendedRate");
assertEquals(0, rate.compareTo(new BigDecimal("15.0000")));
}
@Test
@DisplayName("测试策略推荐 - 策略不存在时返回错误")
void testCalculateRecommended_strategyNotFound() {
when(strategyMapper.selectById(999L)).thenReturn(null);
Map<String, Object> result = service.calculateRecommendedDosing(
999L, new BigDecimal("5.0"), null, null);
assertTrue(result.containsKey("error"));
}
@Test
@DisplayName("测试低库存预警列表")
void testLowStockAlerts() {
ChemicalStock lowStock = new ChemicalStock();
lowStock.setId(1L);
lowStock.setChemicalName("聚合氯化铝");
lowStock.setCurrentStock(new BigDecimal("30"));
lowStock.setMinStock(new BigDecimal("100"));
lowStock.setStatus("low");
lowStock.setStation("一水厂");
when(stockMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(lowStock));
List<Map<String, Object>> alerts = service.lowStockAlerts();
assertFalse(alerts.isEmpty());
Map<String, Object> alert = alerts.get(0);
assertEquals("聚合氯化铝", alert.get("chemicalName"));
assertEquals("low", alert.get("status"));
BigDecimal deficit = (BigDecimal) alert.get("deficit");
assertEquals(0, deficit.compareTo(new BigDecimal("70")));
}
@Test
@DisplayName("测试创建投加策略 - 默认启用")
void testCreateStrategy_defaultEnabled() {
DosingStrategy strategy = new DosingStrategy();
strategy.setStrategyName("测试策略");
strategy.setProcessStage("filtration");
strategy.setChemicalName("活性炭");
when(strategyMapper.insert(any())).thenReturn(1);
DosingStrategy result = service.createStrategy(strategy);
assertTrue(result.getEnabled());
assertNotNull(result.getCreatedTime());
}
}