diff --git a/wm-production/pom.xml b/wm-production/pom.xml index 7c7b09d1..f81b1a8b 100644 --- a/wm-production/pom.xml +++ b/wm-production/pom.xml @@ -12,5 +12,7 @@ com.baomidoumybatis-plus-spring-boot3-starter cn.dev33sa-token-spring-boot3-starter org.postgresqlpostgresql + com.alibabaeasyexcel + com.github.xiaoyminknife4j-openapi3-jakarta-spring-boot-starter \ No newline at end of file diff --git a/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java b/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java new file mode 100644 index 00000000..af5049f5 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java @@ -0,0 +1,214 @@ +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; +import com.water.production.entity.QualityStandard; +import com.water.production.entity.QualityTestPlan; +import com.water.production.entity.QualityTestRecord; +import com.water.production.service.QualityLedgerService; +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.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Tag(name = "水质检测台账管理") +@RestController +@RequestMapping("/api/production/quality") +@RequiredArgsConstructor +public class QualityLedgerController { + + private final QualityLedgerService ledgerService; + private final QualityStandardService standardService; + private final QualityTestPlanService planService; + + // ==================== 记录 CRUD ==================== + + @Operation(summary = "查询检测记录(分页)") + @GetMapping("/records") + public R> queryRecords(QualityQueryRequest request) { + return R.ok(ledgerService.queryRecords(request)); + } + + @Operation(summary = "获取检测记录详情") + @GetMapping("/records/{id}") + public R getRecord(@PathVariable Long id) { + return R.ok(ledgerService.getById(id)); + } + + @Operation(summary = "创建检测记录") + @PostMapping("/records") + public R createRecord(@RequestBody QualityTestRecord record) { + return R.ok(ledgerService.create(record)); + } + + @Operation(summary = "更新检测记录") + @PutMapping("/records/{id}") + public R updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) { + return R.ok(ledgerService.update(id, record)); + } + + @Operation(summary = "删除检测记录") + @DeleteMapping("/records/{id}") + public R deleteRecord(@PathVariable Long id) { + ledgerService.delete(id); + return R.ok(); + } + + @Operation(summary = "批量删除检测记录") + @DeleteMapping("/records/batch") + public R batchDeleteRecords(@RequestBody List ids) { + ledgerService.batchDelete(ids); + return R.ok(); + } + + @Operation(summary = "重新判定所有记录") + @PostMapping("/records/reevaluate") + public R> reevaluateAll() { + int count = ledgerService.reevaluateAll(); + return R.ok(Map.of("updatedCount", count)); + } + + // ==================== 辅助 ==================== + + @Operation(summary = "获取区域列表") + @GetMapping("/areas") + public R> getAreas() { + return R.ok(ledgerService.getAreaList()); + } + + @Operation(summary = "获取采样点列表") + @GetMapping("/sampling-points") + public R> getSamplingPoints() { + return R.ok(ledgerService.getSamplingPointList()); + } + + // ==================== 标准 ==================== + + @Operation(summary = "获取启用的标准列表") + @GetMapping("/standards") + public R> listStandards() { + return R.ok(standardService.listEnabled()); + } + + @Operation(summary = "获取全部标准") + @GetMapping("/standards/all") + public R> listAllStandards() { + return R.ok(standardService.listAll()); + } + + @Operation(summary = "按水质类型获取标准") + @GetMapping("/standards/water-type/{waterType}") + public R> listStandardsByWaterType(@PathVariable String waterType) { + return R.ok(standardService.listByWaterType(waterType)); + } + + @Operation(summary = "获取标准详情") + @GetMapping("/standards/{id}") + public R getStandard(@PathVariable Long id) { + return R.ok(standardService.getById(id)); + } + + @Operation(summary = "创建标准") + @PostMapping("/standards") + public R createStandard(@RequestBody QualityStandard standard) { + return R.ok(standardService.create(standard)); + } + + @Operation(summary = "更新标准") + @PutMapping("/standards/{id}") + public R updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) { + return R.ok(standardService.update(id, standard)); + } + + @Operation(summary = "删除标准") + @DeleteMapping("/standards/{id}") + public R deleteStandard(@PathVariable Long id) { + standardService.delete(id); + return R.ok(); + } + + // ==================== 计划 ==================== + + @Operation(summary = "查询检测计划(分页)") + @GetMapping("/plans") + public R> queryPlans( + @RequestParam(defaultValue = "1") int pageNum, + @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)); + } + + @Operation(summary = "获取计划详情") + @GetMapping("/plans/{id}") + public R getPlan(@PathVariable Long id) { + return R.ok(planService.getById(id)); + } + + @Operation(summary = "创建计划") + @PostMapping("/plans") + public R createPlan(@RequestBody QualityTestPlan plan) { + return R.ok(planService.create(plan)); + } + + @Operation(summary = "更新计划") + @PutMapping("/plans/{id}") + public R updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) { + return R.ok(planService.update(id, plan)); + } + + @Operation(summary = "删除计划") + @DeleteMapping("/plans/{id}") + public R deletePlan(@PathVariable Long id) { + planService.delete(id); + return R.ok(); + } + + @Operation(summary = "切换计划状态") + @PutMapping("/plans/{id}/status") + public R togglePlanStatus(@PathVariable Long id) { + return R.ok(planService.toggleStatus(id)); + } + + @Operation(summary = "获取到期计划") + @GetMapping("/plans/due") + public R> getDuePlans() { + return R.ok(planService.getDuePlans()); + } + + @Operation(summary = "标记计划已执行") + @PostMapping("/plans/{id}/execute") + public R markPlanExecuted(@PathVariable Long id) { + return R.ok(planService.markExecuted(id)); + } + + // ==================== 统计 ==================== + + @Operation(summary = "获取统计数据") + @GetMapping("/statistics") + public R getStatistics( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + return R.ok(ledgerService.getStatistics(startDate, endDate)); + } + + // ==================== 导出 ==================== + + @Operation(summary = "导出Excel") + @PostMapping("/export/excel") + public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException { + ledgerService.exportExcel(request, response); + } +} diff --git a/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java b/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java new file mode 100644 index 00000000..b342e236 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java @@ -0,0 +1,49 @@ +package com.water.production.dto; + +import lombok.Data; + +/** + * 水质检测查询请求 + */ +@Data +public class QualityQueryRequest { + + /** 检测类型 */ + private String testType; + + /** 水质类型 */ + private String waterType; + + /** 区域 */ + private String area; + + /** 采样点 */ + private String samplingPoint; + + /** 检测人 */ + private String tester; + + /** 合格状态 */ + private String complianceStatus; + + /** 开始日期 */ + private String startDate; + + /** 结束日期 */ + private String endDate; + + /** 关键字 */ + private String keyword; + + /** 排序字段 */ + private String sortField; + + /** 排序方向 */ + private String sortOrder; + + /** 页码 */ + private Integer pageNum = 1; + + /** 每页大小 */ + private Integer pageSize = 10; +} diff --git a/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java b/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java new file mode 100644 index 00000000..4e3b19e8 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/dto/QualityStatVO.java @@ -0,0 +1,52 @@ +package com.water.production.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 水质检测统计VO + */ +@Data +public class QualityStatVO { + + /** 总记录数 */ + private Long totalRecords; + + /** 合格数 */ + private Long qualifiedCount; + + /** 不合格数 */ + private Long unqualifiedCount; + + /** 待判定数 */ + private Long pendingCount; + + /** 合格率 */ + private Double qualifiedRate; + + /** 按水质类型统计 */ + private List> byWaterType; + + /** 按区域统计 */ + private List> byArea; + + /** 按检测类型统计 */ + private List> byTestType; + + /** 按日期趋势 */ + private List> trendByDate; + + /** 不合格项目统计 */ + private List> unqualifiedItems; + + /** 平均浊度 */ + private Double avgTurbidity; + + /** 平均pH */ + private Double avgPh; + + /** 平均余氯 */ + private Double avgResidualChlorine; +} diff --git a/wm-production/src/main/java/com/water/production/entity/QualityStandard.java b/wm-production/src/main/java/com/water/production/entity/QualityStandard.java new file mode 100644 index 00000000..2a2202a4 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/entity/QualityStandard.java @@ -0,0 +1,53 @@ +package com.water.production.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 水质标准 + */ +@Data +@TableName("prod_quality_standard") +public class QualityStandard { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 标准名称 */ + private String standardName; + + /** 标准编号 */ + private String standardCode; + + /** 参数名 */ + private String paramName; + + /** 参数标签 */ + private String paramLabel; + + /** 参数单位 */ + private String paramUnit; + + /** 最小值 */ + private Double minValue; + + /** 最大值 */ + private Double maxValue; + + /** 水质类型 */ + private String waterType; + + /** 是否启用 */ + private Boolean enabled; + + @TableLogic + private Integer deleted; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java b/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java new file mode 100644 index 00000000..8b2ab190 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java @@ -0,0 +1,66 @@ +package com.water.production.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 水质检测计划 + */ +@Data +@TableName("prod_quality_test_plan") +public class QualityTestPlan { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 计划名称 */ + private String planName; + + /** 检测类型 */ + private String testType; + + /** 水质类型 */ + private String waterType; + + /** 采样点 */ + private String samplingPoint; + + /** 区域 */ + private String area; + + /** 频率: daily/weekly/monthly */ + private String frequency; + + /** 检测参数 (JSON数组) */ + private String testParams; + + /** 开始日期 */ + private LocalDate startDate; + + /** 结束日期 */ + private LocalDate endDate; + + /** 下次检测日期 */ + private LocalDate nextTestDate; + + /** 状态: active/paused/expired */ + private String status; + + /** 执行次数 */ + private Integer executionCount; + + /** 备注 */ + private String remark; + + @TableLogic + private Integer deleted; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java b/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java new file mode 100644 index 00000000..a6d2dd92 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java @@ -0,0 +1,79 @@ +package com.water.production.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * 水质检测记录 + */ +@Data +@TableName("prod_quality_test_record") +public class QualityTestRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + /** 检测类型: 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; + + /** 浊度 (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(fill = FieldFill.INSERT) + private LocalDateTime createdAt; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedAt; +} diff --git a/wm-production/src/main/java/com/water/production/mapper/QualityStandardMapper.java b/wm-production/src/main/java/com/water/production/mapper/QualityStandardMapper.java new file mode 100644 index 00000000..82c301ab --- /dev/null +++ b/wm-production/src/main/java/com/water/production/mapper/QualityStandardMapper.java @@ -0,0 +1,9 @@ +package com.water.production.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.production.entity.QualityStandard; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QualityStandardMapper extends BaseMapper { +} diff --git a/wm-production/src/main/java/com/water/production/mapper/QualityTestPlanMapper.java b/wm-production/src/main/java/com/water/production/mapper/QualityTestPlanMapper.java new file mode 100644 index 00000000..d513cb25 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/mapper/QualityTestPlanMapper.java @@ -0,0 +1,9 @@ +package com.water.production.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.production.entity.QualityTestPlan; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QualityTestPlanMapper extends BaseMapper { +} diff --git a/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java b/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java new file mode 100644 index 00000000..a8a01cf2 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java @@ -0,0 +1,68 @@ +package com.water.production.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.water.production.entity.QualityTestRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface QualityTestRecordMapper extends BaseMapper { + + List> selectRecordPage( + @Param("testType") String testType, + @Param("waterType") String waterType, + @Param("area") String area, + @Param("samplingPoint") String samplingPoint, + @Param("tester") String tester, + @Param("complianceStatus") String complianceStatus, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("keyword") String keyword, + @Param("sortField") String sortField, + @Param("sortOrder") String sortOrder, + @Param("offset") int offset, + @Param("limit") int limit + ); + + Long countRecords( + @Param("testType") String testType, + @Param("waterType") String waterType, + @Param("area") String area, + @Param("samplingPoint") String samplingPoint, + @Param("tester") String tester, + @Param("complianceStatus") String complianceStatus, + @Param("startDate") String startDate, + @Param("endDate") String endDate, + @Param("keyword") String keyword + ); + + List> statByComplianceStatus( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); + + List> statRateByWaterType( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); + + List> statRateByArea( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); + + List> statUnqualifiedByParam( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); + + List> statMonthlyTrend( + @Param("startDate") String startDate, + @Param("endDate") String endDate + ); + + Map statParamAvg(); +} diff --git a/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java b/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java new file mode 100644 index 00000000..803af132 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/service/QualityLedgerService.java @@ -0,0 +1,311 @@ +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.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class QualityLedgerService { + + private final QualityTestRecordMapper recordMapper; + private final QualityStandardService standardService; + + /** + * 分页查询检测记录 + */ + public Map queryRecords(QualityQueryRequest request) { + int offset = (request.getPageNum() - 1) * request.getPageSize(); + List> records = recordMapper.selectRecordPage( + 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() + ); + Long total = recordMapper.countRecords( + request.getTestType(), request.getWaterType(), request.getArea(), + request.getSamplingPoint(), request.getTester(), request.getComplianceStatus(), + request.getStartDate(), request.getEndDate(), request.getKeyword() + ); + + Map result = new LinkedHashMap<>(); + result.put("records", records); + result.put("total", total); + result.put("pageNum", request.getPageNum()); + result.put("pageSize", request.getPageSize()); + result.put("totalPages", (total + request.getPageSize() - 1) / request.getPageSize()); + return result; + } + + /** + * 获取记录详情 + */ + public QualityTestRecord getById(Long id) { + QualityTestRecord record = recordMapper.selectById(id); + if (record == null) { + throw new BusinessException("检测记录不存在"); + } + return record; + } + + /** + * 创建检测记录(自动合格判定) + */ + @Transactional + public QualityTestRecord create(QualityTestRecord record) { + if (record.getTestDate() == null) record.setTestDate(LocalDate.now()); + evaluateCompliance(record); + recordMapper.insert(record); + log.info("创建水质检测记录: {}/{}", record.getWaterType(), record.getSamplingPoint()); + return record; + } + + /** + * 更新检测记录 + */ + @Transactional + public QualityTestRecord update(Long id, QualityTestRecord record) { + QualityTestRecord existing = getById(id); + record.setId(existing.getId()); + evaluateCompliance(record); + recordMapper.updateById(record); + log.info("更新水质检测记录: {}", id); + return recordMapper.selectById(id); + } + + /** + * 删除检测记录 + */ + @Transactional + public void delete(Long id) { + getById(id); + recordMapper.deleteById(id); + log.info("删除水质检测记录: {}", id); + } + + /** + * 批量删除 + */ + @Transactional + public void batchDelete(List ids) { + if (ids == null || ids.isEmpty()) return; + recordMapper.deleteBatchIds(ids); + log.info("批量删除水质检测记录: {} 条", ids.size()); + } + + /** + * 对记录执行合格判定 + */ + public void evaluateCompliance(QualityTestRecord record) { + if (record.getWaterType() == null) { + record.setComplianceStatus("pending"); + return; + } + + List unqualified = new ArrayList<>(); + + 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 (unqualified.isEmpty()) { + record.setComplianceStatus("qualified"); + record.setUnqualifiedItems(null); + } else { + record.setComplianceStatus("unqualified"); + record.setUnqualifiedItems("[\"" + String.join("\",\"", unqualified) + "\"]"); + } + } + + private void checkParam(String waterType, String paramName, Double value, String label, + List unqualified) { + if (value == null) return; + QualityStandard standard = standardService.getStandard(waterType, paramName); + if (standard == null) return; + if (standard.getMinValue() != null && value < standard.getMinValue()) { + unqualified.add(label + "(偏低)"); + } + if (standard.getMaxValue() != null && value > standard.getMaxValue()) { + unqualified.add(label + "(偏高)"); + } + } + + /** + * 重新判定所有记录 + */ + @Transactional + public int reevaluateAll() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(QualityTestRecord::getComplianceStatus, "pending", "qualified", "unqualified"); + List records = recordMapper.selectList(wrapper); + int count = 0; + for (QualityTestRecord record : records) { + String oldStatus = record.getComplianceStatus(); + evaluateCompliance(record); + if (!oldStatus.equals(record.getComplianceStatus())) { + recordMapper.updateById(record); + count++; + } + } + log.info("重新判定完成,共 {} 条记录状态变更", count); + return count; + } + + /** + * 获取统计数据 + */ + public QualityStatVO getStatistics(String startDate, String endDate) { + QualityStatVO vo = new QualityStatVO(); + + // 按合格状态统计 + List> statusStats = recordMapper.statByComplianceStatus(startDate, endDate); + long total = 0, qualified = 0, unqualified = 0, pending = 0; + for (Map row : statusStats) { + 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; + } + } + 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); + + // 按水质类型 + vo.setByWaterType(recordMapper.statRateByWaterType(startDate, endDate)); + + // 按区域 + vo.setByArea(recordMapper.statRateByArea(startDate, endDate)); + + // 按检测类型 + vo.setByTestType(recordMapper.statByComplianceStatus(startDate, endDate)); + + // 月度趋势 + vo.setTrendByDate(recordMapper.statMonthlyTrend(startDate, endDate)); + + // 不合格项 + vo.setUnqualifiedItems(recordMapper.statUnqualifiedByParam(startDate, endDate)); + + // 参数均值 + Map avgMap = recordMapper.statParamAvg(); + if (avgMap != null) { + vo.setAvgTurbidity(toDouble(avgMap.get("avgTurbidity"))); + vo.setAvgPh(toDouble(avgMap.get("avgPh"))); + vo.setAvgResidualChlorine(toDouble(avgMap.get("avgResidualChlorine"))); + } + + return vo; + } + + private Double toDouble(Object val) { + if (val == null) return null; + if (val instanceof Number) return ((Number) val).doubleValue(); + return null; + } + + /** + * 导出Excel + */ + public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException { + request.setPageNum(1); + request.setPageSize(100000); + Map result = queryRecords(request); + @SuppressWarnings("unchecked") + List> records = (List>) result.get("records"); + + List> excelData = records.stream().map(r -> { + List row = new ArrayList<>(); + row.add(String.valueOf(r.getOrDefault("testType", ""))); + row.add(String.valueOf(r.getOrDefault("waterType", ""))); + row.add(String.valueOf(r.getOrDefault("samplingPoint", ""))); + row.add(String.valueOf(r.getOrDefault("area", ""))); + row.add(String.valueOf(r.getOrDefault("testDate", ""))); + row.add(String.valueOf(r.getOrDefault("tester", ""))); + row.add(String.valueOf(r.getOrDefault("turbidity", ""))); + row.add(String.valueOf(r.getOrDefault("ph", ""))); + row.add(String.valueOf(r.getOrDefault("residualChlorine", ""))); + row.add(String.valueOf(r.getOrDefault("color", ""))); + row.add(String.valueOf(r.getOrDefault("odor", ""))); + row.add(String.valueOf(r.getOrDefault("ecoli", ""))); + row.add(String.valueOf(r.getOrDefault("colonyCount", ""))); + row.add(String.valueOf(r.getOrDefault("complianceStatus", ""))); + row.add(String.valueOf(r.getOrDefault("unqualifiedItems", ""))); + row.add(String.valueOf(r.getOrDefault("remark", ""))); + return row; + }).collect(Collectors.toList()); + + List> head = List.of( + List.of("检测类型"), List.of("水质类型"), List.of("采样点"), List.of("区域"), + List.of("检测日期"), List.of("检测人"), List.of("浊度"), List.of("pH"), + List.of("余氯"), List.of("色度"), List.of("嗅味"), List.of("大肠杆菌"), + List.of("菌落总数"), List.of("合格状态"), List.of("不合格项"), List.of("备注") + ); + + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", + "attachment;filename=" + URLEncoder.encode("水质检测报告.xlsx", StandardCharsets.UTF_8)); + + EasyExcel.write(response.getOutputStream()) + .head(head) + .sheet("检测记录") + .doWrite(excelData.stream().map(row -> (List) (List) row).collect(Collectors.toList())); + } + + /** + * 获取区域列表 + */ + public List getAreaList() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(QualityTestRecord::getArea) + .isNotNull(QualityTestRecord::getArea) + .ne(QualityTestRecord::getArea, "") + .groupBy(QualityTestRecord::getArea); + return recordMapper.selectList(wrapper).stream() + .map(QualityTestRecord::getArea) + .distinct() + .sorted() + .collect(Collectors.toList()); + } + + /** + * 获取采样点列表 + */ + public List getSamplingPointList() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(QualityTestRecord::getSamplingPoint) + .isNotNull(QualityTestRecord::getSamplingPoint) + .ne(QualityTestRecord::getSamplingPoint, "") + .groupBy(QualityTestRecord::getSamplingPoint); + return recordMapper.selectList(wrapper).stream() + .map(QualityTestRecord::getSamplingPoint) + .distinct() + .sorted() + .collect(Collectors.toList()); + } +} diff --git a/wm-production/src/main/java/com/water/production/service/QualityStandardService.java b/wm-production/src/main/java/com/water/production/service/QualityStandardService.java new file mode 100644 index 00000000..bd834300 --- /dev/null +++ b/wm-production/src/main/java/com/water/production/service/QualityStandardService.java @@ -0,0 +1,105 @@ +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.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class QualityStandardService { + + private final QualityStandardMapper standardMapper; + + /** + * 获取所有启用的标准 + */ + public List listEnabled() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getEnabled, true) + .orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName); + return standardMapper.selectList(wrapper); + } + + /** + * 按水质类型获取标准 + */ + public List listByWaterType(String waterType) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getEnabled, true) + .eq(QualityStandard::getWaterType, waterType) + .orderByAsc(QualityStandard::getParamName); + return standardMapper.selectList(wrapper); + } + + /** + * 获取全部标准(含禁用) + */ + public List listAll() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName); + return standardMapper.selectList(wrapper); + } + + /** + * 获取标准详情 + */ + public QualityStandard getById(Long id) { + QualityStandard standard = standardMapper.selectById(id); + if (standard == null) { + throw new BusinessException("标准不存在"); + } + return standard; + } + + /** + * 获取指定水质类型和参数名的标准 + */ + public QualityStandard getStandard(String waterType, String paramName) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityStandard::getWaterType, waterType) + .eq(QualityStandard::getParamName, paramName) + .eq(QualityStandard::getEnabled, true) + .last("LIMIT 1"); + return standardMapper.selectOne(wrapper); + } + + /** + * 创建标准 + */ + @Transactional + public QualityStandard create(QualityStandard standard) { + standardMapper.insert(standard); + log.info("创建水质标准: {} - {}", standard.getStandardName(), standard.getParamLabel()); + return 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); + log.info("删除水质标准: {}", id); + } +} diff --git a/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java b/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java new file mode 100644 index 00000000..4bbf7cdc --- /dev/null +++ b/wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java @@ -0,0 +1,144 @@ +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; + +@Slf4j +@Service +@RequiredArgsConstructor +public class QualityTestPlanService { + + private final QualityTestPlanMapper planMapper; + + /** + * 分页查询计划 + */ + public Page queryPlans(int pageNum, int pageSize, String testType, String waterType, + String area, String status, String keyword) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (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)); + } + wrapper.orderByDesc(QualityTestPlan::getCreatedAt); + return planMapper.selectPage(page, wrapper); + } + + /** + * 获取计划详情 + */ + public QualityTestPlan getById(Long id) { + QualityTestPlan plan = planMapper.selectById(id); + if (plan == null) { + throw new BusinessException("检测计划不存在"); + } + return 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); + } + + /** + * 切换计划状态 + */ + @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"); + } + planMapper.updateById(plan); + log.info("切换检测计划状态: {} -> {}", id, plan.getStatus()); + return plan; + } + + /** + * 获取到期计划 + */ + public List getDuePlans() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QualityTestPlan::getStatus, "active") + .le(QualityTestPlan::getNextTestDate, LocalDate.now()) + .orderByAsc(QualityTestPlan::getNextTestDate); + return planMapper.selectList(wrapper); + } + + /** + * 标记已执行,计算下次检测日期 + */ + @Transactional + public QualityTestPlan markExecuted(Long id) { + QualityTestPlan plan = getById(id); + plan.setExecutionCount(plan.getExecutionCount() + 1); + + // 计算下次检测日期 + LocalDate nextDate = plan.getNextTestDate(); + if (nextDate == null) nextDate = LocalDate.now(); + + switch (plan.getFrequency() != null ? plan.getFrequency() : "daily") { + case "daily" -> nextDate = nextDate.plusDays(1); + case "weekly" -> nextDate = nextDate.plusWeeks(1); + case "monthly" -> nextDate = nextDate.plusMonths(1); + default -> nextDate = nextDate.plusDays(1); + } + plan.setNextTestDate(nextDate); + + // 如果下次检测超过结束日期,自动过期 + if (plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate())) { + plan.setStatus("expired"); + } + + planMapper.updateById(plan); + log.info("标记检测计划已执行: {}, 下次检测: {}", id, nextDate); + return plan; + } +} diff --git a/wm-production/src/main/resources/db/V4__quality_ledger.sql b/wm-production/src/main/resources/db/V4__quality_ledger.sql new file mode 100644 index 00000000..c75a7df6 --- /dev/null +++ b/wm-production/src/main/resources/db/V4__quality_ledger.sql @@ -0,0 +1,134 @@ +-- ============================================================ +-- V4: 水质检测台账 (GB5749-2022) +-- ============================================================ + +-- 1. 水质检测记录表 +CREATE TABLE IF NOT EXISTS prod_quality_test_record ( + 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_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_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/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. 水质标准表 +CREATE TABLE IF NOT EXISTS prod_quality_standard ( + id BIGSERIAL PRIMARY KEY, + 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 NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +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 '水质标准表'; +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(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, + next_test_date DATE, + status VARCHAR(20) NOT NULL DEFAULT 'active', + execution_count INTEGER NOT NULL DEFAULT 0, + 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_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_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/expired'; + +-- ============================================================ +-- GB5749-2022 默认标准数据 +-- ============================================================ + +-- 原水 (rawWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 4.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 10000.0, 'rawWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 500.0, 'rawWater', TRUE); + +-- 出厂水 (factoryWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.3, 4.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'factoryWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'factoryWater', TRUE); + +-- 管网水 (pipeNetworkWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'pipeNetworkWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'pipeNetworkWater', TRUE); + +-- 末梢水 (endUserWater) 标准 +INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES +('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'endUserWater', TRUE), +('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'endUserWater', TRUE); diff --git a/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml b/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml new file mode 100644 index 00000000..14304c73 --- /dev/null +++ b/wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java b/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java new file mode 100644 index 00000000..ec8894ca --- /dev/null +++ b/wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java @@ -0,0 +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.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class QualityLedgerServiceTest { + + @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("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); + + QualityTestRecord result = ledgerService.create(sampleRecord); + + assertEquals("qualified", result.getComplianceStatus()); + assertNull(result.getUnqualifiedItems()); + verify(recordMapper).insert(sampleRecord); + } + + @Test + @DisplayName("2. 创建检测记录-自动判定不合格") + void testCreateRecord_Unqualified() { + sampleRecord.setTurbidity(2.5); // 超标 + sampleRecord.setEcoli(10.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)); + 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("3. 创建检测记录-waterType为空时pending") + void testCreateRecord_PendingWhenNoWaterType() { + sampleRecord.setWaterType(null); + when(recordMapper.insert(any())).thenReturn(1); + + QualityTestRecord result = ledgerService.create(sampleRecord); + + assertEquals("pending", result.getComplianceStatus()); + } + + @Test + @DisplayName("4. 查询检测记录分页") + void testQueryRecords() { + QualityQueryRequest request = new QualityQueryRequest(); + request.setPageNum(1); + request.setPageSize(10); + + List> mockRecords = List.of( + Map.of("id", 1L, "testType", "routine"), + Map.of("id", 2L, "testType", "emergency") + ); + when(recordMapper.selectRecordPage(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq(0), eq(10))) + .thenReturn(mockRecords); + when(recordMapper.countRecords(any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(2L); + + Map result = ledgerService.queryRecords(request); + + assertNotNull(result); + assertEquals(2L, result.get("total")); + assertEquals(1, result.get("pageNum")); + assertEquals(1L, result.get("totalPages")); + } + + @Test + @DisplayName("5. 获取记录详情") + void testGetById() { + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); + + QualityTestRecord result = ledgerService.getById(1L); + + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("routine", result.getTestType()); + } + + @Test + @DisplayName("6. 获取不存在记录抛异常") + void testGetById_NotFound() { + when(recordMapper.selectById(999L)).thenReturn(null); + + assertThrows(Exception.class, () -> ledgerService.getById(999L)); + } + + @Test + @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); + + sampleRecord.setRemark("updated"); + QualityTestRecord result = ledgerService.update(1L, sampleRecord); + + assertNotNull(result); + verify(recordMapper).updateById(any()); + } + + @Test + @DisplayName("8. 删除检测记录") + void testDeleteRecord() { + when(recordMapper.selectById(1L)).thenReturn(sampleRecord); + when(recordMapper.deleteById(1L)).thenReturn(1); + + ledgerService.delete(1L); + + verify(recordMapper).deleteById(1L); + } + + @Test + @DisplayName("9. 批量删除记录") + void testBatchDelete() { + List ids = List.of(1L, 2L, 3L); + when(recordMapper.deleteBatchIds(ids)).thenReturn(3); + + ledgerService.batchDelete(ids); + + verify(recordMapper).deleteBatchIds(ids); + } + + // ==================== 合格判定测试 ==================== + + @Test + @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)); + + ledgerService.evaluateCompliance(sampleRecord); + + assertEquals("unqualified", sampleRecord.getComplianceStatus()); + assertTrue(sampleRecord.getUnqualifiedItems().contains("pH")); + assertTrue(sampleRecord.getUnqualifiedItems().contains("偏低")); + } + + @Test + @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)); + + ledgerService.evaluateCompliance(sampleRecord); + + assertEquals("unqualified", sampleRecord.getComplianceStatus()); + assertTrue(sampleRecord.getUnqualifiedItems().contains("余氯")); + assertTrue(sampleRecord.getUnqualifiedItems().contains("偏高")); + } + + @Test + @DisplayName("12. 合格判定-无标准时不判定") + void testEvaluateCompliance_NoStandard() { + sampleRecord.setWaterType("unknownType"); + when(standardService.getStandard(eq("unknownType"), anyString())).thenReturn(null); + + ledgerService.evaluateCompliance(sampleRecord); + + assertEquals("qualified", sampleRecord.getComplianceStatus()); + } + + // ==================== 统计测试 ==================== + + @Test + @DisplayName("13. 获取统计数据") + void testGetStatistics() { + List> statusStats = List.of( + Map.of("status", "qualified", "count", 80L), + Map.of("status", "unqualified", "count", 15L), + Map.of("status", "pending", "count", 5L) + ); + when(recordMapper.statByComplianceStatus(any(), any())).thenReturn(statusStats); + when(recordMapper.statRateByWaterType(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statRateByArea(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statMonthlyTrend(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statUnqualifiedByParam(any(), any())).thenReturn(Collections.emptyList()); + when(recordMapper.statParamAvg()).thenReturn(Map.of( + "avgTurbidity", 0.45, "avgPh", 7.1, "avgResidualChlorine", 0.6 + )); + + QualityStatVO vo = ledgerService.getStatistics(null, null); + + assertNotNull(vo); + assertEquals(100L, vo.getTotalRecords()); + assertEquals(80L, vo.getQualifiedCount()); + assertEquals(15L, vo.getUnqualifiedCount()); + assertEquals(5L, vo.getPendingCount()); + assertEquals(80.0, vo.getQualifiedRate()); + } + + @Test + @DisplayName("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("15. 获取区域列表") + void testGetAreaList() { + QualityTestRecord r1 = new QualityTestRecord(); + r1.setArea("城区A"); + QualityTestRecord r2 = new QualityTestRecord(); + r2.setArea("城区B"); + when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2)); + + List areas = ledgerService.getAreaList(); + + assertNotNull(areas); + assertEquals(2, areas.size()); + assertTrue(areas.contains("城区A")); + assertTrue(areas.contains("城区B")); + } + + @Test + @DisplayName("16. 获取采样点列表") + void testGetSamplingPointList() { + QualityTestRecord r1 = new QualityTestRecord(); + r1.setSamplingPoint("采样点A"); + QualityTestRecord r2 = new QualityTestRecord(); + r2.setSamplingPoint("采样点B"); + when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2)); + + List points = ledgerService.getSamplingPointList(); + + assertNotNull(points); + assertEquals(2, points.size()); + } + + // ==================== 计划测试 ==================== + + @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); + } + + @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; + } +}