feat(wm-production): #65 全工艺药剂投加监控
This commit is contained in:
+172
@@ -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);
|
||||
}
|
||||
}
|
||||
+218
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user