diff --git a/wm-production/pom.xml b/wm-production/pom.xml index d1a8893a..f81b1a8b 100644 --- a/wm-production/pom.xml +++ b/wm-production/pom.xml @@ -14,6 +14,5 @@ org.postgresqlpostgresql com.alibabaeasyexcel com.github.xiaoyminknife4j-openapi3-jakarta-spring-boot-starter - org.springframework.bootspring-boot-starter-testtest \ No newline at end of file diff --git a/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java b/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java index 1b69115e..af5049f5 100644 --- a/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java +++ b/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java @@ -1,5 +1,6 @@ package com.water.production.controller; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.water.common.core.result.R; import com.water.production.dto.QualityQueryRequest; import com.water.production.dto.QualityStatVO; @@ -11,21 +12,14 @@ import com.water.production.service.QualityStandardService; import com.water.production.service.QualityTestPlanService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import java.io.IOException; import java.util.List; import java.util.Map; -/** - * 水质检测台账 Controller - * 提供检测记录 CRUD、标准管理、检测计划、统计分析、数据导出等接口 - */ @Tag(name = "水质检测台账管理") @RestController @RequestMapping("/api/production/quality") @@ -36,209 +30,173 @@ public class QualityLedgerController { private final QualityStandardService standardService; private final QualityTestPlanService planService; - // ==================== 检测记录 CRUD ==================== + // ==================== 记录 CRUD ==================== - // 1. 分页查询台账 - @Operation(summary = "分页查询水质检测台账") + @Operation(summary = "查询检测记录(分页)") @GetMapping("/records") - public R> listRecords(QualityQueryRequest request) { + public R> queryRecords(QualityQueryRequest request) { return R.ok(ledgerService.queryRecords(request)); } - // 2. 获取记录详情 @Operation(summary = "获取检测记录详情") @GetMapping("/records/{id}") public R getRecord(@PathVariable Long id) { - QualityTestRecord record = ledgerService.getById(id); - if (record == null) return R.fail(404, "记录不存在"); - return R.ok(record); + return R.ok(ledgerService.getById(id)); } - // 3. 创建检测记录 - @Operation(summary = "创建水质检测记录 (自动合格判定)") + @Operation(summary = "创建检测记录") @PostMapping("/records") public R createRecord(@RequestBody QualityTestRecord record) { return R.ok(ledgerService.create(record)); } - // 4. 更新检测记录 - @Operation(summary = "更新检测记录 (重新合格判定)") + @Operation(summary = "更新检测记录") @PutMapping("/records/{id}") - public R updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) { - record.setId(id); - ledgerService.update(record); - return R.ok("更新成功"); + public R updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) { + return R.ok(ledgerService.update(id, record)); } - // 5. 删除检测记录 @Operation(summary = "删除检测记录") @DeleteMapping("/records/{id}") - public R deleteRecord(@PathVariable Long id) { + public R deleteRecord(@PathVariable Long id) { ledgerService.delete(id); - return R.ok("删除成功"); + return R.ok(); } - // 6. 批量删除 @Operation(summary = "批量删除检测记录") @DeleteMapping("/records/batch") - public R batchDeleteRecords(@RequestBody List ids) { + public R batchDeleteRecords(@RequestBody List ids) { ledgerService.batchDelete(ids); - return R.ok("批量删除成功"); + return R.ok(); } - // 7. 重新判定合格状态 - @Operation(summary = "重新判定所有记录合格状态") + @Operation(summary = "重新判定所有记录") @PostMapping("/records/reevaluate") - public R> reevaluateRecords() { + public R> reevaluateAll() { int count = ledgerService.reevaluateAll(); - return R.ok(Map.of("processed", count)); + return R.ok(Map.of("updatedCount", count)); } - // 8. 获取区域列表 - @Operation(summary = "获取所有检测区域") + // ==================== 辅助 ==================== + + @Operation(summary = "获取区域列表") @GetMapping("/areas") public R> getAreas() { return R.ok(ledgerService.getAreaList()); } - // 9. 获取采样点列表 - @Operation(summary = "获取所有采样点") + @Operation(summary = "获取采样点列表") @GetMapping("/sampling-points") public R> getSamplingPoints() { return R.ok(ledgerService.getSamplingPointList()); } - // ==================== 水质标准管理 ==================== + // ==================== 标准 ==================== - // 10. 获取所有启用的标准 - @Operation(summary = "获取启用中的水质标准列表") + @Operation(summary = "获取启用的标准列表") @GetMapping("/standards") public R> listStandards() { return R.ok(standardService.listEnabled()); } - // 11. 获取全部标准 (含停用) - @Operation(summary = "获取所有水质标准 (含停用)") + @Operation(summary = "获取全部标准") @GetMapping("/standards/all") public R> listAllStandards() { return R.ok(standardService.listAll()); } - // 12. 按水样类型获取标准 - @Operation(summary = "按水样类型获取水质标准") + @Operation(summary = "按水质类型获取标准") @GetMapping("/standards/water-type/{waterType}") public R> listStandardsByWaterType(@PathVariable String waterType) { return R.ok(standardService.listByWaterType(waterType)); } - // 13. 获取标准详情 - @Operation(summary = "获取水质标准详情") + @Operation(summary = "获取标准详情") @GetMapping("/standards/{id}") public R getStandard(@PathVariable Long id) { - QualityStandard standard = standardService.getById(id); - if (standard == null) return R.fail(404, "标准不存在"); - return R.ok(standard); + return R.ok(standardService.getById(id)); } - // 14. 创建标准 - @Operation(summary = "创建水质标准") + @Operation(summary = "创建标准") @PostMapping("/standards") public R createStandard(@RequestBody QualityStandard standard) { return R.ok(standardService.create(standard)); } - // 15. 更新标准 - @Operation(summary = "更新水质标准") + @Operation(summary = "更新标准") @PutMapping("/standards/{id}") - public R updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) { - standard.setId(id); - standardService.update(standard); - return R.ok("更新成功"); + public R updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) { + return R.ok(standardService.update(id, standard)); } - // 16. 删除标准 - @Operation(summary = "删除水质标准") + @Operation(summary = "删除标准") @DeleteMapping("/standards/{id}") - public R deleteStandard(@PathVariable Long id) { + public R deleteStandard(@PathVariable Long id) { standardService.delete(id); - return R.ok("删除成功"); + return R.ok(); } - // ==================== 检测计划管理 ==================== + // ==================== 计划 ==================== - // 17. 分页查询检测计划 - @Operation(summary = "分页查询检测计划") + @Operation(summary = "查询检测计划(分页)") @GetMapping("/plans") - public R> listPlans( - @RequestParam(required = false) String status, - @RequestParam(required = false) String frequency, - @RequestParam(required = false) String waterType, - @RequestParam(required = false) String keyword, + public R> queryPlans( @RequestParam(defaultValue = "1") int pageNum, - @RequestParam(defaultValue = "20") int pageSize) { - return R.ok(planService.queryPlans(status, frequency, waterType, keyword, pageNum, pageSize)); + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String testType, + @RequestParam(required = false) String waterType, + @RequestParam(required = false) String area, + @RequestParam(required = false) String status, + @RequestParam(required = false) String keyword) { + return R.ok(planService.queryPlans(pageNum, pageSize, testType, waterType, area, status, keyword)); } - // 18. 获取计划详情 - @Operation(summary = "获取检测计划详情") + @Operation(summary = "获取计划详情") @GetMapping("/plans/{id}") public R getPlan(@PathVariable Long id) { - QualityTestPlan plan = planService.getById(id); - if (plan == null) return R.fail(404, "计划不存在"); - return R.ok(plan); + return R.ok(planService.getById(id)); } - // 19. 创建检测计划 - @Operation(summary = "创建检测计划") + @Operation(summary = "创建计划") @PostMapping("/plans") public R createPlan(@RequestBody QualityTestPlan plan) { return R.ok(planService.create(plan)); } - // 20. 更新检测计划 - @Operation(summary = "更新检测计划") + @Operation(summary = "更新计划") @PutMapping("/plans/{id}") - public R updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) { - plan.setId(id); - planService.update(plan); - return R.ok("更新成功"); + public R updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) { + return R.ok(planService.update(id, plan)); } - // 21. 删除检测计划 - @Operation(summary = "删除检测计划") + @Operation(summary = "删除计划") @DeleteMapping("/plans/{id}") - public R deletePlan(@PathVariable Long id) { + public R deletePlan(@PathVariable Long id) { planService.delete(id); - return R.ok("删除成功"); + return R.ok(); } - // 22. 暂停/恢复计划 - @Operation(summary = "切换检测计划状态 (active/paused/completed)") + @Operation(summary = "切换计划状态") @PutMapping("/plans/{id}/status") - public R togglePlanStatus(@PathVariable Long id, @RequestParam String status) { - planService.toggleStatus(id, status); - return R.ok("状态已更新"); + public R togglePlanStatus(@PathVariable Long id) { + return R.ok(planService.toggleStatus(id)); } - // 23. 获取到期计划 - @Operation(summary = "获取当前到期的检测计划") + @Operation(summary = "获取到期计划") @GetMapping("/plans/due") public R> getDuePlans() { return R.ok(planService.getDuePlans()); } - // 24. 标记计划已执行 - @Operation(summary = "标记检测计划已执行 (更新下次检测日期)") + @Operation(summary = "标记计划已执行") @PostMapping("/plans/{id}/execute") - public R markPlanExecuted(@PathVariable Long id) { - planService.markExecuted(id); - return R.ok("已标记执行"); + public R markPlanExecuted(@PathVariable Long id) { + return R.ok(planService.markExecuted(id)); } - // ==================== 统计分析 ==================== + // ==================== 统计 ==================== - // 25. 综合统计 - @Operation(summary = "水质检测统计分析 (合格率/趋势/指标分布)") + @Operation(summary = "获取统计数据") @GetMapping("/statistics") public R getStatistics( @RequestParam(required = false) String startDate, @@ -246,18 +204,11 @@ public class QualityLedgerController { return R.ok(ledgerService.getStatistics(startDate, endDate)); } - // ==================== 数据导出 ==================== + // ==================== 导出 ==================== - // 26. 导出 Excel - @Operation(summary = "导出质检台账 Excel") + @Operation(summary = "导出Excel") @PostMapping("/export/excel") - public ResponseEntity exportExcel(@RequestBody QualityQueryRequest request) { - byte[] data = ledgerService.exportExcel(request); - String filename = URLEncoder.encode("水质检测台账.xlsx", StandardCharsets.UTF_8); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename) - .contentType(MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) - .body(data); + public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException { + ledgerService.exportExcel(request, response); } } diff --git a/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java b/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java index ddb46aae..b342e236 100644 --- a/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java +++ b/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java @@ -3,47 +3,47 @@ package com.water.production.dto; import lombok.Data; /** - * 水质检测台账查询请求 + * 水质检测查询请求 */ @Data public class QualityQueryRequest { - /** 检测类型: routine/special/complaint */ + /** 检测类型 */ private String testType; - /** 水样类型: raw/treated/network */ + /** 水质类型 */ private String waterType; - /** 所属区域 */ + /** 区域 */ private String area; - /** 采样点 (模糊搜索) */ + /** 采样点 */ private String samplingPoint; - /** 检测人 (模糊搜索) */ + /** 检测人 */ private String tester; - /** 合格状态: qualified/unqualified/pending */ + /** 合格状态 */ private String complianceStatus; - /** 开始日期 (yyyy-MM-dd) */ + /** 开始日期 */ private String startDate; - /** 结束日期 (yyyy-MM-dd) */ + /** 结束日期 */ private String endDate; - /** 关键词搜索 */ + /** 关键字 */ private String keyword; /** 排序字段 */ private String sortField; - /** 排序方向: asc/desc */ + /** 排序方向 */ private String sortOrder; - /** 页码 (默认1) */ + /** 页码 */ private Integer pageNum = 1; - /** 每页条数 (默认20) */ - private Integer pageSize = 20; + /** 每页大小 */ + private Integer pageSize = 10; } diff --git a/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java b/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java index c0c3c3d9..4e3b19e8 100644 --- a/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java +++ b/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java @@ -2,18 +2,17 @@ package com.water.production.dto; import lombok.Data; -import java.math.BigDecimal; import java.util.List; import java.util.Map; /** - * 水质检测统计 VO + * 水质检测统计VO */ @Data public class QualityStatVO { - /** 总检测记录数 */ - private Long totalCount; + /** 总记录数 */ + private Long totalRecords; /** 合格数 */ private Long qualifiedCount; @@ -24,21 +23,30 @@ public class QualityStatVO { /** 待判定数 */ private Long pendingCount; - /** 综合合格率 (%) */ - private BigDecimal qualifiedRate; + /** 合格率 */ + private Double qualifiedRate; - /** 各水样类型合格率 */ - private Map rateByWaterType; + /** 按水质类型统计 */ + private List> byWaterType; - /** 各区域合格率 */ - private Map rateByArea; + /** 按区域统计 */ + private List> byArea; - /** 各指标不合格次数 */ - private Map unqualifiedByParam; + /** 按检测类型统计 */ + private List> byTestType; - /** 月度合格率趋势 [{month, rate}] */ - private List> monthlyTrend; + /** 按日期趋势 */ + private List> trendByDate; - /** 各指标均值统计 */ - private Map> paramAvgStats; + /** 不合格项目统计 */ + private List> unqualifiedItems; + + /** 平均浊度 */ + private Double avgTurbidity; + + /** 平均pH */ + private Double avgPh; + + /** 平均余氯 */ + private Double avgResidualChlorine; } diff --git a/wm-production/src/main/java/com/water/production/entity/QualityStandard.java b/wm-production/src/main/java/com/water/production/entity/QualityStandard.java index 0cface9f..2a2202a4 100644 --- a/wm-production/src/main/java/com/water/production/entity/QualityStandard.java +++ b/wm-production/src/main/java/com/water/production/entity/QualityStandard.java @@ -3,11 +3,10 @@ package com.water.production.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; -import java.math.BigDecimal; import java.time.LocalDateTime; /** - * 水质标准实体 (基于 GB5749-2022) + * 水质标准 */ @Data @TableName("prod_quality_standard") @@ -19,39 +18,36 @@ public class QualityStandard { /** 标准名称 */ private String standardName; - /** 标准编码 (如 GB5749-2022) */ + /** 标准编号 */ private String standardCode; - /** 参数名称: turbidity/ph/residual_chlorine/color/odor/ecoli/colony_count */ + /** 参数名 */ private String paramName; - /** 参数显示名称 */ + /** 参数标签 */ private String paramLabel; /** 参数单位 */ private String paramUnit; - /** 最小值 (null 表示无下限) */ - private BigDecimal minValue; + /** 最小值 */ + private Double minValue; - /** 最大值 (null 表示无上限) */ - private BigDecimal maxValue; + /** 最大值 */ + private Double maxValue; - /** 适用水样类型: raw/treated/network/all */ + /** 水质类型 */ private String waterType; /** 是否启用 */ - private Integer enabled; + private Boolean enabled; - /** 逻辑删除 */ @TableLogic private Integer deleted; - /** 创建时间 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; - /** 更新时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; } diff --git a/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java b/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java index 07b52e04..8b2ab190 100644 --- a/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java +++ b/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java @@ -7,7 +7,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; /** - * 水质检测计划实体 + * 水质检测计划 */ @Data @TableName("prod_quality_test_plan") @@ -19,34 +19,34 @@ public class QualityTestPlan { /** 计划名称 */ private String planName; - /** 检测类型: routine/special */ + /** 检测类型 */ private String testType; - /** 水样类型: raw/treated/network */ + /** 水质类型 */ private String waterType; /** 采样点 */ private String samplingPoint; - /** 所属区域 */ + /** 区域 */ private String area; - /** 检测频率: daily/weekly/monthly */ + /** 频率: daily/weekly/monthly */ private String frequency; - /** 检测参数 (逗号分隔: turbidity,ph,residual_chlorine) */ + /** 检测参数 (JSON数组) */ private String testParams; - /** 计划开始日期 */ + /** 开始日期 */ private LocalDate startDate; - /** 计划结束日期 (null=长期) */ + /** 结束日期 */ private LocalDate endDate; /** 下次检测日期 */ private LocalDate nextTestDate; - /** 计划状态: active/paused/completed */ + /** 状态: active/paused/expired */ private String status; /** 执行次数 */ @@ -55,15 +55,12 @@ public class QualityTestPlan { /** 备注 */ private String remark; - /** 逻辑删除 */ @TableLogic private Integer deleted; - /** 创建时间 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; - /** 更新时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; } diff --git a/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java b/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java index 33357004..a6d2dd92 100644 --- a/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java +++ b/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java @@ -2,11 +2,14 @@ package com.water.production.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; -import java.math.BigDecimal; + import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +/** + * 水质检测记录 + */ @Data @TableName("prod_quality_test_record") public class QualityTestRecord { @@ -14,30 +17,63 @@ public class QualityTestRecord { @TableId(type = IdType.AUTO) private Long id; - private String testType; // routine/special/complaint - private String waterType; // raw/treated/network + /** 检测类型: routine/emergency/special */ + private String testType; + + /** 水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater */ + private String waterType; + + /** 采样点 */ private String samplingPoint; + + /** 所属区域 */ private String area; + + /** 检测日期 */ private LocalDate testDate; + + /** 检测时间 */ private LocalTime testTime; + + /** 检测人 */ private String tester; - private BigDecimal turbidity; - private BigDecimal ph; - private BigDecimal residualChlorine; - private BigDecimal color; - private BigDecimal odor; - private BigDecimal ecoli; - private BigDecimal colonyCount; - private String complianceStatus; // qualified/unqualified/pending + + /** 浊度 (NTU) */ + private Double turbidity; + + /** pH值 */ + private Double ph; + + /** 余氯 (mg/L) */ + private Double residualChlorine; + + /** 色度 (度) */ + private Double color; + + /** 嗅味 */ + private String odor; + + /** 大肠杆菌 (CFU/100mL) */ + private Double ecoli; + + /** 菌落总数 (CFU/mL) */ + private Double colonyCount; + + /** 合格状态: qualified/unqualified/pending */ + private String complianceStatus; + + /** 不合格项目 (JSON) */ private String unqualifiedItems; + + /** 备注 */ private String remark; @TableLogic private Integer deleted; - @TableField("created_at") + @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; - @TableField("updated_at") + @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; } diff --git a/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java b/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java index c371d286..a8a01cf2 100644 --- a/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java +++ b/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java @@ -11,52 +11,58 @@ import java.util.Map; @Mapper public interface QualityTestRecordMapper extends BaseMapper { - /** 分页查询台账 (支持多维度筛选) */ - List> selectRecordPage(@Param("testType") String testType, - @Param("waterType") String waterType, - @Param("area") String area, - @Param("samplingPoint") String samplingPoint, - @Param("tester") String tester, - @Param("complianceStatus") String complianceStatus, - @Param("startDate") String startDate, - @Param("endDate") String endDate, - @Param("keyword") String keyword, - @Param("sortField") String sortField, - @Param("sortOrder") String sortOrder, - @Param("offset") int offset, - @Param("limit") int limit); + List> selectRecordPage( + @Param("testType") String testType, + @Param("waterType") String waterType, + @Param("area") String area, + @Param("samplingPoint") String samplingPoint, + @Param("tester") String tester, + @Param("complianceStatus") String complianceStatus, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("keyword") String keyword, + @Param("sortField") String sortField, + @Param("sortOrder") String sortOrder, + @Param("offset") int offset, + @Param("limit") int limit + ); - /** 统计符合条件的总数 */ - Long countRecords(@Param("testType") String testType, - @Param("waterType") String waterType, - @Param("area") String area, - @Param("samplingPoint") String samplingPoint, - @Param("tester") String tester, - @Param("complianceStatus") String complianceStatus, - @Param("startDate") String startDate, - @Param("endDate") String endDate, - @Param("keyword") String keyword); + Long countRecords( + @Param("testType") String testType, + @Param("waterType") String waterType, + @Param("area") String area, + @Param("samplingPoint") String samplingPoint, + @Param("tester") String tester, + @Param("complianceStatus") String complianceStatus, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("keyword") String keyword + ); - /** 按合格状态统计数量 */ - List> statByComplianceStatus(@Param("startDate") String startDate, - @Param("endDate") String endDate); + List> statByComplianceStatus( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); - /** 按水样类型统计合格率 */ - List> statRateByWaterType(@Param("startDate") String startDate, - @Param("endDate") String endDate); + List> statRateByWaterType( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); - /** 按区域统计合格率 */ - List> statRateByArea(@Param("startDate") String startDate, - @Param("endDate") String endDate); + List> statRateByArea( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); - /** 按指标统计不合格次数 */ - List> statUnqualifiedByParam(@Param("startDate") String startDate, - @Param("endDate") String endDate); + List> statUnqualifiedByParam( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); - /** 月度合格率趋势 */ - List> statMonthlyTrend(@Param("startDate") String startDate, - @Param("endDate") String endDate); + List> statMonthlyTrend( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); - /** 各指标均值统计 */ - List> statParamAvg(); + Map statParamAvg(); } diff --git a/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java b/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java index 24b44eef..803af132 100644 --- a/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java +++ b/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java @@ -2,26 +2,25 @@ package com.water.production.service; import com.alibaba.excel.EasyExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.common.core.exception.BusinessException; import com.water.production.dto.QualityQueryRequest; import com.water.production.dto.QualityStatVO; import com.water.production.entity.QualityStandard; import com.water.production.entity.QualityTestRecord; import com.water.production.mapper.QualityTestRecordMapper; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.math.BigDecimal; -import java.math.RoundingMode; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; -/** - * 水质检测台账管理服务 - * 包含:CRUD、合格判定、多维度查询、统计分析、数据导出 - */ @Slf4j @Service @RequiredArgsConstructor @@ -30,10 +29,8 @@ public class QualityLedgerService { private final QualityTestRecordMapper recordMapper; private final QualityStandardService standardService; - // ========== CRUD ========== - /** - * 分页查询台账 + * 分页查询检测记录 */ public Map queryRecords(QualityQueryRequest request) { int offset = (request.getPageNum() - 1) * request.getPageSize(); @@ -41,8 +38,7 @@ public class QualityLedgerService { request.getTestType(), request.getWaterType(), request.getArea(), request.getSamplingPoint(), request.getTester(), request.getComplianceStatus(), request.getStartDate(), request.getEndDate(), request.getKeyword(), - request.getSortField(), request.getSortOrder(), - offset, request.getPageSize() + request.getSortField(), request.getSortOrder(), offset, request.getPageSize() ); Long total = recordMapper.countRecords( request.getTestType(), request.getWaterType(), request.getArea(), @@ -50,366 +46,266 @@ public class QualityLedgerService { request.getStartDate(), request.getEndDate(), request.getKeyword() ); - int pages = (int) Math.ceil((double) total / request.getPageSize()); - return Map.of( - "records", records, - "total", total, - "pageNum", request.getPageNum(), - "pageSize", request.getPageSize(), - "pages", pages - ); + Map result = new LinkedHashMap<>(); + result.put("records", records); + result.put("total", total); + result.put("pageNum", request.getPageNum()); + result.put("pageSize", request.getPageSize()); + result.put("totalPages", (total + request.getPageSize() - 1) / request.getPageSize()); + return result; } /** * 获取记录详情 */ public QualityTestRecord getById(Long id) { - return recordMapper.selectById(id); - } - - /** - * 创建检测记录 (含自动合格判定) - */ - public QualityTestRecord create(QualityTestRecord record) { - record.setDeleted(0); - // 自动合格判定 - evaluateCompliance(record); - recordMapper.insert(record); + QualityTestRecord record = recordMapper.selectById(id); + if (record == null) { + throw new BusinessException("检测记录不存在"); + } return record; } /** - * 更新检测记录 (重新判定) + * 创建检测记录(自动合格判定) */ - public void update(QualityTestRecord record) { + @Transactional + public QualityTestRecord create(QualityTestRecord record) { + if (record.getTestDate() == null) record.setTestDate(LocalDate.now()); evaluateCompliance(record); - recordMapper.updateById(record); + recordMapper.insert(record); + log.info("创建水质检测记录: {}/{}", record.getWaterType(), record.getSamplingPoint()); + return record; } /** - * 删除检测记录 (逻辑删除) + * 更新检测记录 */ + @Transactional + public QualityTestRecord update(Long id, QualityTestRecord record) { + QualityTestRecord existing = getById(id); + record.setId(existing.getId()); + evaluateCompliance(record); + recordMapper.updateById(record); + log.info("更新水质检测记录: {}", id); + return recordMapper.selectById(id); + } + + /** + * 删除检测记录 + */ + @Transactional public void delete(Long id) { + getById(id); recordMapper.deleteById(id); + log.info("删除水质检测记录: {}", id); } /** * 批量删除 */ + @Transactional public void batchDelete(List ids) { + if (ids == null || ids.isEmpty()) return; recordMapper.deleteBatchIds(ids); + log.info("批量删除水质检测记录: {} 条", ids.size()); } - // ========== 合格判定 ========== - /** - * 根据 GB5749-2022 自动判定水质是否合格 - * 比对所有检测参数与标准值,任何一项超标即为不合格 + * 对记录执行合格判定 */ public void evaluateCompliance(QualityTestRecord record) { - String waterType = record.getWaterType(); - if (waterType == null) waterType = "treated"; + if (record.getWaterType() == null) { + record.setComplianceStatus("pending"); + return; + } - List unqualifiedItems = new ArrayList<>(); + List unqualified = new ArrayList<>(); - checkParam("turbidity", "浊度", record.getTurbidity(), waterType, unqualifiedItems); - checkParam("ph", "pH", record.getPh(), waterType, unqualifiedItems); - checkParam("residual_chlorine", "余氯", record.getResidualChlorine(), waterType, unqualifiedItems); - checkParam("color", "色度", record.getColor(), waterType, unqualifiedItems); - checkParam("odor", "嗅味", record.getOdor(), waterType, unqualifiedItems); - checkParam("ecoli", "大肠杆菌", record.getEcoli(), waterType, unqualifiedItems); - checkParam("colony_count", "菌落总数", record.getColonyCount(), waterType, unqualifiedItems); + checkParam(record.getWaterType(), "turbidity", record.getTurbidity(), "浊度", unqualified); + checkParam(record.getWaterType(), "ph", record.getPh(), "pH", unqualified); + checkParam(record.getWaterType(), "residualChlorine", record.getResidualChlorine(), "余氯", unqualified); + checkParam(record.getWaterType(), "color", record.getColor(), "色度", unqualified); + checkParam(record.getWaterType(), "ecoli", record.getEcoli(), "大肠杆菌", unqualified); + checkParam(record.getWaterType(), "colonyCount", record.getColonyCount(), "菌落总数", unqualified); - if (unqualifiedItems.isEmpty()) { + if (unqualified.isEmpty()) { record.setComplianceStatus("qualified"); record.setUnqualifiedItems(null); } else { record.setComplianceStatus("unqualified"); - // 构建不合格项JSON - record.setUnqualifiedItems(buildUnqualifiedJson(unqualifiedItems)); + record.setUnqualifiedItems("[\"" + String.join("\",\"", unqualified) + "\"]"); } } - private void checkParam(String paramName, String paramLabel, BigDecimal value, - String waterType, List unqualifiedItems) { + private void checkParam(String waterType, String paramName, Double value, String label, + List unqualified) { if (value == null) return; - QualityStandard standard = standardService.getStandard(waterType, paramName); - if (standard == null) return; // 无标准则不判定 - - boolean isUnqualified = false; - if (standard.getMinValue() != null && value.compareTo(standard.getMinValue()) < 0) { - isUnqualified = true; + if (standard == null) return; + if (standard.getMinValue() != null && value < standard.getMinValue()) { + unqualified.add(label + "(偏低)"); } - if (standard.getMaxValue() != null && value.compareTo(standard.getMaxValue()) > 0) { - isUnqualified = true; + if (standard.getMaxValue() != null && value > standard.getMaxValue()) { + unqualified.add(label + "(偏高)"); } - - if (isUnqualified) { - String range = buildRangeDesc(standard); - unqualifiedItems.add(String.format( - "{\"param\":\"%s\",\"label\":\"%s\",\"value\":%s,\"range\":\"%s\",\"unit\":\"%s\"}", - paramName, paramLabel, value.toPlainString(), range, - standard.getParamUnit() != null ? standard.getParamUnit() : "" - )); - } - } - - private String buildRangeDesc(QualityStandard std) { - if (std.getMinValue() != null && std.getMaxValue() != null) { - return std.getMinValue().toPlainString() + "~" + std.getMaxValue().toPlainString(); - } else if (std.getMinValue() != null) { - return "≥" + std.getMinValue().toPlainString(); - } else if (std.getMaxValue() != null) { - return "≤" + std.getMaxValue().toPlainString(); - } - return "无限制"; - } - - private String buildUnqualifiedJson(List items) { - return "[" + String.join(",", items) + "]"; } /** - * 手动重新判定所有记录 + * 重新判定所有记录 */ + @Transactional public int reevaluateAll() { - List records = recordMapper.selectList( - new LambdaQueryWrapper().eq(QualityTestRecord::getDeleted, 0) - ); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(QualityTestRecord::getComplianceStatus, "pending", "qualified", "unqualified"); + List records = recordMapper.selectList(wrapper); int count = 0; for (QualityTestRecord record : records) { + String oldStatus = record.getComplianceStatus(); evaluateCompliance(record); - recordMapper.updateById(record); - count++; + if (!oldStatus.equals(record.getComplianceStatus())) { + recordMapper.updateById(record); + count++; + } } + log.info("重新判定完成,共 {} 条记录状态变更", count); return count; } - // ========== 统计分析 ========== - /** - * 综合统计 + * 获取统计数据 */ public QualityStatVO getStatistics(String startDate, String endDate) { - QualityStatVO stat = new QualityStatVO(); + QualityStatVO vo = new QualityStatVO(); // 按合格状态统计 List> statusStats = recordMapper.statByComplianceStatus(startDate, endDate); long total = 0, qualified = 0, unqualified = 0, pending = 0; for (Map row : statusStats) { - long count = ((Number) row.get("count")).longValue(); - total += count; - String status = (String) row.get("status"); - if ("qualified".equals(status)) qualified = count; - else if ("unqualified".equals(status)) unqualified = count; - else if ("pending".equals(status)) pending = count; + String status = String.valueOf(row.get("status")); + long cnt = ((Number) row.get("count")).longValue(); + total += cnt; + switch (status) { + case "qualified" -> qualified = cnt; + case "unqualified" -> unqualified = cnt; + case "pending" -> pending = cnt; + } } - stat.setTotalCount(total); - stat.setQualifiedCount(qualified); - stat.setUnqualifiedCount(unqualified); - stat.setPendingCount(pending); - stat.setQualifiedRate(total > 0 - ? BigDecimal.valueOf(qualified * 100.0 / total).setScale(1, RoundingMode.HALF_UP) - : BigDecimal.ZERO); + vo.setTotalRecords(total); + vo.setQualifiedCount(qualified); + vo.setUnqualifiedCount(unqualified); + vo.setPendingCount(pending); + vo.setQualifiedRate(total > 0 ? Math.round(qualified * 10000.0 / total) / 100.0 : 0.0); - // 按水样类型合格率 - List> waterTypeStats = recordMapper.statRateByWaterType(startDate, endDate); - Map rateByWaterType = new LinkedHashMap<>(); - for (Map row : waterTypeStats) { - String wt = (String) row.get("waterType"); - long t = ((Number) row.get("total")).longValue(); - long q = ((Number) row.get("qualified")).longValue(); - rateByWaterType.put(wt, t > 0 - ? BigDecimal.valueOf(q * 100.0 / t).setScale(1, RoundingMode.HALF_UP) - : BigDecimal.ZERO); - } - stat.setRateByWaterType(rateByWaterType); + // 按水质类型 + vo.setByWaterType(recordMapper.statRateByWaterType(startDate, endDate)); - // 按区域合格率 - List> areaStats = recordMapper.statRateByArea(startDate, endDate); - Map rateByArea = new LinkedHashMap<>(); - for (Map row : areaStats) { - String area = (String) row.get("area"); - long t = ((Number) row.get("total")).longValue(); - long q = ((Number) row.get("qualified")).longValue(); - rateByArea.put(area, t > 0 - ? BigDecimal.valueOf(q * 100.0 / t).setScale(1, RoundingMode.HALF_UP) - : BigDecimal.ZERO); - } - stat.setRateByArea(rateByArea); + // 按区域 + vo.setByArea(recordMapper.statRateByArea(startDate, endDate)); - // 不合格项统计 - List> unqStats = recordMapper.statUnqualifiedByParam(startDate, endDate); - Map unqByParam = new LinkedHashMap<>(); - for (Map row : unqStats) { - unqByParam.put((String) row.get("paramName"), ((Number) row.get("count")).longValue()); - } - stat.setUnqualifiedByParam(unqByParam); + // 按检测类型 + vo.setByTestType(recordMapper.statByComplianceStatus(startDate, endDate)); // 月度趋势 - List> trend = recordMapper.statMonthlyTrend(startDate, endDate); - stat.setMonthlyTrend(trend); + vo.setTrendByDate(recordMapper.statMonthlyTrend(startDate, endDate)); - // 各指标均值 - List> avgStats = recordMapper.statParamAvg(); - if (!avgStats.isEmpty()) { - stat.setParamAvgStats(avgStats.get(0).entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, - e -> Map.of("avg", e.getValue() != null ? e.getValue() : 0)))); + // 不合格项 + vo.setUnqualifiedItems(recordMapper.statUnqualifiedByParam(startDate, endDate)); + + // 参数均值 + Map avgMap = recordMapper.statParamAvg(); + if (avgMap != null) { + vo.setAvgTurbidity(toDouble(avgMap.get("avgTurbidity"))); + vo.setAvgPh(toDouble(avgMap.get("avgPh"))); + vo.setAvgResidualChlorine(toDouble(avgMap.get("avgResidualChlorine"))); } - return stat; + return vo; } - // ========== 数据导出 ========== + private Double toDouble(Object val) { + if (val == null) return null; + if (val instanceof Number) return ((Number) val).doubleValue(); + return null; + } /** - * 导出 Excel + * 导出Excel */ - public byte[] exportExcel(QualityQueryRequest request) { - // 获取全部数据 (最多10000条) - int offset = 0; - List> records = recordMapper.selectRecordPage( - request.getTestType(), request.getWaterType(), request.getArea(), - request.getSamplingPoint(), request.getTester(), request.getComplianceStatus(), - request.getStartDate(), request.getEndDate(), request.getKeyword(), - "testDate", "desc", offset, 10000 + public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException { + request.setPageNum(1); + request.setPageSize(100000); + Map result = queryRecords(request); + @SuppressWarnings("unchecked") + List> records = (List>) result.get("records"); + + List> excelData = records.stream().map(r -> { + List row = new ArrayList<>(); + row.add(String.valueOf(r.getOrDefault("testType", ""))); + row.add(String.valueOf(r.getOrDefault("waterType", ""))); + row.add(String.valueOf(r.getOrDefault("samplingPoint", ""))); + row.add(String.valueOf(r.getOrDefault("area", ""))); + row.add(String.valueOf(r.getOrDefault("testDate", ""))); + row.add(String.valueOf(r.getOrDefault("tester", ""))); + row.add(String.valueOf(r.getOrDefault("turbidity", ""))); + row.add(String.valueOf(r.getOrDefault("ph", ""))); + row.add(String.valueOf(r.getOrDefault("residualChlorine", ""))); + row.add(String.valueOf(r.getOrDefault("color", ""))); + row.add(String.valueOf(r.getOrDefault("odor", ""))); + row.add(String.valueOf(r.getOrDefault("ecoli", ""))); + row.add(String.valueOf(r.getOrDefault("colonyCount", ""))); + row.add(String.valueOf(r.getOrDefault("complianceStatus", ""))); + row.add(String.valueOf(r.getOrDefault("unqualifiedItems", ""))); + row.add(String.valueOf(r.getOrDefault("remark", ""))); + return row; + }).collect(Collectors.toList()); + + List> head = List.of( + List.of("检测类型"), List.of("水质类型"), List.of("采样点"), List.of("区域"), + List.of("检测日期"), List.of("检测人"), List.of("浊度"), List.of("pH"), + List.of("余氯"), List.of("色度"), List.of("嗅味"), List.of("大肠杆菌"), + List.of("菌落总数"), List.of("合格状态"), List.of("不合格项"), List.of("备注") ); - if (records.isEmpty()) return new byte[0]; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", + "attachment;filename=" + URLEncoder.encode("水质检测报告.xlsx", StandardCharsets.UTF_8)); - List> head = buildExportHead(); - List> data = buildExportData(records); - - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - EasyExcel.write(bos) - .sheet("水质检测台账") - .head(head) - .doWrite(data); - return bos.toByteArray(); - } catch (IOException e) { - log.error("Excel 导出失败", e); - throw new RuntimeException("Excel 导出失败: " + e.getMessage()); - } + EasyExcel.write(response.getOutputStream()) + .head(head) + .sheet("检测记录") + .doWrite(excelData.stream().map(row -> (List) (List) row).collect(Collectors.toList())); } - private List> buildExportHead() { - List> head = new ArrayList<>(); - head.add(List.of("检测日期")); - head.add(List.of("检测类型")); - head.add(List.of("水样类型")); - head.add(List.of("采样点")); - head.add(List.of("区域")); - head.add(List.of("检测人")); - head.add(List.of("浊度(NTU)")); - head.add(List.of("pH")); - head.add(List.of("余氯(mg/L)")); - head.add(List.of("色度(度)")); - head.add(List.of("嗅味(级)")); - head.add(List.of("大肠杆菌(CFU/100mL)")); - head.add(List.of("菌落总数(CFU/mL)")); - head.add(List.of("合格状态")); - head.add(List.of("备注")); - return head; - } - - private List> buildExportData(List> records) { - List> data = new ArrayList<>(); - for (Map r : records) { - List row = new ArrayList<>(); - row.add(r.get("testDate")); - row.add(formatTestType((String) r.get("testType"))); - row.add(formatWaterType((String) r.get("waterType"))); - row.add(r.get("samplingPoint")); - row.add(r.get("area")); - row.add(r.get("tester")); - row.add(r.get("turbidity")); - row.add(r.get("ph")); - row.add(r.get("residualChlorine")); - row.add(r.get("color")); - row.add(r.get("odor")); - row.add(r.get("ecoli")); - row.add(r.get("colonyCount")); - row.add(formatCompliance((String) r.get("complianceStatus"))); - row.add(r.get("remark")); - data.add(row); - } - return data; - } - - private String formatTestType(String type) { - if (type == null) return ""; - return switch (type) { - case "routine" -> "常规检测"; - case "special" -> "专项检测"; - case "complaint" -> "投诉检测"; - default -> type; - }; - } - - private String formatWaterType(String type) { - if (type == null) return ""; - return switch (type) { - case "raw" -> "原水"; - case "treated" -> "出厂水"; - case "network" -> "管网末梢水"; - default -> type; - }; - } - - private String formatCompliance(String status) { - if (status == null) return "待判定"; - return switch (status) { - case "qualified" -> "合格"; - case "unqualified" -> "不合格"; - case "pending" -> "待判定"; - default -> status; - }; - } - - // ========== 辅助查询 ========== - /** - * 获取所有区域列表 + * 获取区域列表 */ public List getAreaList() { - List records = recordMapper.selectList( - new LambdaQueryWrapper() - .select(QualityTestRecord::getArea) - .isNotNull(QualityTestRecord::getArea) - .groupBy(QualityTestRecord::getArea) - ); - return records.stream() + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(QualityTestRecord::getArea) + .isNotNull(QualityTestRecord::getArea) + .ne(QualityTestRecord::getArea, "") + .groupBy(QualityTestRecord::getArea); + return recordMapper.selectList(wrapper).stream() .map(QualityTestRecord::getArea) - .filter(Objects::nonNull) .distinct() .sorted() .collect(Collectors.toList()); } /** - * 获取所有采样点列表 + * 获取采样点列表 */ public List getSamplingPointList() { - List records = recordMapper.selectList( - new LambdaQueryWrapper() - .select(QualityTestRecord::getSamplingPoint) - .isNotNull(QualityTestRecord::getSamplingPoint) - .groupBy(QualityTestRecord::getSamplingPoint) - ); - return records.stream() + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(QualityTestRecord::getSamplingPoint) + .isNotNull(QualityTestRecord::getSamplingPoint) + .ne(QualityTestRecord::getSamplingPoint, "") + .groupBy(QualityTestRecord::getSamplingPoint); + return recordMapper.selectList(wrapper).stream() .map(QualityTestRecord::getSamplingPoint) - .filter(Objects::nonNull) .distinct() .sorted() .collect(Collectors.toList()); } - - /** - * 按ID批量查询记录 - */ - public List listByIds(List ids) { - return recordMapper.selectBatchIds(ids); - } } diff --git a/wm-production/src/main/java/com/water/production/service/QualityStandardService.java b/wm-production/src/main/java/com/water/production/service/QualityStandardService.java index d0521dec..bd834300 100644 --- a/wm-production/src/main/java/com/water/production/service/QualityStandardService.java +++ b/wm-production/src/main/java/com/water/production/service/QualityStandardService.java @@ -1,20 +1,16 @@ package com.water.production.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.water.common.core.exception.BusinessException; import com.water.production.entity.QualityStandard; import com.water.production.mapper.QualityStandardMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; -import java.util.*; -import java.util.stream.Collectors; +import java.util.List; -/** - * 水质标准管理服务 - * 基于 GB5749-2022《生活饮用水卫生标准》 - */ @Slf4j @Service @RequiredArgsConstructor @@ -26,79 +22,84 @@ public class QualityStandardService { * 获取所有启用的标准 */ public List listEnabled() { - return standardMapper.selectList( - new LambdaQueryWrapper() - .eq(QualityStandard::getEnabled, 1) - .orderByAsc(QualityStandard::getParamName) - ); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getEnabled, true) + .orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName); + return standardMapper.selectList(wrapper); } /** - * 按水样类型获取启用的标准 + * 按水质类型获取标准 */ public List listByWaterType(String waterType) { - return standardMapper.selectList( - new LambdaQueryWrapper() - .eq(QualityStandard::getEnabled, 1) - .and(w -> w.eq(QualityStandard::getWaterType, waterType) - .or().eq(QualityStandard::getWaterType, "all")) - .orderByAsc(QualityStandard::getParamName) - ); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getEnabled, true) + .eq(QualityStandard::getWaterType, waterType) + .orderByAsc(QualityStandard::getParamName); + return standardMapper.selectList(wrapper); + } + + /** + * 获取全部标准(含禁用) + */ + public List listAll() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName); + return standardMapper.selectList(wrapper); } /** * 获取标准详情 */ public QualityStandard getById(Long id) { - return standardMapper.selectById(id); + QualityStandard standard = standardMapper.selectById(id); + if (standard == null) { + throw new BusinessException("标准不存在"); + } + return standard; } /** - * 新增标准 + * 获取指定水质类型和参数名的标准 */ + public QualityStandard getStandard(String waterType, String paramName) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getWaterType, waterType) + .eq(QualityStandard::getParamName, paramName) + .eq(QualityStandard::getEnabled, true) + .last("LIMIT 1"); + return standardMapper.selectOne(wrapper); + } + + /** + * 创建标准 + */ + @Transactional public QualityStandard create(QualityStandard standard) { - standard.setEnabled(1); - standard.setDeleted(0); standardMapper.insert(standard); + log.info("创建水质标准: {} - {}", standard.getStandardName(), standard.getParamLabel()); return standard; } /** * 更新标准 */ - public void update(QualityStandard standard) { + @Transactional + public QualityStandard update(Long id, QualityStandard standard) { + QualityStandard existing = getById(id); + standard.setId(existing.getId()); standardMapper.updateById(standard); + log.info("更新水质标准: {}", id); + return standardMapper.selectById(id); } /** - * 删除标准 (逻辑删除) + * 删除标准 */ + @Transactional public void delete(Long id) { + getById(id); standardMapper.deleteById(id); - } - - /** - * 获取所有标准 (含停用) - */ - public List listAll() { - return standardMapper.selectList( - new LambdaQueryWrapper() - .orderByAsc(QualityStandard::getStandardCode) - .orderByAsc(QualityStandard::getParamName) - ); - } - - /** - * 根据水样类型和参数名获取标准 - */ - public QualityStandard getStandard(String waterType, String paramName) { - return standardMapper.selectOne( - new LambdaQueryWrapper() - .eq(QualityStandard::getEnabled, 1) - .eq(QualityStandard::getParamName, paramName) - .and(w -> w.eq(QualityStandard::getWaterType, waterType) - .or().eq(QualityStandard::getWaterType, "all")) - .last("LIMIT 1") - ); + log.info("删除水质标准: {}", id); } } diff --git a/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java b/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java index 462e85a7..4bbf7cdc 100644 --- a/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java +++ b/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java @@ -2,19 +2,17 @@ package com.water.production.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.water.common.core.exception.BusinessException; import com.water.production.entity.QualityTestPlan; import com.water.production.mapper.QualityTestPlanMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; -import java.util.Map; -/** - * 水质检测计划管理服务 - */ @Slf4j @Service @RequiredArgsConstructor @@ -23,125 +21,124 @@ public class QualityTestPlanService { private final QualityTestPlanMapper planMapper; /** - * 分页查询检测计划 + * 分页查询计划 */ - public Map queryPlans(String status, String frequency, String waterType, - String keyword, int pageNum, int pageSize) { + public Page queryPlans(int pageNum, int pageSize, String testType, String waterType, + String area, String status, String keyword) { + Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (status != null && !status.isEmpty()) { - wrapper.eq(QualityTestPlan::getStatus, status); - } - if (frequency != null && !frequency.isEmpty()) { - wrapper.eq(QualityTestPlan::getFrequency, frequency); - } - if (waterType != null && !waterType.isEmpty()) { - wrapper.eq(QualityTestPlan::getWaterType, waterType); - } - if (keyword != null && !keyword.isEmpty()) { + if (testType != null && !testType.isBlank()) wrapper.eq(QualityTestPlan::getTestType, testType); + if (waterType != null && !waterType.isBlank()) wrapper.eq(QualityTestPlan::getWaterType, waterType); + if (area != null && !area.isBlank()) wrapper.eq(QualityTestPlan::getArea, area); + if (status != null && !status.isBlank()) wrapper.eq(QualityTestPlan::getStatus, status); + if (keyword != null && !keyword.isBlank()) { wrapper.and(w -> w.like(QualityTestPlan::getPlanName, keyword) - .or().like(QualityTestPlan::getSamplingPoint, keyword) - .or().like(QualityTestPlan::getArea, keyword)); + .or().like(QualityTestPlan::getSamplingPoint, keyword)); } wrapper.orderByDesc(QualityTestPlan::getCreatedAt); - - Page page = planMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); - - return Map.of( - "records", page.getRecords(), - "total", page.getTotal(), - "pageNum", pageNum, - "pageSize", pageSize, - "pages", page.getPages() - ); + return planMapper.selectPage(page, wrapper); } /** * 获取计划详情 */ public QualityTestPlan getById(Long id) { - return planMapper.selectById(id); - } - - /** - * 创建检测计划 - */ - public QualityTestPlan create(QualityTestPlan plan) { - plan.setDeleted(0); - if (plan.getStatus() == null) { - plan.setStatus("active"); + QualityTestPlan plan = planMapper.selectById(id); + if (plan == null) { + throw new BusinessException("检测计划不存在"); } - if (plan.getExecutionCount() == null) { - plan.setExecutionCount(0); - } - // 计算下次检测日期 - if (plan.getNextTestDate() == null) { - plan.setNextTestDate(plan.getStartDate() != null ? plan.getStartDate() : LocalDate.now()); - } - planMapper.insert(plan); return plan; } /** - * 更新检测计划 + * 创建计划 */ - public void update(QualityTestPlan plan) { - planMapper.updateById(plan); + @Transactional + public QualityTestPlan create(QualityTestPlan plan) { + if (plan.getStatus() == null) plan.setStatus("active"); + if (plan.getExecutionCount() == null) plan.setExecutionCount(0); + if (plan.getNextTestDate() == null) plan.setNextTestDate(plan.getStartDate()); + planMapper.insert(plan); + log.info("创建检测计划: {}", plan.getPlanName()); + return plan; } /** - * 删除检测计划 (逻辑删除) + * 更新计划 */ + @Transactional + public QualityTestPlan update(Long id, QualityTestPlan plan) { + QualityTestPlan existing = getById(id); + plan.setId(existing.getId()); + planMapper.updateById(plan); + log.info("更新检测计划: {}", id); + return planMapper.selectById(id); + } + + /** + * 删除计划 + */ + @Transactional public void delete(Long id) { + getById(id); planMapper.deleteById(id); + log.info("删除检测计划: {}", id); } /** - * 暂停/恢复计划 + * 切换计划状态 */ - public void toggleStatus(Long id, String status) { - QualityTestPlan plan = planMapper.selectById(id); - if (plan == null) { - throw new IllegalArgumentException("计划不存在: " + id); + @Transactional + public QualityTestPlan toggleStatus(Long id) { + QualityTestPlan plan = getById(id); + if ("active".equals(plan.getStatus())) { + plan.setStatus("paused"); + } else if ("paused".equals(plan.getStatus())) { + plan.setStatus("active"); } - plan.setStatus(status); planMapper.updateById(plan); + log.info("切换检测计划状态: {} -> {}", id, plan.getStatus()); + return plan; } /** - * 获取所有到期需执行的计划 + * 获取到期计划 */ public List getDuePlans() { - return planMapper.selectList( - new LambdaQueryWrapper() - .eq(QualityTestPlan::getStatus, "active") - .le(QualityTestPlan::getNextTestDate, LocalDate.now()) - .and(w -> w.isNull(QualityTestPlan::getEndDate) - .or().ge(QualityTestPlan::getEndDate, LocalDate.now())) - .orderByAsc(QualityTestPlan::getNextTestDate) - ); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityTestPlan::getStatus, "active") + .le(QualityTestPlan::getNextTestDate, LocalDate.now()) + .orderByAsc(QualityTestPlan::getNextTestDate); + return planMapper.selectList(wrapper); } /** - * 执行计划后更新下次检测日期 + * 标记已执行,计算下次检测日期 */ - public void markExecuted(Long id) { - QualityTestPlan plan = planMapper.selectById(id); - if (plan == null) return; - + @Transactional + public QualityTestPlan markExecuted(Long id) { + QualityTestPlan plan = getById(id); plan.setExecutionCount(plan.getExecutionCount() + 1); - LocalDate current = plan.getNextTestDate() != null ? plan.getNextTestDate() : LocalDate.now(); - switch (plan.getFrequency()) { - case "daily" -> plan.setNextTestDate(current.plusDays(1)); - case "weekly" -> plan.setNextTestDate(current.plusWeeks(1)); - case "monthly" -> plan.setNextTestDate(current.plusMonths(1)); - } + // 计算下次检测日期 + LocalDate nextDate = plan.getNextTestDate(); + if (nextDate == null) nextDate = LocalDate.now(); - // 检查是否过期 - if (plan.getEndDate() != null && plan.getNextTestDate().isAfter(plan.getEndDate())) { - plan.setStatus("completed"); + switch (plan.getFrequency() != null ? plan.getFrequency() : "daily") { + case "daily" -> nextDate = nextDate.plusDays(1); + case "weekly" -> nextDate = nextDate.plusWeeks(1); + case "monthly" -> nextDate = nextDate.plusMonths(1); + default -> nextDate = nextDate.plusDays(1); + } + plan.setNextTestDate(nextDate); + + // 如果下次检测超过结束日期,自动过期 + if (plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate())) { + plan.setStatus("expired"); } planMapper.updateById(plan); + log.info("标记检测计划已执行: {}, 下次检测: {}", id, nextDate); + return plan; } } diff --git a/wm-production/src/main/resources/db/V4__quality_ledger.sql b/wm-production/src/main/resources/db/V4__quality_ledger.sql index 8bc1b9c8..c75a7df6 100644 --- a/wm-production/src/main/resources/db/V4__quality_ledger.sql +++ b/wm-production/src/main/resources/db/V4__quality_ledger.sql @@ -1,109 +1,134 @@ -- ============================================================ --- V4__quality_ledger.sql --- 水质检测台账模块 DDL --- 包含: 检测记录、水质标准、检测计划 +-- V4: 水质检测台账 (GB5749-2022) -- ============================================================ -- 1. 水质检测记录表 CREATE TABLE IF NOT EXISTS prod_quality_test_record ( - id BIGSERIAL PRIMARY KEY, - test_type VARCHAR(20) NOT NULL DEFAULT 'routine', -- routine/special/complaint - water_type VARCHAR(20) NOT NULL DEFAULT 'treated', -- raw/treated/network - sampling_point VARCHAR(100), -- 采样点 - area VARCHAR(50), -- 所属区域 - test_date DATE NOT NULL, -- 检测日期 - test_time TIME, -- 检测时间 - tester VARCHAR(50), -- 检测人 - turbidity NUMERIC(10,2), -- 浊度 (NTU) - ph NUMERIC(5,2), -- pH值 - residual_chlorine NUMERIC(6,3), -- 余氯 (mg/L) - color NUMERIC(8,2), -- 色度 (度) - odor NUMERIC(4,1), -- 嗅味 (级) - ecoli NUMERIC(10,2), -- 大肠杆菌 (CFU/100mL) - colony_count NUMERIC(10,2), -- 菌落总数 (CFU/mL) - compliance_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- qualified/unqualified/pending - unqualified_items TEXT, -- 不合格项 (JSON) - remark VARCHAR(500), -- 备注 - deleted INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + id BIGSERIAL PRIMARY KEY, + test_type VARCHAR(50) NOT NULL DEFAULT 'routine', + water_type VARCHAR(50) NOT NULL, + sampling_point VARCHAR(200), + area VARCHAR(200), + test_date DATE NOT NULL DEFAULT CURRENT_DATE, + test_time TIME, + tester VARCHAR(100), + turbidity NUMERIC(10,4), + ph NUMERIC(6,3), + residual_chlorine NUMERIC(10,4), + color NUMERIC(10,2), + odor VARCHAR(100), + ecoli NUMERIC(12,2), + colony_count NUMERIC(12,2), + compliance_status VARCHAR(20) DEFAULT 'pending', + unqualified_items TEXT, + remark TEXT, + deleted INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_quality_record_type ON prod_quality_test_record(test_type); +CREATE INDEX IF NOT EXISTS idx_quality_record_test_type ON prod_quality_test_record(test_type); CREATE INDEX IF NOT EXISTS idx_quality_record_water_type ON prod_quality_test_record(water_type); +CREATE INDEX IF NOT EXISTS idx_quality_record_test_date ON prod_quality_test_record(test_date); CREATE INDEX IF NOT EXISTS idx_quality_record_area ON prod_quality_test_record(area); -CREATE INDEX IF NOT EXISTS idx_quality_record_date ON prod_quality_test_record(test_date); CREATE INDEX IF NOT EXISTS idx_quality_record_compliance ON prod_quality_test_record(compliance_status); CREATE INDEX IF NOT EXISTS idx_quality_record_deleted ON prod_quality_test_record(deleted); COMMENT ON TABLE prod_quality_test_record IS '水质检测记录表'; -COMMENT ON COLUMN prod_quality_test_record.test_type IS '检测类型: routine-常规/special-专项/complaint-投诉'; -COMMENT ON COLUMN prod_quality_test_record.water_type IS '水样类型: raw-原水/treated-出厂水/network-管网末梢水'; -COMMENT ON COLUMN prod_quality_test_record.compliance_status IS '合格状态: qualified-合格/unqualified-不合格/pending-待判定'; +COMMENT ON COLUMN prod_quality_test_record.test_type IS '检测类型: routine/emergency/special'; +COMMENT ON COLUMN prod_quality_test_record.water_type IS '水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater'; +COMMENT ON COLUMN prod_quality_test_record.compliance_status IS '合格状态: qualified/unqualified/pending'; --- 2. 水质标准表 (GB5749-2022) +-- 2. 水质标准表 CREATE TABLE IF NOT EXISTS prod_quality_standard ( id BIGSERIAL PRIMARY KEY, - standard_name VARCHAR(100) NOT NULL, - standard_code VARCHAR(50) NOT NULL DEFAULT 'GB5749-2022', - param_name VARCHAR(50) NOT NULL, -- 参数编码 - param_label VARCHAR(50), -- 参数显示名 - param_unit VARCHAR(20), -- 单位 - min_value NUMERIC(12,4), -- 最小值 (NULL=无下限) - max_value NUMERIC(12,4), -- 最大值 (NULL=无上限) - water_type VARCHAR(20) NOT NULL DEFAULT 'all', -- 适用水样类型 - enabled INTEGER NOT NULL DEFAULT 1, + standard_name VARCHAR(200) NOT NULL, + standard_code VARCHAR(100), + param_name VARCHAR(100) NOT NULL, + param_label VARCHAR(100) NOT NULL, + param_unit VARCHAR(50), + min_value NUMERIC(12,4), + max_value NUMERIC(12,4), + water_type VARCHAR(50) NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, deleted INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_quality_standard_code ON prod_quality_standard(standard_code); +CREATE INDEX IF NOT EXISTS idx_quality_standard_water_type ON prod_quality_standard(water_type); CREATE INDEX IF NOT EXISTS idx_quality_standard_param ON prod_quality_standard(param_name); +CREATE INDEX IF NOT EXISTS idx_quality_standard_enabled ON prod_quality_standard(enabled); CREATE INDEX IF NOT EXISTS idx_quality_standard_deleted ON prod_quality_standard(deleted); -COMMENT ON TABLE prod_quality_standard IS '水质标准表 (基于GB5749-2022)'; - --- 初始化 GB5749-2022 默认标准 -INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type) -VALUES - ('生活饮用水卫生标准', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'treated'), - ('生活饮用水卫生标准', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 3.0, 'network'), - ('生活饮用水卫生标准', 'GB5749-2022', 'ph', 'pH', '', 6.5, 8.5, 'all'), - ('生活饮用水卫生标准', 'GB5749-2022', 'residual_chlorine', '余氯', 'mg/L', 0.3, 2.0, 'treated'), - ('生活饮用水卫生标准', 'GB5749-2022', 'residual_chlorine', '余氯', 'mg/L', 0.05, 2.0, 'network'), - ('生活饮用水卫生标准', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'all'), - ('生活饮用水卫生标准', 'GB5749-2022', 'odor', '嗅味', '级', NULL, 2.0, 'all'), - ('生活饮用水卫生标准', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'all'), - ('生活饮用水卫生标准', 'GB5749-2022', 'colony_count', '菌落总数', 'CFU/mL', NULL, 100.0, 'all') -ON CONFLICT DO NOTHING; +COMMENT ON TABLE prod_quality_standard IS '水质标准表'; +COMMENT ON COLUMN prod_quality_standard.water_type IS '水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater'; -- 3. 水质检测计划表 CREATE TABLE IF NOT EXISTS prod_quality_test_plan ( id BIGSERIAL PRIMARY KEY, - plan_name VARCHAR(100) NOT NULL, - test_type VARCHAR(20) NOT NULL DEFAULT 'routine', -- routine/special - water_type VARCHAR(20) NOT NULL DEFAULT 'treated', - sampling_point VARCHAR(100), - area VARCHAR(50), - frequency VARCHAR(20) NOT NULL DEFAULT 'daily', -- daily/weekly/monthly - test_params VARCHAR(200), -- 检测参数 (逗号分隔) + plan_name VARCHAR(200) NOT NULL, + test_type VARCHAR(50) NOT NULL DEFAULT 'routine', + water_type VARCHAR(50) NOT NULL, + sampling_point VARCHAR(200), + area VARCHAR(200), + frequency VARCHAR(20) NOT NULL DEFAULT 'daily', + test_params TEXT, start_date DATE NOT NULL, - end_date DATE, -- NULL=长期 + end_date DATE, next_test_date DATE, - status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/paused/completed + status VARCHAR(20) NOT NULL DEFAULT 'active', execution_count INTEGER NOT NULL DEFAULT 0, - remark VARCHAR(500), + remark TEXT, deleted INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); +CREATE INDEX IF NOT EXISTS idx_quality_plan_water_type ON prod_quality_test_plan(water_type); CREATE INDEX IF NOT EXISTS idx_quality_plan_status ON prod_quality_test_plan(status); -CREATE INDEX IF NOT EXISTS idx_quality_plan_frequency ON prod_quality_test_plan(frequency); CREATE INDEX IF NOT EXISTS idx_quality_plan_next_date ON prod_quality_test_plan(next_test_date); CREATE INDEX IF NOT EXISTS idx_quality_plan_deleted ON prod_quality_test_plan(deleted); COMMENT ON TABLE prod_quality_test_plan IS '水质检测计划表'; -COMMENT ON COLUMN prod_quality_test_plan.frequency IS '检测频率: daily-日检/weekly-周检/monthly-月检'; -COMMENT ON COLUMN prod_quality_test_plan.status IS '计划状态: active-启用/paused-暂停/completed-已完成'; +COMMENT ON COLUMN prod_quality_test_plan.frequency IS '检测频率: daily/weekly/monthly'; +COMMENT ON COLUMN prod_quality_test_plan.status IS '状态: active/paused/expired'; + +-- ============================================================ +-- GB5749-2022 默认标准数据 +-- ============================================================ + +-- 原水 (rawWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 4.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 10000.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 500.0, 'rawWater', TRUE); + +-- 出厂水 (factoryWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.3, 4.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'factoryWater', TRUE); + +-- 管网水 (pipeNetworkWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'pipeNetworkWater', TRUE); + +-- 末梢水 (endUserWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'endUserWater', TRUE); diff --git a/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml b/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml index 323be17a..14304c73 100644 --- a/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml +++ b/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml @@ -2,69 +2,55 @@ - - - WHERE r.deleted = 0 - AND r.test_type = #{testType} - AND r.water_type = #{waterType} - AND r.area = #{area} - - AND r.sampling_point LIKE '%' || #{samplingPoint} || '%' - - - AND r.tester LIKE '%' || #{tester} || '%' - - - AND r.compliance_status = #{complianceStatus} - - AND r.test_date >= #{startDate}::date - AND r.test_date <= #{endDate}::date - - AND (r.sampling_point LIKE '%' || #{keyword} || '%' - OR r.tester LIKE '%' || #{keyword} || '%' - OR r.area LIKE '%' || #{keyword} || '%' - OR r.remark LIKE '%' || #{keyword} || '%') - - - - - - - - - - - diff --git a/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java b/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java index 6ddb4d75..ec8894ca 100644 --- a/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java +++ b/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java @@ -1,586 +1,399 @@ package com.water.production.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.water.production.dto.QualityQueryRequest; import com.water.production.dto.QualityStatVO; import com.water.production.entity.QualityStandard; import com.water.production.entity.QualityTestPlan; import com.water.production.entity.QualityTestRecord; +import com.water.production.mapper.QualityStandardMapper; +import com.water.production.mapper.QualityTestPlanMapper; +import com.water.production.mapper.QualityTestRecordMapper; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalTime; import java.util.*; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; -/** - * 水质检测台账单元测试 - * 覆盖实体构建、合格判定、统计计算、计划调度、查询筛选、CSV转义等 - */ +@ExtendWith(MockitoExtension.class) class QualityLedgerServiceTest { - // ========== 1. 实体完整性测试 ========== + @Mock + private QualityTestRecordMapper recordMapper; + + @Mock + private QualityStandardService standardService; + + @Mock + private QualityTestPlanMapper planMapper; + + @Mock + private QualityStandardMapper standardMapper; + + @InjectMocks + private QualityLedgerService ledgerService; + + private QualityTestPlanService planService; + + private QualityTestRecord sampleRecord; + + @BeforeEach + void setUp() { + planService = new QualityTestPlanService(planMapper); + + sampleRecord = new QualityTestRecord(); + sampleRecord.setId(1L); + sampleRecord.setTestType("routine"); + sampleRecord.setWaterType("factoryWater"); + sampleRecord.setSamplingPoint("出厂水采样点A"); + sampleRecord.setArea("城区A"); + sampleRecord.setTestDate(LocalDate.now()); + sampleRecord.setTestTime(LocalTime.of(10, 0)); + sampleRecord.setTester("张三"); + sampleRecord.setTurbidity(0.5); + sampleRecord.setPh(7.2); + sampleRecord.setResidualChlorine(0.8); + sampleRecord.setColor(5.0); + sampleRecord.setOdor("无"); + sampleRecord.setEcoli(0.0); + sampleRecord.setColonyCount(20.0); + } + + // ==================== 记录 CRUD 测试 ==================== @Test - @DisplayName("QualityTestRecord 实体字段完整性") - void testQualityTestRecordEntity() { - QualityTestRecord record = new QualityTestRecord(); - record.setId(1L); - record.setTestType("routine"); - record.setWaterType("treated"); - record.setSamplingPoint("出厂水口"); - record.setArea("一体化水厂"); - record.setTestDate(LocalDate.of(2026, 6, 14)); - record.setTestTime(LocalTime.of(9, 30)); - record.setTester("张三"); - record.setTurbidity(new BigDecimal("0.5")); - record.setPh(new BigDecimal("7.2")); - record.setResidualChlorine(new BigDecimal("0.5")); - record.setColor(new BigDecimal("5")); - record.setOdor(new BigDecimal("0")); - record.setEcoli(BigDecimal.ZERO); - record.setColonyCount(new BigDecimal("12")); - record.setComplianceStatus("qualified"); - record.setRemark("正常"); + @DisplayName("1. 创建检测记录-自动判定合格") + void testCreateRecord_Qualified() { + when(standardService.getStandard("factoryWater", "turbidity")) + .thenReturn(buildStandard("turbidity", null, 1.0)); + when(standardService.getStandard("factoryWater", "ph")) + .thenReturn(buildStandard("ph", 6.5, 8.5)); + when(standardService.getStandard("factoryWater", "residualChlorine")) + .thenReturn(buildStandard("residualChlorine", 0.3, 4.0)); + when(standardService.getStandard("factoryWater", "color")) + .thenReturn(buildStandard("color", null, 15.0)); + when(standardService.getStandard("factoryWater", "ecoli")) + .thenReturn(buildStandard("ecoli", null, 0.0)); + when(standardService.getStandard("factoryWater", "colonyCount")) + .thenReturn(buildStandard("colonyCount", null, 100.0)); + when(recordMapper.insert(any())).thenReturn(1); - assertEquals(1L, record.getId()); - assertEquals("routine", record.getTestType()); - assertEquals("treated", record.getWaterType()); - assertEquals("出厂水口", record.getSamplingPoint()); - assertEquals(new BigDecimal("0.5"), record.getTurbidity()); - assertEquals(new BigDecimal("7.2"), record.getPh()); - assertEquals(new BigDecimal("0.5"), record.getResidualChlorine()); - assertEquals("qualified", record.getComplianceStatus()); - assertNotNull(record.getTestDate()); - assertNotNull(record.getTestTime()); + QualityTestRecord result = ledgerService.create(sampleRecord); + + assertEquals("qualified", result.getComplianceStatus()); + assertNull(result.getUnqualifiedItems()); + verify(recordMapper).insert(sampleRecord); } @Test - @DisplayName("QualityStandard 实体字段完整性") - void testQualityStandardEntity() { - QualityStandard standard = new QualityStandard(); - standard.setId(1L); - standard.setStandardName("生活饮用水卫生标准"); - standard.setStandardCode("GB5749-2022"); - standard.setParamName("turbidity"); - standard.setParamLabel("浊度"); - standard.setParamUnit("NTU"); - standard.setMinValue(null); - standard.setMaxValue(new BigDecimal("1.0")); - standard.setWaterType("treated"); - standard.setEnabled(1); + @DisplayName("2. 创建检测记录-自动判定不合格") + void testCreateRecord_Unqualified() { + sampleRecord.setTurbidity(2.5); // 超标 + sampleRecord.setEcoli(10.0); // 超标 - assertEquals("GB5749-2022", standard.getStandardCode()); - assertEquals("turbidity", standard.getParamName()); - assertNull(standard.getMinValue()); - assertEquals(new BigDecimal("1.0"), standard.getMaxValue()); - assertEquals("treated", standard.getWaterType()); - assertEquals(1, standard.getEnabled()); + when(standardService.getStandard("factoryWater", "turbidity")) + .thenReturn(buildStandard("turbidity", null, 1.0)); + when(standardService.getStandard("factoryWater", "ph")) + .thenReturn(buildStandard("ph", 6.5, 8.5)); + when(standardService.getStandard("factoryWater", "residualChlorine")) + .thenReturn(buildStandard("residualChlorine", 0.3, 4.0)); + when(standardService.getStandard("factoryWater", "color")) + .thenReturn(buildStandard("color", null, 15.0)); + when(standardService.getStandard("factoryWater", "ecoli")) + .thenReturn(buildStandard("ecoli", null, 0.0)); + when(standardService.getStandard("factoryWater", "colonyCount")) + .thenReturn(buildStandard("colonyCount", null, 100.0)); + when(recordMapper.insert(any())).thenReturn(1); + + QualityTestRecord result = ledgerService.create(sampleRecord); + + assertEquals("unqualified", result.getComplianceStatus()); + assertNotNull(result.getUnqualifiedItems()); + assertTrue(result.getUnqualifiedItems().contains("浊度")); + assertTrue(result.getUnqualifiedItems().contains("大肠杆菌")); } @Test - @DisplayName("QualityTestPlan 实体字段完整性") - void testQualityTestPlanEntity() { - QualityTestPlan plan = new QualityTestPlan(); - plan.setId(1L); - plan.setPlanName("出厂水日检计划"); - plan.setTestType("routine"); - plan.setWaterType("treated"); - plan.setSamplingPoint("出厂水口"); - plan.setArea("一体化水厂"); - plan.setFrequency("daily"); - plan.setTestParams("turbidity,ph,residual_chlorine"); - plan.setStartDate(LocalDate.of(2026, 6, 1)); - plan.setEndDate(null); - plan.setNextTestDate(LocalDate.of(2026, 6, 14)); - plan.setStatus("active"); - plan.setExecutionCount(13); + @DisplayName("3. 创建检测记录-waterType为空时pending") + void testCreateRecord_PendingWhenNoWaterType() { + sampleRecord.setWaterType(null); + when(recordMapper.insert(any())).thenReturn(1); - assertEquals("daily", plan.getFrequency()); - assertEquals("active", plan.getStatus()); - assertEquals(13, plan.getExecutionCount()); - assertNull(plan.getEndDate()); - assertNotNull(plan.getNextTestDate()); - } + QualityTestRecord result = ledgerService.create(sampleRecord); - // ========== 2. 合格判定逻辑测试 ========== - - @Test - @DisplayName("GB5749-2022 合格判定逻辑 - 全部合格") - void testComplianceAllQualified() { - // 模拟 GB5749-2022 标准 - List standards = buildDefaultStandards(); - - // 构建合格记录 - QualityTestRecord record = new QualityTestRecord(); - record.setWaterType("treated"); - record.setTurbidity(new BigDecimal("0.5")); // ≤1.0 ✓ - record.setPh(new BigDecimal("7.2")); // 6.5~8.5 ✓ - record.setResidualChlorine(new BigDecimal("0.5")); // 0.3~2.0 ✓ - record.setColor(new BigDecimal("5")); // ≤15 ✓ - record.setOdor(new BigDecimal("0")); // ≤2 ✓ - record.setEcoli(BigDecimal.ZERO); // =0 ✓ - record.setColonyCount(new BigDecimal("12")); // ≤100 ✓ - - List unqualified = evaluateCompliance(record, standards); - assertTrue(unqualified.isEmpty(), "所有指标在标准范围内应合格"); + assertEquals("pending", result.getComplianceStatus()); } @Test - @DisplayName("GB5749-2022 合格判定逻辑 - 多项超标") - void testComplianceMultipleUnqualified() { - List standards = buildDefaultStandards(); + @DisplayName("4. 查询检测记录分页") + void testQueryRecords() { + QualityQueryRequest request = new QualityQueryRequest(); + request.setPageNum(1); + request.setPageSize(10); - QualityTestRecord record = new QualityTestRecord(); - record.setWaterType("treated"); - record.setTurbidity(new BigDecimal("2.5")); // >1.0 ✗ - record.setPh(new BigDecimal("9.0")); // >8.5 ✗ - record.setResidualChlorine(new BigDecimal("0.1")); // <0.3 ✗ - record.setColor(new BigDecimal("5")); // ≤15 ✓ - record.setOdor(new BigDecimal("0")); // ≤2 ✓ - record.setEcoli(BigDecimal.ZERO); // =0 ✓ - record.setColonyCount(new BigDecimal("12")); // ≤100 ✓ + List> mockRecords = List.of( + Map.of("id", 1L, "testType", "routine"), + Map.of("id", 2L, "testType", "emergency") + ); + when(recordMapper.selectRecordPage(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq(0), eq(10))) + .thenReturn(mockRecords); + when(recordMapper.countRecords(any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(2L); - List unqualified = evaluateCompliance(record, standards); - assertEquals(3, unqualified.size(), "应有3项不合格: 浊度、pH、余氯"); - assertTrue(unqualified.stream().anyMatch(s -> s.contains("turbidity"))); - assertTrue(unqualified.stream().anyMatch(s -> s.contains("ph"))); - assertTrue(unqualified.stream().anyMatch(s -> s.contains("residual_chlorine"))); + Map result = ledgerService.queryRecords(request); + + assertNotNull(result); + assertEquals(2L, result.get("total")); + assertEquals(1, result.get("pageNum")); + assertEquals(1L, result.get("totalPages")); } @Test - @DisplayName("GB5749-2022 合格判定 - 管网末梢水余氯标准不同") - void testComplianceNetworkWaterType() { - List standards = buildDefaultStandards(); + @DisplayName("5. 获取记录详情") + void testGetById() { + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); - QualityTestRecord record = new QualityTestRecord(); - record.setWaterType("network"); - record.setTurbidity(new BigDecimal("2.0")); // ≤3.0 (管网标准) ✓ - record.setPh(new BigDecimal("7.0")); // 6.5~8.5 ✓ - record.setResidualChlorine(new BigDecimal("0.1")); // 0.05~2.0 (管网标准) ✓ - record.setColor(new BigDecimal("5")); // ≤15 ✓ - record.setOdor(new BigDecimal("0")); // ≤2 ✓ - record.setEcoli(BigDecimal.ZERO); - record.setColonyCount(new BigDecimal("50")); + QualityTestRecord result = ledgerService.getById(1L); - List unqualified = evaluateCompliance(record, standards); - assertTrue(unqualified.isEmpty(), - "管网末梢水浊度2.0应合格(标准≤3.0),余氯0.1应合格(标准≥0.05)"); + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("routine", result.getTestType()); } @Test - @DisplayName("合格判定 - null 值不参与判定") - void testComplianceNullValuesSkipped() { - List standards = buildDefaultStandards(); + @DisplayName("6. 获取不存在记录抛异常") + void testGetById_NotFound() { + when(recordMapper.selectById(999L)).thenReturn(null); - QualityTestRecord record = new QualityTestRecord(); - record.setWaterType("treated"); - record.setTurbidity(new BigDecimal("0.5")); - // 其他参数为 null - record.setPh(null); - record.setResidualChlorine(null); - - List unqualified = evaluateCompliance(record, standards); - assertTrue(unqualified.isEmpty(), "null值不应参与判定"); - } - - // ========== 3. 统计计算逻辑测试 ========== - - @Test - @DisplayName("合格率计算") - void testQualifiedRateCalculation() { - long total = 100; - long qualified = 95; - long unqualified = 3; - long pending = 2; - - BigDecimal rate = total > 0 - ? BigDecimal.valueOf(qualified * 100.0 / total).setScale(1, RoundingMode.HALF_UP) - : BigDecimal.ZERO; - - assertEquals(new BigDecimal("95.0"), rate); - assertEquals(total, qualified + unqualified + pending); + assertThrows(Exception.class, () -> ledgerService.getById(999L)); } @Test - @DisplayName("QualityStatVO 数据结构完整性") - void testStatVOStructure() { - QualityStatVO stat = new QualityStatVO(); - stat.setTotalCount(200L); - stat.setQualifiedCount(190L); - stat.setUnqualifiedCount(8L); - stat.setPendingCount(2L); - stat.setQualifiedRate(new BigDecimal("95.0")); + @DisplayName("7. 更新检测记录") + void testUpdateRecord() { + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); + when(standardService.getStandard(anyString(), anyString())).thenReturn(null); + when(recordMapper.updateById(any())).thenReturn(1); + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); - Map rateByWaterType = new LinkedHashMap<>(); - rateByWaterType.put("treated", new BigDecimal("97.5")); - rateByWaterType.put("network", new BigDecimal("92.3")); - stat.setRateByWaterType(rateByWaterType); + sampleRecord.setRemark("updated"); + QualityTestRecord result = ledgerService.update(1L, sampleRecord); - Map unqByParam = new LinkedHashMap<>(); - unqByParam.put("turbidity", 5L); - unqByParam.put("residual_chlorine", 3L); - stat.setUnqualifiedByParam(unqByParam); - - List> trend = new ArrayList<>(); - trend.add(Map.of("month", "2026-05", "rate", new BigDecimal("94.0"))); - trend.add(Map.of("month", "2026-06", "rate", new BigDecimal("96.0"))); - stat.setMonthlyTrend(trend); - - assertEquals(200L, stat.getTotalCount()); - assertEquals(2, stat.getRateByWaterType().size()); - assertEquals(2, stat.getUnqualifiedByParam().size()); - assertEquals(2, stat.getMonthlyTrend().size()); - assertEquals("turbidity", stat.getUnqualifiedByParam().keySet().iterator().next()); - } - - // ========== 4. 查询筛选逻辑测试 ========== - - @Test - @DisplayName("QualityQueryRequest 默认值和筛选") - void testQueryRequestDefaults() { - QualityQueryRequest req = new QualityQueryRequest(); - assertEquals(1, req.getPageNum()); - assertEquals(20, req.getPageSize()); - assertNull(req.getTestType()); - assertNull(req.getWaterType()); - assertNull(req.getArea()); - assertNull(req.getComplianceStatus()); - assertNull(req.getStartDate()); - assertNull(req.getEndDate()); - assertNull(req.getKeyword()); + assertNotNull(result); + verify(recordMapper).updateById(any()); } @Test - @DisplayName("多维度筛选逻辑") - void testMultiDimensionFilter() { - List records = buildMockRecords(); + @DisplayName("8. 删除检测记录") + void testDeleteRecord() { + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); + when(recordMapper.deleteById(1L)).thenReturn(1); - // 按水样类型筛选 - List filtered = records.stream() - .filter(r -> "treated".equals(r.getWaterType())) - .collect(Collectors.toList()); - assertEquals(3, filtered.size()); + ledgerService.delete(1L); - // 按合格状态筛选 - filtered = records.stream() - .filter(r -> "qualified".equals(r.getComplianceStatus())) - .collect(Collectors.toList()); - assertEquals(3, filtered.size()); - - // 按区域筛选 - filtered = records.stream() - .filter(r -> "一体化水厂".equals(r.getArea())) - .collect(Collectors.toList()); - assertEquals(2, filtered.size()); - - // 组合筛选: 出厂水 + 合格 - filtered = records.stream() - .filter(r -> "treated".equals(r.getWaterType()) && "qualified".equals(r.getComplianceStatus())) - .collect(Collectors.toList()); - assertEquals(2, filtered.size()); - - // 关键词搜索 (采样点) - String keyword = "出厂"; - filtered = records.stream() - .filter(r -> r.getSamplingPoint() != null && r.getSamplingPoint().contains(keyword)) - .collect(Collectors.toList()); - assertEquals(2, filtered.size()); + verify(recordMapper).deleteById(1L); } @Test - @DisplayName("分页参数计算") - void testPaginationCalculation() { - int total = 55; - int pageSize = 20; - int pages = (int) Math.ceil((double) total / pageSize); - assertEquals(3, pages); + @DisplayName("9. 批量删除记录") + void testBatchDelete() { + List ids = List.of(1L, 2L, 3L); + when(recordMapper.deleteBatchIds(ids)).thenReturn(3); - // 第2页偏移量 - int offset = (2 - 1) * pageSize; - assertEquals(20, offset); + ledgerService.batchDelete(ids); + + verify(recordMapper).deleteBatchIds(ids); } - // ========== 5. 检测计划调度测试 ========== + // ==================== 合格判定测试 ==================== @Test - @DisplayName("检测计划频率计算 - 日检/周检/月检") - void testPlanFrequencyCalculation() { - LocalDate baseDate = LocalDate.of(2026, 6, 14); + @DisplayName("10. 合格判定-pH偏低") + void testEvaluateCompliance_PhLow() { + sampleRecord.setPh(5.0); + when(standardService.getStandard("factoryWater", "turbidity")).thenReturn(buildStandard("turbidity", null, 1.0)); + when(standardService.getStandard("factoryWater", "ph")).thenReturn(buildStandard("ph", 6.5, 8.5)); + when(standardService.getStandard("factoryWater", "residualChlorine")).thenReturn(buildStandard("residualChlorine", 0.3, 4.0)); + when(standardService.getStandard("factoryWater", "color")).thenReturn(buildStandard("color", null, 15.0)); + when(standardService.getStandard("factoryWater", "ecoli")).thenReturn(buildStandard("ecoli", null, 0.0)); + when(standardService.getStandard("factoryWater", "colonyCount")).thenReturn(buildStandard("colonyCount", null, 100.0)); - // 日检 - assertEquals(baseDate.plusDays(1), calculateNextDate(baseDate, "daily")); - // 周检 - assertEquals(baseDate.plusWeeks(1), calculateNextDate(baseDate, "weekly")); - // 月检 - assertEquals(baseDate.plusMonths(1), calculateNextDate(baseDate, "monthly")); + ledgerService.evaluateCompliance(sampleRecord); + + assertEquals("unqualified", sampleRecord.getComplianceStatus()); + assertTrue(sampleRecord.getUnqualifiedItems().contains("pH")); + assertTrue(sampleRecord.getUnqualifiedItems().contains("偏低")); } @Test - @DisplayName("检测计划过期判定") - void testPlanExpiration() { - QualityTestPlan plan = new QualityTestPlan(); - plan.setStartDate(LocalDate.of(2026, 6, 1)); - plan.setEndDate(LocalDate.of(2026, 6, 30)); - plan.setFrequency("daily"); - plan.setStatus("active"); + @DisplayName("11. 合格判定-余氯偏高") + void testEvaluateCompliance_ChlorineHigh() { + sampleRecord.setResidualChlorine(5.0); + when(standardService.getStandard("factoryWater", "turbidity")).thenReturn(buildStandard("turbidity", null, 1.0)); + when(standardService.getStandard("factoryWater", "ph")).thenReturn(buildStandard("ph", 6.5, 8.5)); + when(standardService.getStandard("factoryWater", "residualChlorine")).thenReturn(buildStandard("residualChlorine", 0.3, 4.0)); + when(standardService.getStandard("factoryWater", "color")).thenReturn(buildStandard("color", null, 15.0)); + when(standardService.getStandard("factoryWater", "ecoli")).thenReturn(buildStandard("ecoli", null, 0.0)); + when(standardService.getStandard("factoryWater", "colonyCount")).thenReturn(buildStandard("colonyCount", null, 100.0)); - // 下次检测日期超出结束日期 - LocalDate nextDate = LocalDate.of(2026, 7, 1); - boolean isExpired = plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate()); - assertTrue(isExpired, "下次检测日期超出结束日期应判定为过期"); + ledgerService.evaluateCompliance(sampleRecord); - // 正常范围内 - nextDate = LocalDate.of(2026, 6, 15); - isExpired = plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate()); - assertFalse(isExpired, "正常范围内不应过期"); + assertEquals("unqualified", sampleRecord.getComplianceStatus()); + assertTrue(sampleRecord.getUnqualifiedItems().contains("余氯")); + assertTrue(sampleRecord.getUnqualifiedItems().contains("偏高")); } @Test - @DisplayName("检测计划到期判定") - void testPlanDueCheck() { - LocalDate today = LocalDate.of(2026, 6, 14); + @DisplayName("12. 合格判定-无标准时不判定") + void testEvaluateCompliance_NoStandard() { + sampleRecord.setWaterType("unknownType"); + when(standardService.getStandard(eq("unknownType"), anyString())).thenReturn(null); - QualityTestPlan duePlan = new QualityTestPlan(); - duePlan.setStatus("active"); - duePlan.setNextTestDate(LocalDate.of(2026, 6, 14)); - duePlan.setEndDate(null); // 长期 + ledgerService.evaluateCompliance(sampleRecord); - QualityTestPlan futurePlan = new QualityTestPlan(); - futurePlan.setStatus("active"); - futurePlan.setNextTestDate(LocalDate.of(2026, 6, 20)); - futurePlan.setEndDate(null); - - QualityTestPlan expiredPlan = new QualityTestPlan(); - expiredPlan.setStatus("active"); - expiredPlan.setNextTestDate(LocalDate.of(2026, 6, 10)); - expiredPlan.setEndDate(LocalDate.of(2026, 6, 12)); // 已过期 - - List plans = List.of(duePlan, futurePlan, expiredPlan); - - // 筛选到期计划: nextTestDate <= today AND (endDate IS NULL OR endDate >= today) - List duePlans = plans.stream() - .filter(p -> "active".equals(p.getStatus())) - .filter(p -> !p.getNextTestDate().isAfter(today)) - .filter(p -> p.getEndDate() == null || !p.getEndDate().isBefore(today)) - .collect(Collectors.toList()); - - assertEquals(1, duePlans.size(), "只有1个计划到期"); - assertEquals(duePlan, duePlans.get(0)); + assertEquals("qualified", sampleRecord.getComplianceStatus()); } - // ========== 6. 数据导出格式测试 ========== + // ==================== 统计测试 ==================== @Test - @DisplayName("检测类型格式化") - void testTestTypeFormatting() { - assertEquals("常规检测", formatTestType("routine")); - assertEquals("专项检测", formatTestType("special")); - assertEquals("投诉检测", formatTestType("complaint")); - assertEquals("", formatTestType(null)); + @DisplayName("13. 获取统计数据") + void testGetStatistics() { + List> statusStats = List.of( + Map.of("status", "qualified", "count", 80L), + Map.of("status", "unqualified", "count", 15L), + Map.of("status", "pending", "count", 5L) + ); + when(recordMapper.statByComplianceStatus(any(), any())).thenReturn(statusStats); + when(recordMapper.statRateByWaterType(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statRateByArea(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statMonthlyTrend(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statUnqualifiedByParam(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statParamAvg()).thenReturn(Map.of( + "avgTurbidity", 0.45, "avgPh", 7.1, "avgResidualChlorine", 0.6 + )); + + QualityStatVO vo = ledgerService.getStatistics(null, null); + + assertNotNull(vo); + assertEquals(100L, vo.getTotalRecords()); + assertEquals(80L, vo.getQualifiedCount()); + assertEquals(15L, vo.getUnqualifiedCount()); + assertEquals(5L, vo.getPendingCount()); + assertEquals(80.0, vo.getQualifiedRate()); } @Test - @DisplayName("水样类型格式化") - void testWaterTypeFormatting() { - assertEquals("原水", formatWaterType("raw")); - assertEquals("出厂水", formatWaterType("treated")); - assertEquals("管网末梢水", formatWaterType("network")); - assertEquals("", formatWaterType(null)); + @DisplayName("14. 统计-空数据") + void testGetStatistics_Empty() { + when(recordMapper.statByComplianceStatus(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statRateByWaterType(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statRateByArea(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statMonthlyTrend(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statUnqualifiedByParam(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statParamAvg()).thenReturn(null); + + QualityStatVO vo = ledgerService.getStatistics(null, null); + + assertEquals(0L, vo.getTotalRecords()); + assertEquals(0.0, vo.getQualifiedRate()); } + // ==================== 辅助方法测试 ==================== + @Test - @DisplayName("合格状态格式化") - void testComplianceFormatting() { - assertEquals("合格", formatCompliance("qualified")); - assertEquals("不合格", formatCompliance("unqualified")); - assertEquals("待判定", formatCompliance("pending")); - assertEquals("待判定", formatCompliance(null)); - } - - // ========== Helper Methods ========== - - private List buildDefaultStandards() { - List standards = new ArrayList<>(); - - // 浊度 - 出厂水 ≤1.0 - QualityStandard s1 = new QualityStandard(); - s1.setParamName("turbidity"); s1.setMinValue(null); s1.setMaxValue(new BigDecimal("1.0")); - s1.setWaterType("treated"); - standards.add(s1); - - // 浊度 - 管网 ≤3.0 - QualityStandard s1n = new QualityStandard(); - s1n.setParamName("turbidity"); s1n.setMinValue(null); s1n.setMaxValue(new BigDecimal("3.0")); - s1n.setWaterType("network"); - standards.add(s1n); - - // pH - 6.5~8.5 - QualityStandard s2 = new QualityStandard(); - s2.setParamName("ph"); s2.setMinValue(new BigDecimal("6.5")); s2.setMaxValue(new BigDecimal("8.5")); - s2.setWaterType("all"); - standards.add(s2); - - // 余氯 - 出厂水 0.3~2.0 - QualityStandard s3 = new QualityStandard(); - s3.setParamName("residual_chlorine"); s3.setMinValue(new BigDecimal("0.3")); - s3.setMaxValue(new BigDecimal("2.0")); s3.setWaterType("treated"); - standards.add(s3); - - // 余氯 - 管网 0.05~2.0 - QualityStandard s3n = new QualityStandard(); - s3n.setParamName("residual_chlorine"); s3n.setMinValue(new BigDecimal("0.05")); - s3n.setMaxValue(new BigDecimal("2.0")); s3n.setWaterType("network"); - standards.add(s3n); - - // 色度 ≤15 - QualityStandard s4 = new QualityStandard(); - s4.setParamName("color"); s4.setMinValue(null); s4.setMaxValue(new BigDecimal("15")); - s4.setWaterType("all"); - standards.add(s4); - - // 嗅味 ≤2 - QualityStandard s5 = new QualityStandard(); - s5.setParamName("odor"); s5.setMinValue(null); s5.setMaxValue(new BigDecimal("2")); - s5.setWaterType("all"); - standards.add(s5); - - // 大肠杆菌 =0 - QualityStandard s6 = new QualityStandard(); - s6.setParamName("ecoli"); s6.setMinValue(null); s6.setMaxValue(BigDecimal.ZERO); - s6.setWaterType("all"); - standards.add(s6); - - // 菌落总数 ≤100 - QualityStandard s7 = new QualityStandard(); - s7.setParamName("colony_count"); s7.setMinValue(null); s7.setMaxValue(new BigDecimal("100")); - s7.setWaterType("all"); - standards.add(s7); - - return standards; - } - - /** - * 模拟合格判定逻辑 (不依赖 Spring 容器) - */ - private List evaluateCompliance(QualityTestRecord record, List standards) { - String waterType = record.getWaterType(); - if (waterType == null) waterType = "treated"; - - List unqualified = new ArrayList<>(); - String wt = waterType; - - checkParam("turbidity", record.getTurbidity(), wt, standards, unqualified); - checkParam("ph", record.getPh(), wt, standards, unqualified); - checkParam("residual_chlorine", record.getResidualChlorine(), wt, standards, unqualified); - checkParam("color", record.getColor(), wt, standards, unqualified); - checkParam("odor", record.getOdor(), wt, standards, unqualified); - checkParam("ecoli", record.getEcoli(), wt, standards, unqualified); - checkParam("colony_count", record.getColonyCount(), wt, standards, unqualified); - - return unqualified; - } - - private void checkParam(String paramName, BigDecimal value, String waterType, - List standards, List unqualified) { - if (value == null) return; - - QualityStandard standard = standards.stream() - .filter(s -> paramName.equals(s.getParamName())) - .filter(s -> waterType.equals(s.getWaterType()) || "all".equals(s.getWaterType())) - .findFirst().orElse(null); - - if (standard == null) return; - - boolean isUnqualified = false; - if (standard.getMinValue() != null && value.compareTo(standard.getMinValue()) < 0) { - isUnqualified = true; - } - if (standard.getMaxValue() != null && value.compareTo(standard.getMaxValue()) > 0) { - isUnqualified = true; - } - - if (isUnqualified) { - unqualified.add("{\"param\":\"" + paramName + "\"}"); - } - } - - private LocalDate calculateNextDate(LocalDate current, String frequency) { - return switch (frequency) { - case "daily" -> current.plusDays(1); - case "weekly" -> current.plusWeeks(1); - case "monthly" -> current.plusMonths(1); - default -> current; - }; - } - - private List buildMockRecords() { - List records = new ArrayList<>(); - + @DisplayName("15. 获取区域列表") + void testGetAreaList() { QualityTestRecord r1 = new QualityTestRecord(); - r1.setId(1L); r1.setWaterType("treated"); r1.setArea("一体化水厂"); - r1.setSamplingPoint("出厂水口"); r1.setComplianceStatus("qualified"); - r1.setTestDate(LocalDate.of(2026, 6, 14)); - records.add(r1); - + r1.setArea("城区A"); QualityTestRecord r2 = new QualityTestRecord(); - r2.setId(2L); r2.setWaterType("treated"); r2.setArea("一体化水厂"); - r2.setSamplingPoint("出厂水口"); r2.setComplianceStatus("qualified"); - r2.setTestDate(LocalDate.of(2026, 6, 13)); - records.add(r2); + r2.setArea("城区B"); + when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2)); - QualityTestRecord r3 = new QualityTestRecord(); - r3.setId(3L); r3.setWaterType("network"); r3.setArea("管网一区"); - r3.setSamplingPoint("末梢点A"); r3.setComplianceStatus("unqualified"); - r3.setTestDate(LocalDate.of(2026, 6, 14)); - records.add(r3); + List areas = ledgerService.getAreaList(); - QualityTestRecord r4 = new QualityTestRecord(); - r4.setId(4L); r4.setWaterType("treated"); r4.setArea("二水厂"); - r4.setSamplingPoint("出厂水口"); r4.setComplianceStatus("qualified"); - r4.setTestDate(LocalDate.of(2026, 6, 12)); - records.add(r4); - - QualityTestRecord r5 = new QualityTestRecord(); - r5.setId(5L); r5.setWaterType("network"); r5.setArea("管网一区"); - r5.setSamplingPoint("末梢点B"); r5.setComplianceStatus("unqualified"); - r5.setTestDate(LocalDate.of(2026, 6, 11)); - records.add(r5); - - return records; + assertNotNull(areas); + assertEquals(2, areas.size()); + assertTrue(areas.contains("城区A")); + assertTrue(areas.contains("城区B")); } - private String formatTestType(String type) { - if (type == null) return ""; - return switch (type) { - case "routine" -> "常规检测"; - case "special" -> "专项检测"; - case "complaint" -> "投诉检测"; - default -> type; - }; + @Test + @DisplayName("16. 获取采样点列表") + void testGetSamplingPointList() { + QualityTestRecord r1 = new QualityTestRecord(); + r1.setSamplingPoint("采样点A"); + QualityTestRecord r2 = new QualityTestRecord(); + r2.setSamplingPoint("采样点B"); + when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2)); + + List points = ledgerService.getSamplingPointList(); + + assertNotNull(points); + assertEquals(2, points.size()); } - private String formatWaterType(String type) { - if (type == null) return ""; - return switch (type) { - case "raw" -> "原水"; - case "treated" -> "出厂水"; - case "network" -> "管网末梢水"; - default -> type; - }; + // ==================== 计划测试 ==================== + + @Test + @DisplayName("17. 创建检测计划") + void testCreatePlan() { + QualityTestPlan plan = buildPlan(); + when(planMapper.insert(any())).thenReturn(1); + + QualityTestPlan result = planService.create(plan); + + assertEquals("active", result.getStatus()); + assertEquals(0, result.getExecutionCount()); + verify(planMapper).insert(plan); } - private String formatCompliance(String status) { - if (status == null) return "待判定"; - return switch (status) { - case "qualified" -> "合格"; - case "unqualified" -> "不合格"; - case "pending" -> "待判定"; - default -> status; - }; + @Test + @DisplayName("18. 标记计划已执行-日检+1天") + void testMarkPlanExecuted() { + QualityTestPlan plan = buildPlan(); + plan.setId(1L); + plan.setNextTestDate(LocalDate.of(2026, 6, 14)); + plan.setExecutionCount(5); + when(planMapper.selectById(1L)).thenReturn(plan); + when(planMapper.updateById(any())).thenReturn(1); + + QualityTestPlan result = planService.markExecuted(1L); + + assertEquals(6, result.getExecutionCount()); + assertEquals(LocalDate.of(2026, 6, 15), result.getNextTestDate()); + } + + // ==================== 辅助方法 ==================== + + private QualityStandard buildStandard(String paramName, Double min, Double max) { + QualityStandard s = new QualityStandard(); + s.setParamName(paramName); + s.setMinValue(min); + s.setMaxValue(max); + s.setEnabled(true); + return s; + } + + private QualityTestPlan buildPlan() { + QualityTestPlan plan = new QualityTestPlan(); + plan.setPlanName("日检计划"); + plan.setTestType("routine"); + plan.setWaterType("factoryWater"); + plan.setSamplingPoint("出厂水采样点A"); + plan.setArea("城区A"); + plan.setFrequency("daily"); + plan.setStartDate(LocalDate.now()); + return plan; } }