Merge remote-tracking branch 'origin/feature/issue-66'
# Conflicts: # wm-production/pom.xml # wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java # wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java # wm-production/src/main/java/com/water/production/dto/QualityStatVO.java # wm-production/src/main/java/com/water/production/entity/QualityStandard.java # wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java # wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java # wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java # wm-production/src/main/java/com/water/production/service/QualityLedgerService.java # wm-production/src/main/java/com/water/production/service/QualityStandardService.java # wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java # wm-production/src/main/resources/db/V4__quality_ledger.sql # wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml # wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java
This commit is contained in:
@@ -14,6 +14,5 @@
|
||||
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
|
||||
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId></dependency>
|
||||
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId></dependency>
|
||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+67
-116
@@ -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<Map<String, Object>> listRecords(QualityQueryRequest request) {
|
||||
public R<Map<String, Object>> queryRecords(QualityQueryRequest request) {
|
||||
return R.ok(ledgerService.queryRecords(request));
|
||||
}
|
||||
|
||||
// 2. 获取记录详情
|
||||
@Operation(summary = "获取检测记录详情")
|
||||
@GetMapping("/records/{id}")
|
||||
public R<QualityTestRecord> 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<QualityTestRecord> createRecord(@RequestBody QualityTestRecord record) {
|
||||
return R.ok(ledgerService.create(record));
|
||||
}
|
||||
|
||||
// 4. 更新检测记录
|
||||
@Operation(summary = "更新检测记录 (重新合格判定)")
|
||||
@Operation(summary = "更新检测记录")
|
||||
@PutMapping("/records/{id}")
|
||||
public R<String> updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) {
|
||||
record.setId(id);
|
||||
ledgerService.update(record);
|
||||
return R.ok("更新成功");
|
||||
public R<QualityTestRecord> updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) {
|
||||
return R.ok(ledgerService.update(id, record));
|
||||
}
|
||||
|
||||
// 5. 删除检测记录
|
||||
@Operation(summary = "删除检测记录")
|
||||
@DeleteMapping("/records/{id}")
|
||||
public R<String> deleteRecord(@PathVariable Long id) {
|
||||
public R<Void> deleteRecord(@PathVariable Long id) {
|
||||
ledgerService.delete(id);
|
||||
return R.ok("删除成功");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// 6. 批量删除
|
||||
@Operation(summary = "批量删除检测记录")
|
||||
@DeleteMapping("/records/batch")
|
||||
public R<String> batchDeleteRecords(@RequestBody List<Long> ids) {
|
||||
public R<Void> batchDeleteRecords(@RequestBody List<Long> ids) {
|
||||
ledgerService.batchDelete(ids);
|
||||
return R.ok("批量删除成功");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// 7. 重新判定合格状态
|
||||
@Operation(summary = "重新判定所有记录合格状态")
|
||||
@Operation(summary = "重新判定所有记录")
|
||||
@PostMapping("/records/reevaluate")
|
||||
public R<Map<String, Object>> reevaluateRecords() {
|
||||
public R<Map<String, Object>> 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<List<String>> getAreas() {
|
||||
return R.ok(ledgerService.getAreaList());
|
||||
}
|
||||
|
||||
// 9. 获取采样点列表
|
||||
@Operation(summary = "获取所有采样点")
|
||||
@Operation(summary = "获取采样点列表")
|
||||
@GetMapping("/sampling-points")
|
||||
public R<List<String>> getSamplingPoints() {
|
||||
return R.ok(ledgerService.getSamplingPointList());
|
||||
}
|
||||
|
||||
// ==================== 水质标准管理 ====================
|
||||
// ==================== 标准 ====================
|
||||
|
||||
// 10. 获取所有启用的标准
|
||||
@Operation(summary = "获取启用中的水质标准列表")
|
||||
@Operation(summary = "获取启用的标准列表")
|
||||
@GetMapping("/standards")
|
||||
public R<List<QualityStandard>> listStandards() {
|
||||
return R.ok(standardService.listEnabled());
|
||||
}
|
||||
|
||||
// 11. 获取全部标准 (含停用)
|
||||
@Operation(summary = "获取所有水质标准 (含停用)")
|
||||
@Operation(summary = "获取全部标准")
|
||||
@GetMapping("/standards/all")
|
||||
public R<List<QualityStandard>> listAllStandards() {
|
||||
return R.ok(standardService.listAll());
|
||||
}
|
||||
|
||||
// 12. 按水样类型获取标准
|
||||
@Operation(summary = "按水样类型获取水质标准")
|
||||
@Operation(summary = "按水质类型获取标准")
|
||||
@GetMapping("/standards/water-type/{waterType}")
|
||||
public R<List<QualityStandard>> listStandardsByWaterType(@PathVariable String waterType) {
|
||||
return R.ok(standardService.listByWaterType(waterType));
|
||||
}
|
||||
|
||||
// 13. 获取标准详情
|
||||
@Operation(summary = "获取水质标准详情")
|
||||
@Operation(summary = "获取标准详情")
|
||||
@GetMapping("/standards/{id}")
|
||||
public R<QualityStandard> 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<QualityStandard> createStandard(@RequestBody QualityStandard standard) {
|
||||
return R.ok(standardService.create(standard));
|
||||
}
|
||||
|
||||
// 15. 更新标准
|
||||
@Operation(summary = "更新水质标准")
|
||||
@Operation(summary = "更新标准")
|
||||
@PutMapping("/standards/{id}")
|
||||
public R<String> updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) {
|
||||
standard.setId(id);
|
||||
standardService.update(standard);
|
||||
return R.ok("更新成功");
|
||||
public R<QualityStandard> updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) {
|
||||
return R.ok(standardService.update(id, standard));
|
||||
}
|
||||
|
||||
// 16. 删除标准
|
||||
@Operation(summary = "删除水质标准")
|
||||
@Operation(summary = "删除标准")
|
||||
@DeleteMapping("/standards/{id}")
|
||||
public R<String> deleteStandard(@PathVariable Long id) {
|
||||
public R<Void> deleteStandard(@PathVariable Long id) {
|
||||
standardService.delete(id);
|
||||
return R.ok("删除成功");
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 检测计划管理 ====================
|
||||
// ==================== 计划 ====================
|
||||
|
||||
// 17. 分页查询检测计划
|
||||
@Operation(summary = "分页查询检测计划")
|
||||
@Operation(summary = "查询检测计划(分页)")
|
||||
@GetMapping("/plans")
|
||||
public R<Map<String, Object>> listPlans(
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String frequency,
|
||||
@RequestParam(required = false) String waterType,
|
||||
@RequestParam(required = false) String keyword,
|
||||
public R<Page<QualityTestPlan>> 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<QualityTestPlan> 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<QualityTestPlan> createPlan(@RequestBody QualityTestPlan plan) {
|
||||
return R.ok(planService.create(plan));
|
||||
}
|
||||
|
||||
// 20. 更新检测计划
|
||||
@Operation(summary = "更新检测计划")
|
||||
@Operation(summary = "更新计划")
|
||||
@PutMapping("/plans/{id}")
|
||||
public R<String> updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) {
|
||||
plan.setId(id);
|
||||
planService.update(plan);
|
||||
return R.ok("更新成功");
|
||||
public R<QualityTestPlan> updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) {
|
||||
return R.ok(planService.update(id, plan));
|
||||
}
|
||||
|
||||
// 21. 删除检测计划
|
||||
@Operation(summary = "删除检测计划")
|
||||
@Operation(summary = "删除计划")
|
||||
@DeleteMapping("/plans/{id}")
|
||||
public R<String> deletePlan(@PathVariable Long id) {
|
||||
public R<Void> 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<String> togglePlanStatus(@PathVariable Long id, @RequestParam String status) {
|
||||
planService.toggleStatus(id, status);
|
||||
return R.ok("状态已更新");
|
||||
public R<QualityTestPlan> togglePlanStatus(@PathVariable Long id) {
|
||||
return R.ok(planService.toggleStatus(id));
|
||||
}
|
||||
|
||||
// 23. 获取到期计划
|
||||
@Operation(summary = "获取当前到期的检测计划")
|
||||
@Operation(summary = "获取到期计划")
|
||||
@GetMapping("/plans/due")
|
||||
public R<List<QualityTestPlan>> getDuePlans() {
|
||||
return R.ok(planService.getDuePlans());
|
||||
}
|
||||
|
||||
// 24. 标记计划已执行
|
||||
@Operation(summary = "标记检测计划已执行 (更新下次检测日期)")
|
||||
@Operation(summary = "标记计划已执行")
|
||||
@PostMapping("/plans/{id}/execute")
|
||||
public R<String> markPlanExecuted(@PathVariable Long id) {
|
||||
planService.markExecuted(id);
|
||||
return R.ok("已标记执行");
|
||||
public R<QualityTestPlan> markPlanExecuted(@PathVariable Long id) {
|
||||
return R.ok(planService.markExecuted(id));
|
||||
}
|
||||
|
||||
// ==================== 统计分析 ====================
|
||||
// ==================== 统计 ====================
|
||||
|
||||
// 25. 综合统计
|
||||
@Operation(summary = "水质检测统计分析 (合格率/趋势/指标分布)")
|
||||
@Operation(summary = "获取统计数据")
|
||||
@GetMapping("/statistics")
|
||||
public R<QualityStatVO> 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<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String, BigDecimal> rateByWaterType;
|
||||
/** 按水质类型统计 */
|
||||
private List<Map<String, Object>> byWaterType;
|
||||
|
||||
/** 各区域合格率 */
|
||||
private Map<String, BigDecimal> rateByArea;
|
||||
/** 按区域统计 */
|
||||
private List<Map<String, Object>> byArea;
|
||||
|
||||
/** 各指标不合格次数 */
|
||||
private Map<String, Long> unqualifiedByParam;
|
||||
/** 按检测类型统计 */
|
||||
private List<Map<String, Object>> byTestType;
|
||||
|
||||
/** 月度合格率趋势 [{month, rate}] */
|
||||
private List<Map<String, Object>> monthlyTrend;
|
||||
/** 按日期趋势 */
|
||||
private List<Map<String, Object>> trendByDate;
|
||||
|
||||
/** 各指标均值统计 */
|
||||
private Map<String, Map<String, Object>> paramAvgStats;
|
||||
/** 不合格项目统计 */
|
||||
private List<Map<String, Object>> unqualifiedItems;
|
||||
|
||||
/** 平均浊度 */
|
||||
private Double avgTurbidity;
|
||||
|
||||
/** 平均pH */
|
||||
private Double avgPh;
|
||||
|
||||
/** 平均余氯 */
|
||||
private Double avgResidualChlorine;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+47
-41
@@ -11,52 +11,58 @@ import java.util.Map;
|
||||
@Mapper
|
||||
public interface QualityTestRecordMapper extends BaseMapper<QualityTestRecord> {
|
||||
|
||||
/** 分页查询台账 (支持多维度筛选) */
|
||||
List<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> statByComplianceStatus(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
List<Map<String, Object>> statByComplianceStatus(
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
/** 按水样类型统计合格率 */
|
||||
List<Map<String, Object>> statRateByWaterType(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
List<Map<String, Object>> statRateByWaterType(
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
/** 按区域统计合格率 */
|
||||
List<Map<String, Object>> statRateByArea(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
List<Map<String, Object>> statRateByArea(
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
/** 按指标统计不合格次数 */
|
||||
List<Map<String, Object>> statUnqualifiedByParam(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
List<Map<String, Object>> statUnqualifiedByParam(
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
/** 月度合格率趋势 */
|
||||
List<Map<String, Object>> statMonthlyTrend(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
List<Map<String, Object>> statMonthlyTrend(
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate
|
||||
);
|
||||
|
||||
/** 各指标均值统计 */
|
||||
List<Map<String, Object>> statParamAvg();
|
||||
Map<String, Object> statParamAvg();
|
||||
}
|
||||
|
||||
+170
-274
@@ -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<String, Object> 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<String, Object> 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<Long> 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<String> unqualifiedItems = new ArrayList<>();
|
||||
List<String> 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<String> unqualifiedItems) {
|
||||
private void checkParam(String waterType, String paramName, Double value, String label,
|
||||
List<String> 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<String> items) {
|
||||
return "[" + String.join(",", items) + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重新判定所有记录
|
||||
* 重新判定所有记录
|
||||
*/
|
||||
@Transactional
|
||||
public int reevaluateAll() {
|
||||
List<QualityTestRecord> records = recordMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityTestRecord>().eq(QualityTestRecord::getDeleted, 0)
|
||||
);
|
||||
LambdaQueryWrapper<QualityTestRecord> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.in(QualityTestRecord::getComplianceStatus, "pending", "qualified", "unqualified");
|
||||
List<QualityTestRecord> 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<Map<String, Object>> statusStats = recordMapper.statByComplianceStatus(startDate, endDate);
|
||||
long total = 0, qualified = 0, unqualified = 0, pending = 0;
|
||||
for (Map<String, Object> 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<Map<String, Object>> waterTypeStats = recordMapper.statRateByWaterType(startDate, endDate);
|
||||
Map<String, BigDecimal> rateByWaterType = new LinkedHashMap<>();
|
||||
for (Map<String, Object> 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<Map<String, Object>> areaStats = recordMapper.statRateByArea(startDate, endDate);
|
||||
Map<String, BigDecimal> rateByArea = new LinkedHashMap<>();
|
||||
for (Map<String, Object> 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<Map<String, Object>> unqStats = recordMapper.statUnqualifiedByParam(startDate, endDate);
|
||||
Map<String, Long> unqByParam = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : unqStats) {
|
||||
unqByParam.put((String) row.get("paramName"), ((Number) row.get("count")).longValue());
|
||||
}
|
||||
stat.setUnqualifiedByParam(unqByParam);
|
||||
// 按检测类型
|
||||
vo.setByTestType(recordMapper.statByComplianceStatus(startDate, endDate));
|
||||
|
||||
// 月度趋势
|
||||
List<Map<String, Object>> trend = recordMapper.statMonthlyTrend(startDate, endDate);
|
||||
stat.setMonthlyTrend(trend);
|
||||
vo.setTrendByDate(recordMapper.statMonthlyTrend(startDate, endDate));
|
||||
|
||||
// 各指标均值
|
||||
List<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> result = queryRecords(request);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> records = (List<Map<String, Object>>) result.get("records");
|
||||
|
||||
List<List<String>> excelData = records.stream().map(r -> {
|
||||
List<String> 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<List<String>> 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<List<String>> head = buildExportHead();
|
||||
List<List<Object>> 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<Object>) (List<?>) row).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private List<List<String>> buildExportHead() {
|
||||
List<List<String>> 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<List<Object>> buildExportData(List<Map<String, Object>> records) {
|
||||
List<List<Object>> data = new ArrayList<>();
|
||||
for (Map<String, Object> r : records) {
|
||||
List<Object> 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<String> getAreaList() {
|
||||
List<QualityTestRecord> records = recordMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityTestRecord>()
|
||||
.select(QualityTestRecord::getArea)
|
||||
.isNotNull(QualityTestRecord::getArea)
|
||||
.groupBy(QualityTestRecord::getArea)
|
||||
);
|
||||
return records.stream()
|
||||
LambdaQueryWrapper<QualityTestRecord> 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<String> getSamplingPointList() {
|
||||
List<QualityTestRecord> records = recordMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityTestRecord>()
|
||||
.select(QualityTestRecord::getSamplingPoint)
|
||||
.isNotNull(QualityTestRecord::getSamplingPoint)
|
||||
.groupBy(QualityTestRecord::getSamplingPoint)
|
||||
);
|
||||
return records.stream()
|
||||
LambdaQueryWrapper<QualityTestRecord> 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<QualityTestRecord> listByIds(List<Long> ids) {
|
||||
return recordMapper.selectBatchIds(ids);
|
||||
}
|
||||
}
|
||||
|
||||
+52
-51
@@ -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<QualityStandard> listEnabled() {
|
||||
return standardMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityStandard>()
|
||||
.eq(QualityStandard::getEnabled, 1)
|
||||
.orderByAsc(QualityStandard::getParamName)
|
||||
);
|
||||
LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(QualityStandard::getEnabled, true)
|
||||
.orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName);
|
||||
return standardMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按水样类型获取启用的标准
|
||||
* 按水质类型获取标准
|
||||
*/
|
||||
public List<QualityStandard> listByWaterType(String waterType) {
|
||||
return standardMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityStandard>()
|
||||
.eq(QualityStandard::getEnabled, 1)
|
||||
.and(w -> w.eq(QualityStandard::getWaterType, waterType)
|
||||
.or().eq(QualityStandard::getWaterType, "all"))
|
||||
.orderByAsc(QualityStandard::getParamName)
|
||||
);
|
||||
LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(QualityStandard::getEnabled, true)
|
||||
.eq(QualityStandard::getWaterType, waterType)
|
||||
.orderByAsc(QualityStandard::getParamName);
|
||||
return standardMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部标准(含禁用)
|
||||
*/
|
||||
public List<QualityStandard> listAll() {
|
||||
LambdaQueryWrapper<QualityStandard> 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<QualityStandard> 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<QualityStandard> listAll() {
|
||||
return standardMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityStandard>()
|
||||
.orderByAsc(QualityStandard::getStandardCode)
|
||||
.orderByAsc(QualityStandard::getParamName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据水样类型和参数名获取标准
|
||||
*/
|
||||
public QualityStandard getStandard(String waterType, String paramName) {
|
||||
return standardMapper.selectOne(
|
||||
new LambdaQueryWrapper<QualityStandard>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
+77
-80
@@ -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<String, Object> queryPlans(String status, String frequency, String waterType,
|
||||
String keyword, int pageNum, int pageSize) {
|
||||
public Page<QualityTestPlan> queryPlans(int pageNum, int pageSize, String testType, String waterType,
|
||||
String area, String status, String keyword) {
|
||||
Page<QualityTestPlan> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<QualityTestPlan> 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<QualityTestPlan> 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<QualityTestPlan> getDuePlans() {
|
||||
return planMapper.selectList(
|
||||
new LambdaQueryWrapper<QualityTestPlan>()
|
||||
.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<QualityTestPlan> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,69 +2,55 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.water.production.mapper.QualityTestRecordMapper">
|
||||
|
||||
<!-- 通用 WHERE 条件 -->
|
||||
<sql id="queryConditions">
|
||||
WHERE r.deleted = 0
|
||||
<if test="testType != null and testType != ''">AND r.test_type = #{testType}</if>
|
||||
<if test="waterType != null and waterType != ''">AND r.water_type = #{waterType}</if>
|
||||
<if test="area != null and area != ''">AND r.area = #{area}</if>
|
||||
<if test="samplingPoint != null and samplingPoint != ''">
|
||||
AND r.sampling_point LIKE '%' || #{samplingPoint} || '%'
|
||||
</if>
|
||||
<if test="tester != null and tester != ''">
|
||||
AND r.tester LIKE '%' || #{tester} || '%'
|
||||
</if>
|
||||
<if test="complianceStatus != null and complianceStatus != ''">
|
||||
AND r.compliance_status = #{complianceStatus}
|
||||
</if>
|
||||
<if test="startDate != null and startDate != ''">AND r.test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND r.test_date <= #{endDate}::date</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (r.sampling_point LIKE '%' || #{keyword} || '%'
|
||||
OR r.tester LIKE '%' || #{keyword} || '%'
|
||||
OR r.area LIKE '%' || #{keyword} || '%'
|
||||
OR r.remark LIKE '%' || #{keyword} || '%')
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<!-- 分页查询台账 -->
|
||||
<select id="selectRecordPage" resultType="java.util.Map">
|
||||
SELECT
|
||||
r.id, r.test_type AS "testType", r.water_type AS "waterType",
|
||||
r.sampling_point AS "samplingPoint", r.area, r.test_date AS "testDate",
|
||||
r.test_time AS "testTime", r.tester, r.turbidity, r.ph,
|
||||
r.residual_chlorine AS "residualChlorine", r.color, r.odor,
|
||||
r.ecoli, r.colony_count AS "colonyCount",
|
||||
r.compliance_status AS "complianceStatus",
|
||||
r.unqualified_items AS "unqualifiedItems", r.remark,
|
||||
r.created_at AS "createdAt", r.updated_at AS "updatedAt"
|
||||
FROM prod_quality_test_record r
|
||||
<include refid="queryConditions"/>
|
||||
id, test_type, water_type, sampling_point, area, test_date, test_time,
|
||||
tester, turbidity, ph, residual_chlorine, color, odor, ecoli, colony_count,
|
||||
compliance_status, unqualified_items, remark, created_at, updated_at
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="testType != null and testType != ''">AND test_type = #{testType}</if>
|
||||
<if test="waterType != null and waterType != ''">AND water_type = #{waterType}</if>
|
||||
<if test="area != null and area != ''">AND area = #{area}</if>
|
||||
<if test="samplingPoint != null and samplingPoint != ''">AND sampling_point = #{samplingPoint}</if>
|
||||
<if test="tester != null and tester != ''">AND tester LIKE '%' || #{tester} || '%'</if>
|
||||
<if test="complianceStatus != null and complianceStatus != ''">AND compliance_status = #{complianceStatus}</if>
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND test_date <= #{endDate}::date</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (sampling_point LIKE '%' || #{keyword} || '%' OR tester LIKE '%' || #{keyword} || '%' OR remark LIKE '%' || #{keyword} || '%')
|
||||
</if>
|
||||
<choose>
|
||||
<when test="sortField != null and sortField == 'testDate'">
|
||||
ORDER BY r.test_date
|
||||
<if test="sortOrder != null and sortOrder == 'asc'">ASC</if>
|
||||
<if test="sortOrder == null or sortOrder != 'asc'">DESC</if>
|
||||
<when test="sortField != null and sortField != '' and sortOrder != null and sortOrder == 'asc'">
|
||||
ORDER BY ${sortField} ASC
|
||||
</when>
|
||||
<when test="sortField != null and sortField == 'tester'">
|
||||
ORDER BY r.tester
|
||||
<if test="sortOrder != null and sortOrder == 'asc'">ASC</if>
|
||||
<if test="sortOrder == null or sortOrder != 'asc'">DESC</if>
|
||||
<when test="sortField != null and sortField != '' and sortOrder != null and sortOrder == 'desc'">
|
||||
ORDER BY ${sortField} DESC
|
||||
</when>
|
||||
<otherwise>ORDER BY r.created_at DESC</otherwise>
|
||||
<otherwise>ORDER BY created_at DESC</otherwise>
|
||||
</choose>
|
||||
OFFSET #{offset} LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 统计总数 -->
|
||||
<select id="countRecords" resultType="java.lang.Long">
|
||||
SELECT COUNT(*) FROM prod_quality_test_record r
|
||||
<include refid="queryConditions"/>
|
||||
SELECT COUNT(*)
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="testType != null and testType != ''">AND test_type = #{testType}</if>
|
||||
<if test="waterType != null and waterType != ''">AND water_type = #{waterType}</if>
|
||||
<if test="area != null and area != ''">AND area = #{area}</if>
|
||||
<if test="samplingPoint != null and samplingPoint != ''">AND sampling_point = #{samplingPoint}</if>
|
||||
<if test="tester != null and tester != ''">AND tester LIKE '%' || #{tester} || '%'</if>
|
||||
<if test="complianceStatus != null and complianceStatus != ''">AND compliance_status = #{complianceStatus}</if>
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND test_date <= #{endDate}::date</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (sampling_point LIKE '%' || #{keyword} || '%' OR tester LIKE '%' || #{keyword} || '%' OR remark LIKE '%' || #{keyword} || '%')
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 按合格状态统计 -->
|
||||
<select id="statByComplianceStatus" resultType="java.util.Map">
|
||||
SELECT compliance_status AS "status", COUNT(*) AS count
|
||||
SELECT compliance_status AS status, COUNT(*) AS count
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
@@ -72,11 +58,12 @@
|
||||
GROUP BY compliance_status
|
||||
</select>
|
||||
|
||||
<!-- 按水样类型统计合格率 -->
|
||||
<select id="statRateByWaterType" resultType="java.util.Map">
|
||||
SELECT water_type AS "waterType",
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified
|
||||
SELECT
|
||||
water_type AS waterType,
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
|
||||
ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
@@ -84,11 +71,12 @@
|
||||
GROUP BY water_type
|
||||
</select>
|
||||
|
||||
<!-- 按区域统计合格率 -->
|
||||
<select id="statRateByArea" resultType="java.util.Map">
|
||||
SELECT area,
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified
|
||||
SELECT
|
||||
area,
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
|
||||
ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
@@ -96,55 +84,39 @@
|
||||
GROUP BY area
|
||||
</select>
|
||||
|
||||
<!-- 按指标统计不合格次数 -->
|
||||
<select id="statUnqualifiedByParam" resultType="java.util.Map">
|
||||
SELECT param_name AS "paramName", COUNT(*) AS count
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(
|
||||
CASE
|
||||
WHEN unqualified_items IS NOT NULL AND unqualified_items != ''
|
||||
THEN unqualified_items::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
)->>'param' AS param_name
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0 AND compliance_status = 'unqualified'
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND test_date <= #{endDate}::date</if>
|
||||
) sub
|
||||
WHERE param_name IS NOT NULL
|
||||
GROUP BY param_name
|
||||
SELECT
|
||||
jsonb_array_elements_text(unqualified_items::jsonb) AS param,
|
||||
COUNT(*) AS count
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0 AND compliance_status = 'unqualified' AND unqualified_items IS NOT NULL
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND test_date <= #{endDate}::date</if>
|
||||
GROUP BY param
|
||||
ORDER BY count DESC
|
||||
</select>
|
||||
|
||||
<!-- 月度合格率趋势 -->
|
||||
<select id="statMonthlyTrend" resultType="java.util.Map">
|
||||
SELECT TO_CHAR(test_date, 'YYYY-MM') AS month,
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
|
||||
ROUND(
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / COUNT(*), 1
|
||||
) AS rate
|
||||
SELECT
|
||||
TO_CHAR(test_date, 'YYYY-MM') AS month,
|
||||
COUNT(*) AS total,
|
||||
COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
|
||||
ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
<if test="startDate != null and startDate != ''">AND test_date >= #{startDate}::date</if>
|
||||
<if test="endDate != null and endDate != ''">AND test_date <= #{endDate}::date</if>
|
||||
GROUP BY TO_CHAR(test_date, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
GROUP BY month
|
||||
ORDER BY month
|
||||
</select>
|
||||
|
||||
<!-- 各指标均值统计 -->
|
||||
<select id="statParamAvg" resultType="java.util.Map">
|
||||
SELECT
|
||||
ROUND(AVG(turbidity), 2) AS "turbidityAvg",
|
||||
ROUND(AVG(ph), 2) AS "phAvg",
|
||||
ROUND(AVG(residual_chlorine), 3) AS "residualChlorineAvg",
|
||||
ROUND(AVG(color), 2) AS "colorAvg",
|
||||
ROUND(AVG(odor), 2) AS "odorAvg",
|
||||
ROUND(AVG(ecoli), 2) AS "ecoliAvg",
|
||||
ROUND(AVG(colony_count), 2) AS "colonyCountAvg"
|
||||
ROUND(AVG(turbidity)::numeric, 4) AS avgTurbidity,
|
||||
ROUND(AVG(ph)::numeric, 4) AS avgPh,
|
||||
ROUND(AVG(residual_chlorine)::numeric, 4) AS avgResidualChlorine
|
||||
FROM prod_quality_test_record
|
||||
WHERE deleted = 0
|
||||
WHERE deleted = 0 AND compliance_status IS NOT NULL
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
+304
-491
@@ -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<QualityStandard> 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<String> unqualified = evaluateCompliance(record, standards);
|
||||
assertTrue(unqualified.isEmpty(), "所有指标在标准范围内应合格");
|
||||
assertEquals("pending", result.getComplianceStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GB5749-2022 合格判定逻辑 - 多项超标")
|
||||
void testComplianceMultipleUnqualified() {
|
||||
List<QualityStandard> 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<Map<String, Object>> 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<String> 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<String, Object> 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<QualityStandard> 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<String> 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<QualityStandard> 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<String> 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<String, BigDecimal> 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<String, Long> unqByParam = new LinkedHashMap<>();
|
||||
unqByParam.put("turbidity", 5L);
|
||||
unqByParam.put("residual_chlorine", 3L);
|
||||
stat.setUnqualifiedByParam(unqByParam);
|
||||
|
||||
List<Map<String, Object>> 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<QualityTestRecord> records = buildMockRecords();
|
||||
@DisplayName("8. 删除检测记录")
|
||||
void testDeleteRecord() {
|
||||
when(recordMapper.selectById(1L)).thenReturn(sampleRecord);
|
||||
when(recordMapper.deleteById(1L)).thenReturn(1);
|
||||
|
||||
// 按水样类型筛选
|
||||
List<QualityTestRecord> 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<Long> 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<QualityTestPlan> plans = List.of(duePlan, futurePlan, expiredPlan);
|
||||
|
||||
// 筛选到期计划: nextTestDate <= today AND (endDate IS NULL OR endDate >= today)
|
||||
List<QualityTestPlan> 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<Map<String, Object>> 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<QualityStandard> buildDefaultStandards() {
|
||||
List<QualityStandard> 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<String> evaluateCompliance(QualityTestRecord record, List<QualityStandard> standards) {
|
||||
String waterType = record.getWaterType();
|
||||
if (waterType == null) waterType = "treated";
|
||||
|
||||
List<String> 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<QualityStandard> standards, List<String> 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<QualityTestRecord> buildMockRecords() {
|
||||
List<QualityTestRecord> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user