Преглед изворни кода

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
bot_dev2 пре 5 дана
родитељ
комит
8e1b9d8dd2

+ 0
- 1
wm-production/pom.xml Прегледај датотеку

@@ -14,6 +14,5 @@
14 14
         <dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
15 15
         <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId></dependency>
16 16
         <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId></dependency>
17
-        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
18 17
     </dependencies>
19 18
 </project>

+ 67
- 116
wm-production/src/main/java/com/water/production/controller/QualityLedgerController.java Прегледај датотеку

@@ -1,5 +1,6 @@
1 1
 package com.water.production.controller;
2 2
 
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
3 4
 import com.water.common.core.result.R;
4 5
 import com.water.production.dto.QualityQueryRequest;
5 6
 import com.water.production.dto.QualityStatVO;
@@ -11,21 +12,14 @@ import com.water.production.service.QualityStandardService;
11 12
 import com.water.production.service.QualityTestPlanService;
12 13
 import io.swagger.v3.oas.annotations.Operation;
13 14
 import io.swagger.v3.oas.annotations.tags.Tag;
15
+import jakarta.servlet.http.HttpServletResponse;
14 16
 import lombok.RequiredArgsConstructor;
15
-import org.springframework.http.HttpHeaders;
16
-import org.springframework.http.MediaType;
17
-import org.springframework.http.ResponseEntity;
18 17
 import org.springframework.web.bind.annotation.*;
19 18
 
20
-import java.net.URLEncoder;
21
-import java.nio.charset.StandardCharsets;
19
+import java.io.IOException;
22 20
 import java.util.List;
23 21
 import java.util.Map;
24 22
 
25
-/**
26
- * 水质检测台账 Controller
27
- * 提供检测记录 CRUD、标准管理、检测计划、统计分析、数据导出等接口
28
- */
29 23
 @Tag(name = "水质检测台账管理")
30 24
 @RestController
31 25
 @RequestMapping("/api/production/quality")
@@ -36,209 +30,173 @@ public class QualityLedgerController {
36 30
     private final QualityStandardService standardService;
37 31
     private final QualityTestPlanService planService;
38 32
 
39
-    // ==================== 检测记录 CRUD ====================
33
+    // ==================== 记录 CRUD ====================
40 34
 
41
-    // 1. 分页查询台账
42
-    @Operation(summary = "分页查询水质检测台账")
35
+    @Operation(summary = "查询检测记录(分页)")
43 36
     @GetMapping("/records")
44
-    public R<Map<String, Object>> listRecords(QualityQueryRequest request) {
37
+    public R<Map<String, Object>> queryRecords(QualityQueryRequest request) {
45 38
         return R.ok(ledgerService.queryRecords(request));
46 39
     }
47 40
 
48
-    // 2. 获取记录详情
49 41
     @Operation(summary = "获取检测记录详情")
50 42
     @GetMapping("/records/{id}")
51 43
     public R<QualityTestRecord> getRecord(@PathVariable Long id) {
52
-        QualityTestRecord record = ledgerService.getById(id);
53
-        if (record == null) return R.fail(404, "记录不存在");
54
-        return R.ok(record);
44
+        return R.ok(ledgerService.getById(id));
55 45
     }
56 46
 
57
-    // 3. 创建检测记录
58
-    @Operation(summary = "创建水质检测记录 (自动合格判定)")
47
+    @Operation(summary = "创建检测记录")
59 48
     @PostMapping("/records")
60 49
     public R<QualityTestRecord> createRecord(@RequestBody QualityTestRecord record) {
61 50
         return R.ok(ledgerService.create(record));
62 51
     }
63 52
 
64
-    // 4. 更新检测记录
65
-    @Operation(summary = "更新检测记录 (重新合格判定)")
53
+    @Operation(summary = "更新检测记录")
66 54
     @PutMapping("/records/{id}")
67
-    public R<String> updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) {
68
-        record.setId(id);
69
-        ledgerService.update(record);
70
-        return R.ok("更新成功");
55
+    public R<QualityTestRecord> updateRecord(@PathVariable Long id, @RequestBody QualityTestRecord record) {
56
+        return R.ok(ledgerService.update(id, record));
71 57
     }
72 58
 
73
-    // 5. 删除检测记录
74 59
     @Operation(summary = "删除检测记录")
75 60
     @DeleteMapping("/records/{id}")
76
-    public R<String> deleteRecord(@PathVariable Long id) {
61
+    public R<Void> deleteRecord(@PathVariable Long id) {
77 62
         ledgerService.delete(id);
78
-        return R.ok("删除成功");
63
+        return R.ok();
79 64
     }
80 65
 
81
-    // 6. 批量删除
82 66
     @Operation(summary = "批量删除检测记录")
83 67
     @DeleteMapping("/records/batch")
84
-    public R<String> batchDeleteRecords(@RequestBody List<Long> ids) {
68
+    public R<Void> batchDeleteRecords(@RequestBody List<Long> ids) {
85 69
         ledgerService.batchDelete(ids);
86
-        return R.ok("批量删除成功");
70
+        return R.ok();
87 71
     }
88 72
 
89
-    // 7. 重新判定合格状态
90
-    @Operation(summary = "重新判定所有记录合格状态")
73
+    @Operation(summary = "重新判定所有记录")
91 74
     @PostMapping("/records/reevaluate")
92
-    public R<Map<String, Object>> reevaluateRecords() {
75
+    public R<Map<String, Object>> reevaluateAll() {
93 76
         int count = ledgerService.reevaluateAll();
94
-        return R.ok(Map.of("processed", count));
77
+        return R.ok(Map.of("updatedCount", count));
95 78
     }
96 79
 
97
-    // 8. 获取区域列表
98
-    @Operation(summary = "获取所有检测区域")
80
+    // ==================== 辅助 ====================
81
+
82
+    @Operation(summary = "获取区域列表")
99 83
     @GetMapping("/areas")
100 84
     public R<List<String>> getAreas() {
101 85
         return R.ok(ledgerService.getAreaList());
102 86
     }
103 87
 
104
-    // 9. 获取采样点列表
105
-    @Operation(summary = "获取所有采样点")
88
+    @Operation(summary = "获取采样点列表")
106 89
     @GetMapping("/sampling-points")
107 90
     public R<List<String>> getSamplingPoints() {
108 91
         return R.ok(ledgerService.getSamplingPointList());
109 92
     }
110 93
 
111
-    // ==================== 水质标准管理 ====================
94
+    // ==================== 标准 ====================
112 95
 
113
-    // 10. 获取所有启用的标准
114
-    @Operation(summary = "获取启用中的水质标准列表")
96
+    @Operation(summary = "获取启用的标准列表")
115 97
     @GetMapping("/standards")
116 98
     public R<List<QualityStandard>> listStandards() {
117 99
         return R.ok(standardService.listEnabled());
118 100
     }
119 101
 
120
-    // 11. 获取全部标准 (含停用)
121
-    @Operation(summary = "获取所有水质标准 (含停用)")
102
+    @Operation(summary = "获取全部标准")
122 103
     @GetMapping("/standards/all")
123 104
     public R<List<QualityStandard>> listAllStandards() {
124 105
         return R.ok(standardService.listAll());
125 106
     }
126 107
 
127
-    // 12. 按水样类型获取标准
128
-    @Operation(summary = "按水样类型获取水质标准")
108
+    @Operation(summary = "按水质类型获取标准")
129 109
     @GetMapping("/standards/water-type/{waterType}")
130 110
     public R<List<QualityStandard>> listStandardsByWaterType(@PathVariable String waterType) {
131 111
         return R.ok(standardService.listByWaterType(waterType));
132 112
     }
133 113
 
134
-    // 13. 获取标准详情
135
-    @Operation(summary = "获取水质标准详情")
114
+    @Operation(summary = "获取标准详情")
136 115
     @GetMapping("/standards/{id}")
137 116
     public R<QualityStandard> getStandard(@PathVariable Long id) {
138
-        QualityStandard standard = standardService.getById(id);
139
-        if (standard == null) return R.fail(404, "标准不存在");
140
-        return R.ok(standard);
117
+        return R.ok(standardService.getById(id));
141 118
     }
142 119
 
143
-    // 14. 创建标准
144
-    @Operation(summary = "创建水质标准")
120
+    @Operation(summary = "创建标准")
145 121
     @PostMapping("/standards")
146 122
     public R<QualityStandard> createStandard(@RequestBody QualityStandard standard) {
147 123
         return R.ok(standardService.create(standard));
148 124
     }
149 125
 
150
-    // 15. 更新标准
151
-    @Operation(summary = "更新水质标准")
126
+    @Operation(summary = "更新标准")
152 127
     @PutMapping("/standards/{id}")
153
-    public R<String> updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) {
154
-        standard.setId(id);
155
-        standardService.update(standard);
156
-        return R.ok("更新成功");
128
+    public R<QualityStandard> updateStandard(@PathVariable Long id, @RequestBody QualityStandard standard) {
129
+        return R.ok(standardService.update(id, standard));
157 130
     }
158 131
 
159
-    // 16. 删除标准
160
-    @Operation(summary = "删除水质标准")
132
+    @Operation(summary = "删除标准")
161 133
     @DeleteMapping("/standards/{id}")
162
-    public R<String> deleteStandard(@PathVariable Long id) {
134
+    public R<Void> deleteStandard(@PathVariable Long id) {
163 135
         standardService.delete(id);
164
-        return R.ok("删除成功");
136
+        return R.ok();
165 137
     }
166 138
 
167
-    // ==================== 检测计划管理 ====================
139
+    // ==================== 计划 ====================
168 140
 
169
-    // 17. 分页查询检测计划
170
-    @Operation(summary = "分页查询检测计划")
141
+    @Operation(summary = "查询检测计划(分页)")
171 142
     @GetMapping("/plans")
172
-    public R<Map<String, Object>> listPlans(
173
-            @RequestParam(required = false) String status,
174
-            @RequestParam(required = false) String frequency,
175
-            @RequestParam(required = false) String waterType,
176
-            @RequestParam(required = false) String keyword,
143
+    public R<Page<QualityTestPlan>> queryPlans(
177 144
             @RequestParam(defaultValue = "1") int pageNum,
178
-            @RequestParam(defaultValue = "20") int pageSize) {
179
-        return R.ok(planService.queryPlans(status, frequency, waterType, keyword, pageNum, pageSize));
145
+            @RequestParam(defaultValue = "10") int pageSize,
146
+            @RequestParam(required = false) String testType,
147
+            @RequestParam(required = false) String waterType,
148
+            @RequestParam(required = false) String area,
149
+            @RequestParam(required = false) String status,
150
+            @RequestParam(required = false) String keyword) {
151
+        return R.ok(planService.queryPlans(pageNum, pageSize, testType, waterType, area, status, keyword));
180 152
     }
181 153
 
182
-    // 18. 获取计划详情
183
-    @Operation(summary = "获取检测计划详情")
154
+    @Operation(summary = "获取计划详情")
184 155
     @GetMapping("/plans/{id}")
185 156
     public R<QualityTestPlan> getPlan(@PathVariable Long id) {
186
-        QualityTestPlan plan = planService.getById(id);
187
-        if (plan == null) return R.fail(404, "计划不存在");
188
-        return R.ok(plan);
157
+        return R.ok(planService.getById(id));
189 158
     }
190 159
 
191
-    // 19. 创建检测计划
192
-    @Operation(summary = "创建检测计划")
160
+    @Operation(summary = "创建计划")
193 161
     @PostMapping("/plans")
194 162
     public R<QualityTestPlan> createPlan(@RequestBody QualityTestPlan plan) {
195 163
         return R.ok(planService.create(plan));
196 164
     }
197 165
 
198
-    // 20. 更新检测计划
199
-    @Operation(summary = "更新检测计划")
166
+    @Operation(summary = "更新计划")
200 167
     @PutMapping("/plans/{id}")
201
-    public R<String> updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) {
202
-        plan.setId(id);
203
-        planService.update(plan);
204
-        return R.ok("更新成功");
168
+    public R<QualityTestPlan> updatePlan(@PathVariable Long id, @RequestBody QualityTestPlan plan) {
169
+        return R.ok(planService.update(id, plan));
205 170
     }
206 171
 
207
-    // 21. 删除检测计划
208
-    @Operation(summary = "删除检测计划")
172
+    @Operation(summary = "删除计划")
209 173
     @DeleteMapping("/plans/{id}")
210
-    public R<String> deletePlan(@PathVariable Long id) {
174
+    public R<Void> deletePlan(@PathVariable Long id) {
211 175
         planService.delete(id);
212
-        return R.ok("删除成功");
176
+        return R.ok();
213 177
     }
214 178
 
215
-    // 22. 暂停/恢复计划
216
-    @Operation(summary = "切换检测计划状态 (active/paused/completed)")
179
+    @Operation(summary = "切换计划状态")
217 180
     @PutMapping("/plans/{id}/status")
218
-    public R<String> togglePlanStatus(@PathVariable Long id, @RequestParam String status) {
219
-        planService.toggleStatus(id, status);
220
-        return R.ok("状态已更新");
181
+    public R<QualityTestPlan> togglePlanStatus(@PathVariable Long id) {
182
+        return R.ok(planService.toggleStatus(id));
221 183
     }
222 184
 
223
-    // 23. 获取到期计划
224
-    @Operation(summary = "获取当前到期的检测计划")
185
+    @Operation(summary = "获取到期计划")
225 186
     @GetMapping("/plans/due")
226 187
     public R<List<QualityTestPlan>> getDuePlans() {
227 188
         return R.ok(planService.getDuePlans());
228 189
     }
229 190
 
230
-    // 24. 标记计划已执行
231
-    @Operation(summary = "标记检测计划已执行 (更新下次检测日期)")
191
+    @Operation(summary = "标记计划已执行")
232 192
     @PostMapping("/plans/{id}/execute")
233
-    public R<String> markPlanExecuted(@PathVariable Long id) {
234
-        planService.markExecuted(id);
235
-        return R.ok("已标记执行");
193
+    public R<QualityTestPlan> markPlanExecuted(@PathVariable Long id) {
194
+        return R.ok(planService.markExecuted(id));
236 195
     }
237 196
 
238
-    // ==================== 统计分析 ====================
197
+    // ==================== 统计 ====================
239 198
 
240
-    // 25. 综合统计
241
-    @Operation(summary = "水质检测统计分析 (合格率/趋势/指标分布)")
199
+    @Operation(summary = "获取统计数据")
242 200
     @GetMapping("/statistics")
243 201
     public R<QualityStatVO> getStatistics(
244 202
             @RequestParam(required = false) String startDate,
@@ -246,18 +204,11 @@ public class QualityLedgerController {
246 204
         return R.ok(ledgerService.getStatistics(startDate, endDate));
247 205
     }
248 206
 
249
-    // ==================== 数据导出 ====================
207
+    // ==================== 导出 ====================
250 208
 
251
-    // 26. 导出 Excel
252
-    @Operation(summary = "导出质检台账 Excel")
209
+    @Operation(summary = "导出Excel")
253 210
     @PostMapping("/export/excel")
254
-    public ResponseEntity<byte[]> exportExcel(@RequestBody QualityQueryRequest request) {
255
-        byte[] data = ledgerService.exportExcel(request);
256
-        String filename = URLEncoder.encode("水质检测台账.xlsx", StandardCharsets.UTF_8);
257
-        return ResponseEntity.ok()
258
-                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
259
-                .contentType(MediaType.parseMediaType(
260
-                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
261
-                .body(data);
211
+    public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException {
212
+        ledgerService.exportExcel(request, response);
262 213
     }
263 214
 }

+ 14
- 14
wm-production/src/main/java/com/water/production/dto/QualityQueryRequest.java Прегледај датотеку

@@ -3,47 +3,47 @@ package com.water.production.dto;
3 3
 import lombok.Data;
4 4
 
5 5
 /**
6
- * 水质检测台账查询请求
6
+ * 水质检测查询请求
7 7
  */
8 8
 @Data
9 9
 public class QualityQueryRequest {
10 10
 
11
-    /** 检测类型: routine/special/complaint */
11
+    /** 检测类型 */
12 12
     private String testType;
13 13
 
14
-    /** 水样类型: raw/treated/network */
14
+    /** 水质类型 */
15 15
     private String waterType;
16 16
 
17
-    /** 所属区域 */
17
+    /** 区域 */
18 18
     private String area;
19 19
 
20
-    /** 采样点 (模糊搜索) */
20
+    /** 采样点 */
21 21
     private String samplingPoint;
22 22
 
23
-    /** 检测人 (模糊搜索) */
23
+    /** 检测人 */
24 24
     private String tester;
25 25
 
26
-    /** 合格状态: qualified/unqualified/pending */
26
+    /** 合格状态 */
27 27
     private String complianceStatus;
28 28
 
29
-    /** 开始日期 (yyyy-MM-dd) */
29
+    /** 开始日期 */
30 30
     private String startDate;
31 31
 
32
-    /** 结束日期 (yyyy-MM-dd) */
32
+    /** 结束日期 */
33 33
     private String endDate;
34 34
 
35
-    /** 关键词搜索 */
35
+    /** 关键 */
36 36
     private String keyword;
37 37
 
38 38
     /** 排序字段 */
39 39
     private String sortField;
40 40
 
41
-    /** 排序方向: asc/desc */
41
+    /** 排序方向 */
42 42
     private String sortOrder;
43 43
 
44
-    /** 页码 (默认1) */
44
+    /** 页码 */
45 45
     private Integer pageNum = 1;
46 46
 
47
-    /** 每页条数 (默认20) */
48
-    private Integer pageSize = 20;
47
+    /** 每页大小 */
48
+    private Integer pageSize = 10;
49 49
 }

+ 24
- 16
wm-production/src/main/java/com/water/production/dto/QualityStatVO.java Прегледај датотеку

@@ -2,18 +2,17 @@ package com.water.production.dto;
2 2
 
3 3
 import lombok.Data;
4 4
 
5
-import java.math.BigDecimal;
6 5
 import java.util.List;
7 6
 import java.util.Map;
8 7
 
9 8
 /**
10
- * 水质检测统计 VO
9
+ * 水质检测统计VO
11 10
  */
12 11
 @Data
13 12
 public class QualityStatVO {
14 13
 
15
-    /** 总检测记录数 */
16
-    private Long totalCount;
14
+    /** 总记录数 */
15
+    private Long totalRecords;
17 16
 
18 17
     /** 合格数 */
19 18
     private Long qualifiedCount;
@@ -24,21 +23,30 @@ public class QualityStatVO {
24 23
     /** 待判定数 */
25 24
     private Long pendingCount;
26 25
 
27
-    /** 综合合格率 (%) */
28
-    private BigDecimal qualifiedRate;
26
+    /** 合格率 */
27
+    private Double qualifiedRate;
29 28
 
30
-    /** 各水样类型合格率 */
31
-    private Map<String, BigDecimal> rateByWaterType;
29
+    /** 按水质类型统计 */
30
+    private List<Map<String, Object>> byWaterType;
32 31
 
33
-    /** 各区域合格率 */
34
-    private Map<String, BigDecimal> rateByArea;
32
+    /** 按区域统计 */
33
+    private List<Map<String, Object>> byArea;
35 34
 
36
-    /** 各指标不合格次数 */
37
-    private Map<String, Long> unqualifiedByParam;
35
+    /** 按检测类型统计 */
36
+    private List<Map<String, Object>> byTestType;
38 37
 
39
-    /** 月度合格率趋势 [{month, rate}] */
40
-    private List<Map<String, Object>> monthlyTrend;
38
+    /** 按日期趋势 */
39
+    private List<Map<String, Object>> trendByDate;
41 40
 
42
-    /** 各指标均值统计 */
43
-    private Map<String, Map<String, Object>> paramAvgStats;
41
+    /** 不合格项目统计 */
42
+    private List<Map<String, Object>> unqualifiedItems;
43
+
44
+    /** 平均浊度 */
45
+    private Double avgTurbidity;
46
+
47
+    /** 平均pH */
48
+    private Double avgPh;
49
+
50
+    /** 平均余氯 */
51
+    private Double avgResidualChlorine;
44 52
 }

+ 10
- 14
wm-production/src/main/java/com/water/production/entity/QualityStandard.java Прегледај датотеку

@@ -3,11 +3,10 @@ package com.water.production.entity;
3 3
 import com.baomidou.mybatisplus.annotation.*;
4 4
 import lombok.Data;
5 5
 
6
-import java.math.BigDecimal;
7 6
 import java.time.LocalDateTime;
8 7
 
9 8
 /**
10
- * 水质标准实体 (基于 GB5749-2022)
9
+ * 水质标准
11 10
  */
12 11
 @Data
13 12
 @TableName("prod_quality_standard")
@@ -19,39 +18,36 @@ public class QualityStandard {
19 18
     /** 标准名称 */
20 19
     private String standardName;
21 20
 
22
-    /** 标准编码 (如 GB5749-2022) */
21
+    /** 标准编 */
23 22
     private String standardCode;
24 23
 
25
-    /** 参数名称: turbidity/ph/residual_chlorine/color/odor/ecoli/colony_count */
24
+    /** 参数名 */
26 25
     private String paramName;
27 26
 
28
-    /** 参数显示名称 */
27
+    /** 参数标签 */
29 28
     private String paramLabel;
30 29
 
31 30
     /** 参数单位 */
32 31
     private String paramUnit;
33 32
 
34
-    /** 最小值 (null 表示无下限) */
35
-    private BigDecimal minValue;
33
+    /** 最小值 */
34
+    private Double minValue;
36 35
 
37
-    /** 最大值 (null 表示无上限) */
38
-    private BigDecimal maxValue;
36
+    /** 最大值 */
37
+    private Double maxValue;
39 38
 
40
-    /** 适用水样类型: raw/treated/network/all */
39
+    /** 水质类型 */
41 40
     private String waterType;
42 41
 
43 42
     /** 是否启用 */
44
-    private Integer enabled;
43
+    private Boolean enabled;
45 44
 
46
-    /** 逻辑删除 */
47 45
     @TableLogic
48 46
     private Integer deleted;
49 47
 
50
-    /** 创建时间 */
51 48
     @TableField(fill = FieldFill.INSERT)
52 49
     private LocalDateTime createdAt;
53 50
 
54
-    /** 更新时间 */
55 51
     @TableField(fill = FieldFill.INSERT_UPDATE)
56 52
     private LocalDateTime updatedAt;
57 53
 }

+ 9
- 12
wm-production/src/main/java/com/water/production/entity/QualityTestPlan.java Прегледај датотеку

@@ -7,7 +7,7 @@ import java.time.LocalDate;
7 7
 import java.time.LocalDateTime;
8 8
 
9 9
 /**
10
- * 水质检测计划实体
10
+ * 水质检测计划
11 11
  */
12 12
 @Data
13 13
 @TableName("prod_quality_test_plan")
@@ -19,34 +19,34 @@ public class QualityTestPlan {
19 19
     /** 计划名称 */
20 20
     private String planName;
21 21
 
22
-    /** 检测类型: routine/special */
22
+    /** 检测类型 */
23 23
     private String testType;
24 24
 
25
-    /** 水样类型: raw/treated/network */
25
+    /** 水质类型 */
26 26
     private String waterType;
27 27
 
28 28
     /** 采样点 */
29 29
     private String samplingPoint;
30 30
 
31
-    /** 所属区域 */
31
+    /** 区域 */
32 32
     private String area;
33 33
 
34
-    /** 检测频率: daily/weekly/monthly */
34
+    /** 频率: daily/weekly/monthly */
35 35
     private String frequency;
36 36
 
37
-    /** 检测参数 (逗号分隔: turbidity,ph,residual_chlorine) */
37
+    /** 检测参数 (JSON数组) */
38 38
     private String testParams;
39 39
 
40
-    /** 计划开始日期 */
40
+    /** 开始日期 */
41 41
     private LocalDate startDate;
42 42
 
43
-    /** 计划结束日期 (null=长期) */
43
+    /** 结束日期 */
44 44
     private LocalDate endDate;
45 45
 
46 46
     /** 下次检测日期 */
47 47
     private LocalDate nextTestDate;
48 48
 
49
-    /** 计划状态: active/paused/completed */
49
+    /** 状态: active/paused/expired */
50 50
     private String status;
51 51
 
52 52
     /** 执行次数 */
@@ -55,15 +55,12 @@ public class QualityTestPlan {
55 55
     /** 备注 */
56 56
     private String remark;
57 57
 
58
-    /** 逻辑删除 */
59 58
     @TableLogic
60 59
     private Integer deleted;
61 60
 
62
-    /** 创建时间 */
63 61
     @TableField(fill = FieldFill.INSERT)
64 62
     private LocalDateTime createdAt;
65 63
 
66
-    /** 更新时间 */
67 64
     @TableField(fill = FieldFill.INSERT_UPDATE)
68 65
     private LocalDateTime updatedAt;
69 66
 }

+ 49
- 13
wm-production/src/main/java/com/water/production/entity/QualityTestRecord.java Прегледај датотеку

@@ -2,11 +2,14 @@ package com.water.production.entity;
2 2
 
3 3
 import com.baomidou.mybatisplus.annotation.*;
4 4
 import lombok.Data;
5
-import java.math.BigDecimal;
5
+
6 6
 import java.time.LocalDate;
7 7
 import java.time.LocalDateTime;
8 8
 import java.time.LocalTime;
9 9
 
10
+/**
11
+ * 水质检测记录
12
+ */
10 13
 @Data
11 14
 @TableName("prod_quality_test_record")
12 15
 public class QualityTestRecord {
@@ -14,30 +17,63 @@ public class QualityTestRecord {
14 17
     @TableId(type = IdType.AUTO)
15 18
     private Long id;
16 19
 
17
-    private String testType;       // routine/special/complaint
18
-    private String waterType;      // raw/treated/network
20
+    /** 检测类型: routine/emergency/special */
21
+    private String testType;
22
+
23
+    /** 水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater */
24
+    private String waterType;
25
+
26
+    /** 采样点 */
19 27
     private String samplingPoint;
28
+
29
+    /** 所属区域 */
20 30
     private String area;
31
+
32
+    /** 检测日期 */
21 33
     private LocalDate testDate;
34
+
35
+    /** 检测时间 */
22 36
     private LocalTime testTime;
37
+
38
+    /** 检测人 */
23 39
     private String tester;
24
-    private BigDecimal turbidity;
25
-    private BigDecimal ph;
26
-    private BigDecimal residualChlorine;
27
-    private BigDecimal color;
28
-    private BigDecimal odor;
29
-    private BigDecimal ecoli;
30
-    private BigDecimal colonyCount;
31
-    private String complianceStatus; // qualified/unqualified/pending
40
+
41
+    /** 浊度 (NTU) */
42
+    private Double turbidity;
43
+
44
+    /** pH值 */
45
+    private Double ph;
46
+
47
+    /** 余氯 (mg/L) */
48
+    private Double residualChlorine;
49
+
50
+    /** 色度 (度) */
51
+    private Double color;
52
+
53
+    /** 嗅味 */
54
+    private String odor;
55
+
56
+    /** 大肠杆菌 (CFU/100mL) */
57
+    private Double ecoli;
58
+
59
+    /** 菌落总数 (CFU/mL) */
60
+    private Double colonyCount;
61
+
62
+    /** 合格状态: qualified/unqualified/pending */
63
+    private String complianceStatus;
64
+
65
+    /** 不合格项目 (JSON) */
32 66
     private String unqualifiedItems;
67
+
68
+    /** 备注 */
33 69
     private String remark;
34 70
 
35 71
     @TableLogic
36 72
     private Integer deleted;
37 73
 
38
-    @TableField("created_at")
74
+    @TableField(fill = FieldFill.INSERT)
39 75
     private LocalDateTime createdAt;
40 76
 
41
-    @TableField("updated_at")
77
+    @TableField(fill = FieldFill.INSERT_UPDATE)
42 78
     private LocalDateTime updatedAt;
43 79
 }

+ 54
- 48
wm-production/src/main/java/com/water/production/mapper/QualityTestRecordMapper.java Прегледај датотеку

@@ -11,52 +11,58 @@ import java.util.Map;
11 11
 @Mapper
12 12
 public interface QualityTestRecordMapper extends BaseMapper<QualityTestRecord> {
13 13
 
14
-    /** 分页查询台账 (支持多维度筛选) */
15
-    List<Map<String, Object>> selectRecordPage(@Param("testType") String testType,
16
-                                                @Param("waterType") String waterType,
17
-                                                @Param("area") String area,
18
-                                                @Param("samplingPoint") String samplingPoint,
19
-                                                @Param("tester") String tester,
20
-                                                @Param("complianceStatus") String complianceStatus,
21
-                                                @Param("startDate") String startDate,
22
-                                                @Param("endDate") String endDate,
23
-                                                @Param("keyword") String keyword,
24
-                                                @Param("sortField") String sortField,
25
-                                                @Param("sortOrder") String sortOrder,
26
-                                                @Param("offset") int offset,
27
-                                                @Param("limit") int limit);
28
-
29
-    /** 统计符合条件的总数 */
30
-    Long countRecords(@Param("testType") String testType,
31
-                      @Param("waterType") String waterType,
32
-                      @Param("area") String area,
33
-                      @Param("samplingPoint") String samplingPoint,
34
-                      @Param("tester") String tester,
35
-                      @Param("complianceStatus") String complianceStatus,
36
-                      @Param("startDate") String startDate,
37
-                      @Param("endDate") String endDate,
38
-                      @Param("keyword") String keyword);
39
-
40
-    /** 按合格状态统计数量 */
41
-    List<Map<String, Object>> statByComplianceStatus(@Param("startDate") String startDate,
42
-                                                      @Param("endDate") String endDate);
43
-
44
-    /** 按水样类型统计合格率 */
45
-    List<Map<String, Object>> statRateByWaterType(@Param("startDate") String startDate,
46
-                                                   @Param("endDate") String endDate);
47
-
48
-    /** 按区域统计合格率 */
49
-    List<Map<String, Object>> statRateByArea(@Param("startDate") String startDate,
50
-                                              @Param("endDate") String endDate);
51
-
52
-    /** 按指标统计不合格次数 */
53
-    List<Map<String, Object>> statUnqualifiedByParam(@Param("startDate") String startDate,
54
-                                                      @Param("endDate") String endDate);
55
-
56
-    /** 月度合格率趋势 */
57
-    List<Map<String, Object>> statMonthlyTrend(@Param("startDate") String startDate,
58
-                                                @Param("endDate") String endDate);
59
-
60
-    /** 各指标均值统计 */
61
-    List<Map<String, Object>> statParamAvg();
14
+    List<Map<String, Object>> selectRecordPage(
15
+        @Param("testType") String testType,
16
+        @Param("waterType") String waterType,
17
+        @Param("area") String area,
18
+        @Param("samplingPoint") String samplingPoint,
19
+        @Param("tester") String tester,
20
+        @Param("complianceStatus") String complianceStatus,
21
+        @Param("startDate") String startDate,
22
+        @Param("endDate") String endDate,
23
+        @Param("keyword") String keyword,
24
+        @Param("sortField") String sortField,
25
+        @Param("sortOrder") String sortOrder,
26
+        @Param("offset") int offset,
27
+        @Param("limit") int limit
28
+    );
29
+
30
+    Long countRecords(
31
+        @Param("testType") String testType,
32
+        @Param("waterType") String waterType,
33
+        @Param("area") String area,
34
+        @Param("samplingPoint") String samplingPoint,
35
+        @Param("tester") String tester,
36
+        @Param("complianceStatus") String complianceStatus,
37
+        @Param("startDate") String startDate,
38
+        @Param("endDate") String endDate,
39
+        @Param("keyword") String keyword
40
+    );
41
+
42
+    List<Map<String, Object>> statByComplianceStatus(
43
+        @Param("startDate") String startDate,
44
+        @Param("endDate") String endDate
45
+    );
46
+
47
+    List<Map<String, Object>> statRateByWaterType(
48
+        @Param("startDate") String startDate,
49
+        @Param("endDate") String endDate
50
+    );
51
+
52
+    List<Map<String, Object>> statRateByArea(
53
+        @Param("startDate") String startDate,
54
+        @Param("endDate") String endDate
55
+    );
56
+
57
+    List<Map<String, Object>> statUnqualifiedByParam(
58
+        @Param("startDate") String startDate,
59
+        @Param("endDate") String endDate
60
+    );
61
+
62
+    List<Map<String, Object>> statMonthlyTrend(
63
+        @Param("startDate") String startDate,
64
+        @Param("endDate") String endDate
65
+    );
66
+
67
+    Map<String, Object> statParamAvg();
62 68
 }

+ 164
- 268
wm-production/src/main/java/com/water/production/service/QualityLedgerService.java Прегледај датотеку

@@ -2,26 +2,25 @@ package com.water.production.service;
2 2
 
3 3
 import com.alibaba.excel.EasyExcel;
4 4
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
5
+import com.water.common.core.exception.BusinessException;
5 6
 import com.water.production.dto.QualityQueryRequest;
6 7
 import com.water.production.dto.QualityStatVO;
7 8
 import com.water.production.entity.QualityStandard;
8 9
 import com.water.production.entity.QualityTestRecord;
9 10
 import com.water.production.mapper.QualityTestRecordMapper;
11
+import jakarta.servlet.http.HttpServletResponse;
10 12
 import lombok.RequiredArgsConstructor;
11 13
 import lombok.extern.slf4j.Slf4j;
12 14
 import org.springframework.stereotype.Service;
15
+import org.springframework.transaction.annotation.Transactional;
13 16
 
14
-import java.io.ByteArrayOutputStream;
15 17
 import java.io.IOException;
16
-import java.math.BigDecimal;
17
-import java.math.RoundingMode;
18
+import java.net.URLEncoder;
19
+import java.nio.charset.StandardCharsets;
20
+import java.time.LocalDate;
18 21
 import java.util.*;
19 22
 import java.util.stream.Collectors;
20 23
 
21
-/**
22
- * 水质检测台账管理服务
23
- * 包含:CRUD、合格判定、多维度查询、统计分析、数据导出
24
- */
25 24
 @Slf4j
26 25
 @Service
27 26
 @RequiredArgsConstructor
@@ -30,10 +29,8 @@ public class QualityLedgerService {
30 29
     private final QualityTestRecordMapper recordMapper;
31 30
     private final QualityStandardService standardService;
32 31
 
33
-    // ========== CRUD ==========
34
-
35 32
     /**
36
-     * 分页查询台账
33
+     * 分页查询检测记录
37 34
      */
38 35
     public Map<String, Object> queryRecords(QualityQueryRequest request) {
39 36
         int offset = (request.getPageNum() - 1) * request.getPageSize();
@@ -41,8 +38,7 @@ public class QualityLedgerService {
41 38
                 request.getTestType(), request.getWaterType(), request.getArea(),
42 39
                 request.getSamplingPoint(), request.getTester(), request.getComplianceStatus(),
43 40
                 request.getStartDate(), request.getEndDate(), request.getKeyword(),
44
-                request.getSortField(), request.getSortOrder(),
45
-                offset, request.getPageSize()
41
+                request.getSortField(), request.getSortOrder(), offset, request.getPageSize()
46 42
         );
47 43
         Long total = recordMapper.countRecords(
48 44
                 request.getTestType(), request.getWaterType(), request.getArea(),
@@ -50,366 +46,266 @@ public class QualityLedgerService {
50 46
                 request.getStartDate(), request.getEndDate(), request.getKeyword()
51 47
         );
52 48
 
53
-        int pages = (int) Math.ceil((double) total / request.getPageSize());
54
-        return Map.of(
55
-                "records", records,
56
-                "total", total,
57
-                "pageNum", request.getPageNum(),
58
-                "pageSize", request.getPageSize(),
59
-                "pages", pages
60
-        );
49
+        Map<String, Object> result = new LinkedHashMap<>();
50
+        result.put("records", records);
51
+        result.put("total", total);
52
+        result.put("pageNum", request.getPageNum());
53
+        result.put("pageSize", request.getPageSize());
54
+        result.put("totalPages", (total + request.getPageSize() - 1) / request.getPageSize());
55
+        return result;
61 56
     }
62 57
 
63 58
     /**
64 59
      * 获取记录详情
65 60
      */
66 61
     public QualityTestRecord getById(Long id) {
67
-        return recordMapper.selectById(id);
62
+        QualityTestRecord record = recordMapper.selectById(id);
63
+        if (record == null) {
64
+            throw new BusinessException("检测记录不存在");
65
+        }
66
+        return record;
68 67
     }
69 68
 
70 69
     /**
71
-     * 创建检测记录 (含自动合格判定)
70
+     * 创建检测记录(自动合格判定)
72 71
      */
72
+    @Transactional
73 73
     public QualityTestRecord create(QualityTestRecord record) {
74
-        record.setDeleted(0);
75
-        // 自动合格判定
74
+        if (record.getTestDate() == null) record.setTestDate(LocalDate.now());
76 75
         evaluateCompliance(record);
77 76
         recordMapper.insert(record);
77
+        log.info("创建水质检测记录: {}/{}", record.getWaterType(), record.getSamplingPoint());
78 78
         return record;
79 79
     }
80 80
 
81 81
     /**
82
-     * 更新检测记录 (重新判定)
82
+     * 更新检测记录
83 83
      */
84
-    public void update(QualityTestRecord record) {
84
+    @Transactional
85
+    public QualityTestRecord update(Long id, QualityTestRecord record) {
86
+        QualityTestRecord existing = getById(id);
87
+        record.setId(existing.getId());
85 88
         evaluateCompliance(record);
86 89
         recordMapper.updateById(record);
90
+        log.info("更新水质检测记录: {}", id);
91
+        return recordMapper.selectById(id);
87 92
     }
88 93
 
89 94
     /**
90
-     * 删除检测记录 (逻辑删除)
95
+     * 删除检测记录
91 96
      */
97
+    @Transactional
92 98
     public void delete(Long id) {
99
+        getById(id);
93 100
         recordMapper.deleteById(id);
101
+        log.info("删除水质检测记录: {}", id);
94 102
     }
95 103
 
96 104
     /**
97 105
      * 批量删除
98 106
      */
107
+    @Transactional
99 108
     public void batchDelete(List<Long> ids) {
109
+        if (ids == null || ids.isEmpty()) return;
100 110
         recordMapper.deleteBatchIds(ids);
111
+        log.info("批量删除水质检测记录: {} 条", ids.size());
101 112
     }
102 113
 
103
-    // ========== 合格判定 ==========
104
-
105 114
     /**
106
-     * 根据 GB5749-2022 自动判定水质是否合格
107
-     * 比对所有检测参数与标准值,任何一项超标即为不合格
115
+     * 对记录执行合格判定
108 116
      */
109 117
     public void evaluateCompliance(QualityTestRecord record) {
110
-        String waterType = record.getWaterType();
111
-        if (waterType == null) waterType = "treated";
118
+        if (record.getWaterType() == null) {
119
+            record.setComplianceStatus("pending");
120
+            return;
121
+        }
112 122
 
113
-        List<String> unqualifiedItems = new ArrayList<>();
123
+        List<String> unqualified = new ArrayList<>();
114 124
 
115
-        checkParam("turbidity", "浊度", record.getTurbidity(), waterType, unqualifiedItems);
116
-        checkParam("ph", "pH", record.getPh(), waterType, unqualifiedItems);
117
-        checkParam("residual_chlorine", "余氯", record.getResidualChlorine(), waterType, unqualifiedItems);
118
-        checkParam("color", "色度", record.getColor(), waterType, unqualifiedItems);
119
-        checkParam("odor", "嗅味", record.getOdor(), waterType, unqualifiedItems);
120
-        checkParam("ecoli", "大肠杆菌", record.getEcoli(), waterType, unqualifiedItems);
121
-        checkParam("colony_count", "菌落总数", record.getColonyCount(), waterType, unqualifiedItems);
125
+        checkParam(record.getWaterType(), "turbidity", record.getTurbidity(), "浊度", unqualified);
126
+        checkParam(record.getWaterType(), "ph", record.getPh(), "pH", unqualified);
127
+        checkParam(record.getWaterType(), "residualChlorine", record.getResidualChlorine(), "余氯", unqualified);
128
+        checkParam(record.getWaterType(), "color", record.getColor(), "色度", unqualified);
129
+        checkParam(record.getWaterType(), "ecoli", record.getEcoli(), "大肠杆菌", unqualified);
130
+        checkParam(record.getWaterType(), "colonyCount", record.getColonyCount(), "菌落总数", unqualified);
122 131
 
123
-        if (unqualifiedItems.isEmpty()) {
132
+        if (unqualified.isEmpty()) {
124 133
             record.setComplianceStatus("qualified");
125 134
             record.setUnqualifiedItems(null);
126 135
         } else {
127 136
             record.setComplianceStatus("unqualified");
128
-            // 构建不合格项JSON
129
-            record.setUnqualifiedItems(buildUnqualifiedJson(unqualifiedItems));
137
+            record.setUnqualifiedItems("[\"" + String.join("\",\"", unqualified) + "\"]");
130 138
         }
131 139
     }
132 140
 
133
-    private void checkParam(String paramName, String paramLabel, BigDecimal value,
134
-                            String waterType, List<String> unqualifiedItems) {
141
+    private void checkParam(String waterType, String paramName, Double value, String label,
142
+                             List<String> unqualified) {
135 143
         if (value == null) return;
136
-
137 144
         QualityStandard standard = standardService.getStandard(waterType, paramName);
138
-        if (standard == null) return; // 无标准则不判定
139
-
140
-        boolean isUnqualified = false;
141
-        if (standard.getMinValue() != null && value.compareTo(standard.getMinValue()) < 0) {
142
-            isUnqualified = true;
145
+        if (standard == null) return;
146
+        if (standard.getMinValue() != null && value < standard.getMinValue()) {
147
+            unqualified.add(label + "(偏低)");
143 148
         }
144
-        if (standard.getMaxValue() != null && value.compareTo(standard.getMaxValue()) > 0) {
145
-            isUnqualified = true;
146
-        }
147
-
148
-        if (isUnqualified) {
149
-            String range = buildRangeDesc(standard);
150
-            unqualifiedItems.add(String.format(
151
-                    "{\"param\":\"%s\",\"label\":\"%s\",\"value\":%s,\"range\":\"%s\",\"unit\":\"%s\"}",
152
-                    paramName, paramLabel, value.toPlainString(), range,
153
-                    standard.getParamUnit() != null ? standard.getParamUnit() : ""
154
-            ));
149
+        if (standard.getMaxValue() != null && value > standard.getMaxValue()) {
150
+            unqualified.add(label + "(偏高)");
155 151
         }
156 152
     }
157 153
 
158
-    private String buildRangeDesc(QualityStandard std) {
159
-        if (std.getMinValue() != null && std.getMaxValue() != null) {
160
-            return std.getMinValue().toPlainString() + "~" + std.getMaxValue().toPlainString();
161
-        } else if (std.getMinValue() != null) {
162
-            return "≥" + std.getMinValue().toPlainString();
163
-        } else if (std.getMaxValue() != null) {
164
-            return "≤" + std.getMaxValue().toPlainString();
165
-        }
166
-        return "无限制";
167
-    }
168
-
169
-    private String buildUnqualifiedJson(List<String> items) {
170
-        return "[" + String.join(",", items) + "]";
171
-    }
172
-
173 154
     /**
174
-     * 手动重新判定所有记录
155
+     * 重新判定所有记录
175 156
      */
157
+    @Transactional
176 158
     public int reevaluateAll() {
177
-        List<QualityTestRecord> records = recordMapper.selectList(
178
-                new LambdaQueryWrapper<QualityTestRecord>().eq(QualityTestRecord::getDeleted, 0)
179
-        );
159
+        LambdaQueryWrapper<QualityTestRecord> wrapper = new LambdaQueryWrapper<>();
160
+        wrapper.in(QualityTestRecord::getComplianceStatus, "pending", "qualified", "unqualified");
161
+        List<QualityTestRecord> records = recordMapper.selectList(wrapper);
180 162
         int count = 0;
181 163
         for (QualityTestRecord record : records) {
164
+            String oldStatus = record.getComplianceStatus();
182 165
             evaluateCompliance(record);
183
-            recordMapper.updateById(record);
184
-            count++;
166
+            if (!oldStatus.equals(record.getComplianceStatus())) {
167
+                recordMapper.updateById(record);
168
+                count++;
169
+            }
185 170
         }
171
+        log.info("重新判定完成,共 {} 条记录状态变更", count);
186 172
         return count;
187 173
     }
188 174
 
189
-    // ========== 统计分析 ==========
190
-
191 175
     /**
192
-     * 综合统计
176
+     * 获取统计数据
193 177
      */
194 178
     public QualityStatVO getStatistics(String startDate, String endDate) {
195
-        QualityStatVO stat = new QualityStatVO();
179
+        QualityStatVO vo = new QualityStatVO();
196 180
 
197 181
         // 按合格状态统计
198 182
         List<Map<String, Object>> statusStats = recordMapper.statByComplianceStatus(startDate, endDate);
199 183
         long total = 0, qualified = 0, unqualified = 0, pending = 0;
200 184
         for (Map<String, Object> row : statusStats) {
201
-            long count = ((Number) row.get("count")).longValue();
202
-            total += count;
203
-            String status = (String) row.get("status");
204
-            if ("qualified".equals(status)) qualified = count;
205
-            else if ("unqualified".equals(status)) unqualified = count;
206
-            else if ("pending".equals(status)) pending = count;
185
+            String status = String.valueOf(row.get("status"));
186
+            long cnt = ((Number) row.get("count")).longValue();
187
+            total += cnt;
188
+            switch (status) {
189
+                case "qualified" -> qualified = cnt;
190
+                case "unqualified" -> unqualified = cnt;
191
+                case "pending" -> pending = cnt;
192
+            }
207 193
         }
208
-        stat.setTotalCount(total);
209
-        stat.setQualifiedCount(qualified);
210
-        stat.setUnqualifiedCount(unqualified);
211
-        stat.setPendingCount(pending);
212
-        stat.setQualifiedRate(total > 0
213
-                ? BigDecimal.valueOf(qualified * 100.0 / total).setScale(1, RoundingMode.HALF_UP)
214
-                : BigDecimal.ZERO);
215
-
216
-        // 按水样类型合格率
217
-        List<Map<String, Object>> waterTypeStats = recordMapper.statRateByWaterType(startDate, endDate);
218
-        Map<String, BigDecimal> rateByWaterType = new LinkedHashMap<>();
219
-        for (Map<String, Object> row : waterTypeStats) {
220
-            String wt = (String) row.get("waterType");
221
-            long t = ((Number) row.get("total")).longValue();
222
-            long q = ((Number) row.get("qualified")).longValue();
223
-            rateByWaterType.put(wt, t > 0
224
-                    ? BigDecimal.valueOf(q * 100.0 / t).setScale(1, RoundingMode.HALF_UP)
225
-                    : BigDecimal.ZERO);
226
-        }
227
-        stat.setRateByWaterType(rateByWaterType);
228
-
229
-        // 按区域合格率
230
-        List<Map<String, Object>> areaStats = recordMapper.statRateByArea(startDate, endDate);
231
-        Map<String, BigDecimal> rateByArea = new LinkedHashMap<>();
232
-        for (Map<String, Object> row : areaStats) {
233
-            String area = (String) row.get("area");
234
-            long t = ((Number) row.get("total")).longValue();
235
-            long q = ((Number) row.get("qualified")).longValue();
236
-            rateByArea.put(area, t > 0
237
-                    ? BigDecimal.valueOf(q * 100.0 / t).setScale(1, RoundingMode.HALF_UP)
238
-                    : BigDecimal.ZERO);
239
-        }
240
-        stat.setRateByArea(rateByArea);
194
+        vo.setTotalRecords(total);
195
+        vo.setQualifiedCount(qualified);
196
+        vo.setUnqualifiedCount(unqualified);
197
+        vo.setPendingCount(pending);
198
+        vo.setQualifiedRate(total > 0 ? Math.round(qualified * 10000.0 / total) / 100.0 : 0.0);
241 199
 
242
-        // 不合格项统计
243
-        List<Map<String, Object>> unqStats = recordMapper.statUnqualifiedByParam(startDate, endDate);
244
-        Map<String, Long> unqByParam = new LinkedHashMap<>();
245
-        for (Map<String, Object> row : unqStats) {
246
-            unqByParam.put((String) row.get("paramName"), ((Number) row.get("count")).longValue());
247
-        }
248
-        stat.setUnqualifiedByParam(unqByParam);
200
+        // 按水质类型
201
+        vo.setByWaterType(recordMapper.statRateByWaterType(startDate, endDate));
249 202
 
250
-        // 月度趋势
251
-        List<Map<String, Object>> trend = recordMapper.statMonthlyTrend(startDate, endDate);
252
-        stat.setMonthlyTrend(trend);
253
-
254
-        // 各指标均值
255
-        List<Map<String, Object>> avgStats = recordMapper.statParamAvg();
256
-        if (!avgStats.isEmpty()) {
257
-            stat.setParamAvgStats(avgStats.get(0).entrySet().stream()
258
-                    .collect(Collectors.toMap(Map.Entry::getKey,
259
-                            e -> Map.of("avg", e.getValue() != null ? e.getValue() : 0))));
260
-        }
203
+        // 按区域
204
+        vo.setByArea(recordMapper.statRateByArea(startDate, endDate));
261 205
 
262
-        return stat;
263
-    }
206
+        // 按检测类型
207
+        vo.setByTestType(recordMapper.statByComplianceStatus(startDate, endDate));
264 208
 
265
-    // ========== 数据导出 ==========
266
-
267
-    /**
268
-     * 导出 Excel
269
-     */
270
-    public byte[] exportExcel(QualityQueryRequest request) {
271
-        // 获取全部数据 (最多10000条)
272
-        int offset = 0;
273
-        List<Map<String, Object>> records = recordMapper.selectRecordPage(
274
-                request.getTestType(), request.getWaterType(), request.getArea(),
275
-                request.getSamplingPoint(), request.getTester(), request.getComplianceStatus(),
276
-                request.getStartDate(), request.getEndDate(), request.getKeyword(),
277
-                "testDate", "desc", offset, 10000
278
-        );
279
-
280
-        if (records.isEmpty()) return new byte[0];
209
+        // 月度趋势
210
+        vo.setTrendByDate(recordMapper.statMonthlyTrend(startDate, endDate));
281 211
 
282
-        List<List<String>> head = buildExportHead();
283
-        List<List<Object>> data = buildExportData(records);
212
+        // 不合格项
213
+        vo.setUnqualifiedItems(recordMapper.statUnqualifiedByParam(startDate, endDate));
284 214
 
285
-        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
286
-            EasyExcel.write(bos)
287
-                    .sheet("水质检测台账")
288
-                    .head(head)
289
-                    .doWrite(data);
290
-            return bos.toByteArray();
291
-        } catch (IOException e) {
292
-            log.error("Excel 导出失败", e);
293
-            throw new RuntimeException("Excel 导出失败: " + e.getMessage());
215
+        // 参数均值
216
+        Map<String, Object> avgMap = recordMapper.statParamAvg();
217
+        if (avgMap != null) {
218
+            vo.setAvgTurbidity(toDouble(avgMap.get("avgTurbidity")));
219
+            vo.setAvgPh(toDouble(avgMap.get("avgPh")));
220
+            vo.setAvgResidualChlorine(toDouble(avgMap.get("avgResidualChlorine")));
294 221
         }
295
-    }
296 222
 
297
-    private List<List<String>> buildExportHead() {
298
-        List<List<String>> head = new ArrayList<>();
299
-        head.add(List.of("检测日期"));
300
-        head.add(List.of("检测类型"));
301
-        head.add(List.of("水样类型"));
302
-        head.add(List.of("采样点"));
303
-        head.add(List.of("区域"));
304
-        head.add(List.of("检测人"));
305
-        head.add(List.of("浊度(NTU)"));
306
-        head.add(List.of("pH"));
307
-        head.add(List.of("余氯(mg/L)"));
308
-        head.add(List.of("色度(度)"));
309
-        head.add(List.of("嗅味(级)"));
310
-        head.add(List.of("大肠杆菌(CFU/100mL)"));
311
-        head.add(List.of("菌落总数(CFU/mL)"));
312
-        head.add(List.of("合格状态"));
313
-        head.add(List.of("备注"));
314
-        return head;
223
+        return vo;
315 224
     }
316 225
 
317
-    private List<List<Object>> buildExportData(List<Map<String, Object>> records) {
318
-        List<List<Object>> data = new ArrayList<>();
319
-        for (Map<String, Object> r : records) {
320
-            List<Object> row = new ArrayList<>();
321
-            row.add(r.get("testDate"));
322
-            row.add(formatTestType((String) r.get("testType")));
323
-            row.add(formatWaterType((String) r.get("waterType")));
324
-            row.add(r.get("samplingPoint"));
325
-            row.add(r.get("area"));
326
-            row.add(r.get("tester"));
327
-            row.add(r.get("turbidity"));
328
-            row.add(r.get("ph"));
329
-            row.add(r.get("residualChlorine"));
330
-            row.add(r.get("color"));
331
-            row.add(r.get("odor"));
332
-            row.add(r.get("ecoli"));
333
-            row.add(r.get("colonyCount"));
334
-            row.add(formatCompliance((String) r.get("complianceStatus")));
335
-            row.add(r.get("remark"));
336
-            data.add(row);
337
-        }
338
-        return data;
226
+    private Double toDouble(Object val) {
227
+        if (val == null) return null;
228
+        if (val instanceof Number) return ((Number) val).doubleValue();
229
+        return null;
339 230
     }
340 231
 
341
-    private String formatTestType(String type) {
342
-        if (type == null) return "";
343
-        return switch (type) {
344
-            case "routine" -> "常规检测";
345
-            case "special" -> "专项检测";
346
-            case "complaint" -> "投诉检测";
347
-            default -> type;
348
-        };
349
-    }
232
+    /**
233
+     * 导出Excel
234
+     */
235
+    public void exportExcel(QualityQueryRequest request, HttpServletResponse response) throws IOException {
236
+        request.setPageNum(1);
237
+        request.setPageSize(100000);
238
+        Map<String, Object> result = queryRecords(request);
239
+        @SuppressWarnings("unchecked")
240
+        List<Map<String, Object>> records = (List<Map<String, Object>>) result.get("records");
241
+
242
+        List<List<String>> excelData = records.stream().map(r -> {
243
+            List<String> row = new ArrayList<>();
244
+            row.add(String.valueOf(r.getOrDefault("testType", "")));
245
+            row.add(String.valueOf(r.getOrDefault("waterType", "")));
246
+            row.add(String.valueOf(r.getOrDefault("samplingPoint", "")));
247
+            row.add(String.valueOf(r.getOrDefault("area", "")));
248
+            row.add(String.valueOf(r.getOrDefault("testDate", "")));
249
+            row.add(String.valueOf(r.getOrDefault("tester", "")));
250
+            row.add(String.valueOf(r.getOrDefault("turbidity", "")));
251
+            row.add(String.valueOf(r.getOrDefault("ph", "")));
252
+            row.add(String.valueOf(r.getOrDefault("residualChlorine", "")));
253
+            row.add(String.valueOf(r.getOrDefault("color", "")));
254
+            row.add(String.valueOf(r.getOrDefault("odor", "")));
255
+            row.add(String.valueOf(r.getOrDefault("ecoli", "")));
256
+            row.add(String.valueOf(r.getOrDefault("colonyCount", "")));
257
+            row.add(String.valueOf(r.getOrDefault("complianceStatus", "")));
258
+            row.add(String.valueOf(r.getOrDefault("unqualifiedItems", "")));
259
+            row.add(String.valueOf(r.getOrDefault("remark", "")));
260
+            return row;
261
+        }).collect(Collectors.toList());
262
+
263
+        List<List<String>> head = List.of(
264
+                List.of("检测类型"), List.of("水质类型"), List.of("采样点"), List.of("区域"),
265
+                List.of("检测日期"), List.of("检测人"), List.of("浊度"), List.of("pH"),
266
+                List.of("余氯"), List.of("色度"), List.of("嗅味"), List.of("大肠杆菌"),
267
+                List.of("菌落总数"), List.of("合格状态"), List.of("不合格项"), List.of("备注")
268
+        );
350 269
 
351
-    private String formatWaterType(String type) {
352
-        if (type == null) return "";
353
-        return switch (type) {
354
-            case "raw" -> "原水";
355
-            case "treated" -> "出厂水";
356
-            case "network" -> "管网末梢水";
357
-            default -> type;
358
-        };
359
-    }
270
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
271
+        response.setHeader("Content-Disposition",
272
+                "attachment;filename=" + URLEncoder.encode("水质检测报告.xlsx", StandardCharsets.UTF_8));
360 273
 
361
-    private String formatCompliance(String status) {
362
-        if (status == null) return "待判定";
363
-        return switch (status) {
364
-            case "qualified" -> "合格";
365
-            case "unqualified" -> "不合格";
366
-            case "pending" -> "待判定";
367
-            default -> status;
368
-        };
274
+        EasyExcel.write(response.getOutputStream())
275
+                .head(head)
276
+                .sheet("检测记录")
277
+                .doWrite(excelData.stream().map(row -> (List<Object>) (List<?>) row).collect(Collectors.toList()));
369 278
     }
370 279
 
371
-    // ========== 辅助查询 ==========
372
-
373 280
     /**
374
-     * 获取所有区域列表
281
+     * 获取区域列表
375 282
      */
376 283
     public List<String> getAreaList() {
377
-        List<QualityTestRecord> records = recordMapper.selectList(
378
-                new LambdaQueryWrapper<QualityTestRecord>()
379
-                        .select(QualityTestRecord::getArea)
380
-                        .isNotNull(QualityTestRecord::getArea)
381
-                        .groupBy(QualityTestRecord::getArea)
382
-        );
383
-        return records.stream()
284
+        LambdaQueryWrapper<QualityTestRecord> wrapper = new LambdaQueryWrapper<>();
285
+        wrapper.select(QualityTestRecord::getArea)
286
+               .isNotNull(QualityTestRecord::getArea)
287
+               .ne(QualityTestRecord::getArea, "")
288
+               .groupBy(QualityTestRecord::getArea);
289
+        return recordMapper.selectList(wrapper).stream()
384 290
                 .map(QualityTestRecord::getArea)
385
-                .filter(Objects::nonNull)
386 291
                 .distinct()
387 292
                 .sorted()
388 293
                 .collect(Collectors.toList());
389 294
     }
390 295
 
391 296
     /**
392
-     * 获取所有采样点列表
297
+     * 获取采样点列表
393 298
      */
394 299
     public List<String> getSamplingPointList() {
395
-        List<QualityTestRecord> records = recordMapper.selectList(
396
-                new LambdaQueryWrapper<QualityTestRecord>()
397
-                        .select(QualityTestRecord::getSamplingPoint)
398
-                        .isNotNull(QualityTestRecord::getSamplingPoint)
399
-                        .groupBy(QualityTestRecord::getSamplingPoint)
400
-        );
401
-        return records.stream()
300
+        LambdaQueryWrapper<QualityTestRecord> wrapper = new LambdaQueryWrapper<>();
301
+        wrapper.select(QualityTestRecord::getSamplingPoint)
302
+               .isNotNull(QualityTestRecord::getSamplingPoint)
303
+               .ne(QualityTestRecord::getSamplingPoint, "")
304
+               .groupBy(QualityTestRecord::getSamplingPoint);
305
+        return recordMapper.selectList(wrapper).stream()
402 306
                 .map(QualityTestRecord::getSamplingPoint)
403
-                .filter(Objects::nonNull)
404 307
                 .distinct()
405 308
                 .sorted()
406 309
                 .collect(Collectors.toList());
407 310
     }
408
-
409
-    /**
410
-     * 按ID批量查询记录
411
-     */
412
-    public List<QualityTestRecord> listByIds(List<Long> ids) {
413
-        return recordMapper.selectBatchIds(ids);
414
-    }
415 311
 }

+ 52
- 51
wm-production/src/main/java/com/water/production/service/QualityStandardService.java Прегледај датотеку

@@ -1,20 +1,16 @@
1 1
 package com.water.production.service;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.common.core.exception.BusinessException;
4 5
 import com.water.production.entity.QualityStandard;
5 6
 import com.water.production.mapper.QualityStandardMapper;
6 7
 import lombok.RequiredArgsConstructor;
7 8
 import lombok.extern.slf4j.Slf4j;
8 9
 import org.springframework.stereotype.Service;
10
+import org.springframework.transaction.annotation.Transactional;
9 11
 
10
-import java.math.BigDecimal;
11
-import java.util.*;
12
-import java.util.stream.Collectors;
12
+import java.util.List;
13 13
 
14
-/**
15
- * 水质标准管理服务
16
- * 基于 GB5749-2022《生活饮用水卫生标准》
17
- */
18 14
 @Slf4j
19 15
 @Service
20 16
 @RequiredArgsConstructor
@@ -26,79 +22,84 @@ public class QualityStandardService {
26 22
      * 获取所有启用的标准
27 23
      */
28 24
     public List<QualityStandard> listEnabled() {
29
-        return standardMapper.selectList(
30
-                new LambdaQueryWrapper<QualityStandard>()
31
-                        .eq(QualityStandard::getEnabled, 1)
32
-                        .orderByAsc(QualityStandard::getParamName)
33
-        );
25
+        LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
26
+        wrapper.eq(QualityStandard::getEnabled, true)
27
+               .orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName);
28
+        return standardMapper.selectList(wrapper);
34 29
     }
35 30
 
36 31
     /**
37
-     * 按水样类型获取启用的标准
32
+     * 按水质类型获取标准
38 33
      */
39 34
     public List<QualityStandard> listByWaterType(String waterType) {
40
-        return standardMapper.selectList(
41
-                new LambdaQueryWrapper<QualityStandard>()
42
-                        .eq(QualityStandard::getEnabled, 1)
43
-                        .and(w -> w.eq(QualityStandard::getWaterType, waterType)
44
-                                .or().eq(QualityStandard::getWaterType, "all"))
45
-                        .orderByAsc(QualityStandard::getParamName)
46
-        );
35
+        LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
36
+        wrapper.eq(QualityStandard::getEnabled, true)
37
+               .eq(QualityStandard::getWaterType, waterType)
38
+               .orderByAsc(QualityStandard::getParamName);
39
+        return standardMapper.selectList(wrapper);
47 40
     }
48 41
 
49 42
     /**
50
-     * 获取标准详情
43
+     * 获取全部标准(含禁用)
51 44
      */
52
-    public QualityStandard getById(Long id) {
53
-        return standardMapper.selectById(id);
45
+    public List<QualityStandard> listAll() {
46
+        LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
47
+        wrapper.orderByAsc(QualityStandard::getWaterType, QualityStandard::getParamName);
48
+        return standardMapper.selectList(wrapper);
54 49
     }
55 50
 
56 51
     /**
57
-     * 新增标准
52
+     * 获取标准详情
58 53
      */
59
-    public QualityStandard create(QualityStandard standard) {
60
-        standard.setEnabled(1);
61
-        standard.setDeleted(0);
62
-        standardMapper.insert(standard);
54
+    public QualityStandard getById(Long id) {
55
+        QualityStandard standard = standardMapper.selectById(id);
56
+        if (standard == null) {
57
+            throw new BusinessException("标准不存在");
58
+        }
63 59
         return standard;
64 60
     }
65 61
 
66 62
     /**
67
-     * 更新标准
63
+     * 获取指定水质类型和参数名的标准
68 64
      */
69
-    public void update(QualityStandard standard) {
70
-        standardMapper.updateById(standard);
65
+    public QualityStandard getStandard(String waterType, String paramName) {
66
+        LambdaQueryWrapper<QualityStandard> wrapper = new LambdaQueryWrapper<>();
67
+        wrapper.eq(QualityStandard::getWaterType, waterType)
68
+               .eq(QualityStandard::getParamName, paramName)
69
+               .eq(QualityStandard::getEnabled, true)
70
+               .last("LIMIT 1");
71
+        return standardMapper.selectOne(wrapper);
71 72
     }
72 73
 
73 74
     /**
74
-     * 删除标准 (逻辑删除)
75
+     * 创建标准
75 76
      */
76
-    public void delete(Long id) {
77
-        standardMapper.deleteById(id);
77
+    @Transactional
78
+    public QualityStandard create(QualityStandard standard) {
79
+        standardMapper.insert(standard);
80
+        log.info("创建水质标准: {} - {}", standard.getStandardName(), standard.getParamLabel());
81
+        return standard;
78 82
     }
79 83
 
80 84
     /**
81
-     * 获取所有标准 (含停用)
85
+     * 更新标准
82 86
      */
83
-    public List<QualityStandard> listAll() {
84
-        return standardMapper.selectList(
85
-                new LambdaQueryWrapper<QualityStandard>()
86
-                        .orderByAsc(QualityStandard::getStandardCode)
87
-                        .orderByAsc(QualityStandard::getParamName)
88
-        );
87
+    @Transactional
88
+    public QualityStandard update(Long id, QualityStandard standard) {
89
+        QualityStandard existing = getById(id);
90
+        standard.setId(existing.getId());
91
+        standardMapper.updateById(standard);
92
+        log.info("更新水质标准: {}", id);
93
+        return standardMapper.selectById(id);
89 94
     }
90 95
 
91 96
     /**
92
-     * 根据水样类型和参数名获取标准
97
+     * 删除标准
93 98
      */
94
-    public QualityStandard getStandard(String waterType, String paramName) {
95
-        return standardMapper.selectOne(
96
-                new LambdaQueryWrapper<QualityStandard>()
97
-                        .eq(QualityStandard::getEnabled, 1)
98
-                        .eq(QualityStandard::getParamName, paramName)
99
-                        .and(w -> w.eq(QualityStandard::getWaterType, waterType)
100
-                                .or().eq(QualityStandard::getWaterType, "all"))
101
-                        .last("LIMIT 1")
102
-        );
99
+    @Transactional
100
+    public void delete(Long id) {
101
+        getById(id);
102
+        standardMapper.deleteById(id);
103
+        log.info("删除水质标准: {}", id);
103 104
     }
104 105
 }

+ 70
- 73
wm-production/src/main/java/com/water/production/service/QualityTestPlanService.java Прегледај датотеку

@@ -2,19 +2,17 @@ package com.water.production.service;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 4
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.water.common.core.exception.BusinessException;
5 6
 import com.water.production.entity.QualityTestPlan;
6 7
 import com.water.production.mapper.QualityTestPlanMapper;
7 8
 import lombok.RequiredArgsConstructor;
8 9
 import lombok.extern.slf4j.Slf4j;
9 10
 import org.springframework.stereotype.Service;
11
+import org.springframework.transaction.annotation.Transactional;
10 12
 
11 13
 import java.time.LocalDate;
12 14
 import java.util.List;
13
-import java.util.Map;
14 15
 
15
-/**
16
- * 水质检测计划管理服务
17
- */
18 16
 @Slf4j
19 17
 @Service
20 18
 @RequiredArgsConstructor
@@ -23,125 +21,124 @@ public class QualityTestPlanService {
23 21
     private final QualityTestPlanMapper planMapper;
24 22
 
25 23
     /**
26
-     * 分页查询检测计划
24
+     * 分页查询计划
27 25
      */
28
-    public Map<String, Object> queryPlans(String status, String frequency, String waterType,
29
-                                           String keyword, int pageNum, int pageSize) {
26
+    public Page<QualityTestPlan> queryPlans(int pageNum, int pageSize, String testType, String waterType,
27
+                                             String area, String status, String keyword) {
28
+        Page<QualityTestPlan> page = new Page<>(pageNum, pageSize);
30 29
         LambdaQueryWrapper<QualityTestPlan> wrapper = new LambdaQueryWrapper<>();
31
-        if (status != null && !status.isEmpty()) {
32
-            wrapper.eq(QualityTestPlan::getStatus, status);
33
-        }
34
-        if (frequency != null && !frequency.isEmpty()) {
35
-            wrapper.eq(QualityTestPlan::getFrequency, frequency);
36
-        }
37
-        if (waterType != null && !waterType.isEmpty()) {
38
-            wrapper.eq(QualityTestPlan::getWaterType, waterType);
39
-        }
40
-        if (keyword != null && !keyword.isEmpty()) {
30
+        if (testType != null && !testType.isBlank()) wrapper.eq(QualityTestPlan::getTestType, testType);
31
+        if (waterType != null && !waterType.isBlank()) wrapper.eq(QualityTestPlan::getWaterType, waterType);
32
+        if (area != null && !area.isBlank()) wrapper.eq(QualityTestPlan::getArea, area);
33
+        if (status != null && !status.isBlank()) wrapper.eq(QualityTestPlan::getStatus, status);
34
+        if (keyword != null && !keyword.isBlank()) {
41 35
             wrapper.and(w -> w.like(QualityTestPlan::getPlanName, keyword)
42
-                    .or().like(QualityTestPlan::getSamplingPoint, keyword)
43
-                    .or().like(QualityTestPlan::getArea, keyword));
36
+                              .or().like(QualityTestPlan::getSamplingPoint, keyword));
44 37
         }
45 38
         wrapper.orderByDesc(QualityTestPlan::getCreatedAt);
46
-
47
-        Page<QualityTestPlan> page = planMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
48
-
49
-        return Map.of(
50
-                "records", page.getRecords(),
51
-                "total", page.getTotal(),
52
-                "pageNum", pageNum,
53
-                "pageSize", pageSize,
54
-                "pages", page.getPages()
55
-        );
39
+        return planMapper.selectPage(page, wrapper);
56 40
     }
57 41
 
58 42
     /**
59 43
      * 获取计划详情
60 44
      */
61 45
     public QualityTestPlan getById(Long id) {
62
-        return planMapper.selectById(id);
46
+        QualityTestPlan plan = planMapper.selectById(id);
47
+        if (plan == null) {
48
+            throw new BusinessException("检测计划不存在");
49
+        }
50
+        return plan;
63 51
     }
64 52
 
65 53
     /**
66
-     * 创建检测计划
54
+     * 创建计划
67 55
      */
56
+    @Transactional
68 57
     public QualityTestPlan create(QualityTestPlan plan) {
69
-        plan.setDeleted(0);
70
-        if (plan.getStatus() == null) {
71
-            plan.setStatus("active");
72
-        }
73
-        if (plan.getExecutionCount() == null) {
74
-            plan.setExecutionCount(0);
75
-        }
76
-        // 计算下次检测日期
77
-        if (plan.getNextTestDate() == null) {
78
-            plan.setNextTestDate(plan.getStartDate() != null ? plan.getStartDate() : LocalDate.now());
79
-        }
58
+        if (plan.getStatus() == null) plan.setStatus("active");
59
+        if (plan.getExecutionCount() == null) plan.setExecutionCount(0);
60
+        if (plan.getNextTestDate() == null) plan.setNextTestDate(plan.getStartDate());
80 61
         planMapper.insert(plan);
62
+        log.info("创建检测计划: {}", plan.getPlanName());
81 63
         return plan;
82 64
     }
83 65
 
84 66
     /**
85
-     * 更新检测计划
67
+     * 更新计划
86 68
      */
87
-    public void update(QualityTestPlan plan) {
69
+    @Transactional
70
+    public QualityTestPlan update(Long id, QualityTestPlan plan) {
71
+        QualityTestPlan existing = getById(id);
72
+        plan.setId(existing.getId());
88 73
         planMapper.updateById(plan);
74
+        log.info("更新检测计划: {}", id);
75
+        return planMapper.selectById(id);
89 76
     }
90 77
 
91 78
     /**
92
-     * 删除检测计划 (逻辑删除)
79
+     * 删除计划
93 80
      */
81
+    @Transactional
94 82
     public void delete(Long id) {
83
+        getById(id);
95 84
         planMapper.deleteById(id);
85
+        log.info("删除检测计划: {}", id);
96 86
     }
97 87
 
98 88
     /**
99
-     * 暂停/恢复计划
89
+     * 切换计划状态
100 90
      */
101
-    public void toggleStatus(Long id, String status) {
102
-        QualityTestPlan plan = planMapper.selectById(id);
103
-        if (plan == null) {
104
-            throw new IllegalArgumentException("计划不存在: " + id);
91
+    @Transactional
92
+    public QualityTestPlan toggleStatus(Long id) {
93
+        QualityTestPlan plan = getById(id);
94
+        if ("active".equals(plan.getStatus())) {
95
+            plan.setStatus("paused");
96
+        } else if ("paused".equals(plan.getStatus())) {
97
+            plan.setStatus("active");
105 98
         }
106
-        plan.setStatus(status);
107 99
         planMapper.updateById(plan);
100
+        log.info("切换检测计划状态: {} -> {}", id, plan.getStatus());
101
+        return plan;
108 102
     }
109 103
 
110 104
     /**
111
-     * 获取所有到期需执行的计划
105
+     * 获取到期计划
112 106
      */
113 107
     public List<QualityTestPlan> getDuePlans() {
114
-        return planMapper.selectList(
115
-                new LambdaQueryWrapper<QualityTestPlan>()
116
-                        .eq(QualityTestPlan::getStatus, "active")
117
-                        .le(QualityTestPlan::getNextTestDate, LocalDate.now())
118
-                        .and(w -> w.isNull(QualityTestPlan::getEndDate)
119
-                                .or().ge(QualityTestPlan::getEndDate, LocalDate.now()))
120
-                        .orderByAsc(QualityTestPlan::getNextTestDate)
121
-        );
108
+        LambdaQueryWrapper<QualityTestPlan> wrapper = new LambdaQueryWrapper<>();
109
+        wrapper.eq(QualityTestPlan::getStatus, "active")
110
+               .le(QualityTestPlan::getNextTestDate, LocalDate.now())
111
+               .orderByAsc(QualityTestPlan::getNextTestDate);
112
+        return planMapper.selectList(wrapper);
122 113
     }
123 114
 
124 115
     /**
125
-     * 执行计划后更新下次检测日期
116
+     * 标记已执行,计算下次检测日期
126 117
      */
127
-    public void markExecuted(Long id) {
128
-        QualityTestPlan plan = planMapper.selectById(id);
129
-        if (plan == null) return;
130
-
118
+    @Transactional
119
+    public QualityTestPlan markExecuted(Long id) {
120
+        QualityTestPlan plan = getById(id);
131 121
         plan.setExecutionCount(plan.getExecutionCount() + 1);
132 122
 
133
-        LocalDate current = plan.getNextTestDate() != null ? plan.getNextTestDate() : LocalDate.now();
134
-        switch (plan.getFrequency()) {
135
-            case "daily" -> plan.setNextTestDate(current.plusDays(1));
136
-            case "weekly" -> plan.setNextTestDate(current.plusWeeks(1));
137
-            case "monthly" -> plan.setNextTestDate(current.plusMonths(1));
123
+        // 计算下次检测日期
124
+        LocalDate nextDate = plan.getNextTestDate();
125
+        if (nextDate == null) nextDate = LocalDate.now();
126
+
127
+        switch (plan.getFrequency() != null ? plan.getFrequency() : "daily") {
128
+            case "daily" -> nextDate = nextDate.plusDays(1);
129
+            case "weekly" -> nextDate = nextDate.plusWeeks(1);
130
+            case "monthly" -> nextDate = nextDate.plusMonths(1);
131
+            default -> nextDate = nextDate.plusDays(1);
138 132
         }
133
+        plan.setNextTestDate(nextDate);
139 134
 
140
-        // 检查是否过期
141
-        if (plan.getEndDate() != null && plan.getNextTestDate().isAfter(plan.getEndDate())) {
142
-            plan.setStatus("completed");
135
+        // 如果下次检测超过结束日期,自动过期
136
+        if (plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate())) {
137
+            plan.setStatus("expired");
143 138
         }
144 139
 
145 140
         planMapper.updateById(plan);
141
+        log.info("标记检测计划已执行: {}, 下次检测: {}", id, nextDate);
142
+        return plan;
146 143
     }
147 144
 }

+ 97
- 67
wm-production/src/main/resources/db/V4__quality_ledger.sql Прегледај датотеку

@@ -1,109 +1,134 @@
1 1
 -- ============================================================
2
+-- V4: 水质检测台账 (GB5749-2022)
2 3
 -- ============================================================
3 4
 
4 5
 -- 1. 水质检测记录表
5 6
 CREATE TABLE IF NOT EXISTS prod_quality_test_record (
6
-    id                  BIGSERIAL PRIMARY KEY,
7
-    test_type           VARCHAR(20)     NOT NULL DEFAULT 'routine',   -- routine/special/complaint
8
-    water_type          VARCHAR(20)     NOT NULL DEFAULT 'treated',   -- raw/treated/network
9
-    sampling_point      VARCHAR(100),                                  -- 采样点
10
-    area                VARCHAR(50),                                   -- 所属区域
11
-    test_date           DATE            NOT NULL,                      -- 检测日期
12
-    test_time           TIME,                                          -- 检测时间
13
-    tester              VARCHAR(50),                                   -- 检测人
14
-    turbidity           NUMERIC(10,2),                                 -- 浊度 (NTU)
15
-    ph                  NUMERIC(5,2),                                  -- pH值
16
-    residual_chlorine   NUMERIC(6,3),                                  -- 余氯 (mg/L)
17
-    color               NUMERIC(8,2),                                  -- 色度 (度)
18
-    odor                NUMERIC(4,1),                                  -- 嗅味 (级)
19
-    ecoli               NUMERIC(10,2),                                 -- 大肠杆菌 (CFU/100mL)
20
-    colony_count        NUMERIC(10,2),                                 -- 菌落总数 (CFU/mL)
21
-    compliance_status   VARCHAR(20)     NOT NULL DEFAULT 'pending',    -- qualified/unqualified/pending
22
-    unqualified_items   TEXT,                                          -- 不合格项 (JSON)
23
-    remark              VARCHAR(500),                                  -- 备注
24
-    deleted             INTEGER         NOT NULL DEFAULT 0,
25
-    created_at          TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
26
-    updated_at          TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
7
+    id              BIGSERIAL PRIMARY KEY,
8
+    test_type       VARCHAR(50)     NOT NULL DEFAULT 'routine',
9
+    water_type      VARCHAR(50)     NOT NULL,
10
+    sampling_point  VARCHAR(200),
11
+    area            VARCHAR(200),
12
+    test_date       DATE            NOT NULL DEFAULT CURRENT_DATE,
13
+    test_time       TIME,
14
+    tester          VARCHAR(100),
15
+    turbidity       NUMERIC(10,4),
16
+    ph              NUMERIC(6,3),
17
+    residual_chlorine NUMERIC(10,4),
18
+    color           NUMERIC(10,2),
19
+    odor            VARCHAR(100),
20
+    ecoli           NUMERIC(12,2),
21
+    colony_count    NUMERIC(12,2),
22
+    compliance_status VARCHAR(20)   DEFAULT 'pending',
23
+    unqualified_items TEXT,
24
+    remark          TEXT,
25
+    deleted         INTEGER         NOT NULL DEFAULT 0,
26
+    created_at      TIMESTAMP       NOT NULL DEFAULT NOW(),
27
+    updated_at      TIMESTAMP       NOT NULL DEFAULT NOW()
27 28
 );
28 29
 
29
-CREATE INDEX IF NOT EXISTS idx_quality_record_type ON prod_quality_test_record(test_type);
30
+CREATE INDEX IF NOT EXISTS idx_quality_record_test_type ON prod_quality_test_record(test_type);
30 31
 CREATE INDEX IF NOT EXISTS idx_quality_record_water_type ON prod_quality_test_record(water_type);
32
+CREATE INDEX IF NOT EXISTS idx_quality_record_test_date ON prod_quality_test_record(test_date);
31 33
 CREATE INDEX IF NOT EXISTS idx_quality_record_area ON prod_quality_test_record(area);
32
-CREATE INDEX IF NOT EXISTS idx_quality_record_date ON prod_quality_test_record(test_date);
33 34
 CREATE INDEX IF NOT EXISTS idx_quality_record_compliance ON prod_quality_test_record(compliance_status);
34 35
 CREATE INDEX IF NOT EXISTS idx_quality_record_deleted ON prod_quality_test_record(deleted);
35 36
 
36 37
 COMMENT ON TABLE prod_quality_test_record IS '水质检测记录表';
37
-COMMENT ON COLUMN prod_quality_test_record.test_type IS '检测类型: routine-常规/special-专项/complaint-投诉';
38
-COMMENT ON COLUMN prod_quality_test_record.water_type IS '水样类型: raw-原水/treated-出厂水/network-管网末梢水';
39
-COMMENT ON COLUMN prod_quality_test_record.compliance_status IS '合格状态: qualified-合格/unqualified-不合格/pending-待判定';
38
+COMMENT ON COLUMN prod_quality_test_record.test_type IS '检测类型: routine/emergency/special';
39
+COMMENT ON COLUMN prod_quality_test_record.water_type IS '水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater';
40
+COMMENT ON COLUMN prod_quality_test_record.compliance_status IS '合格状态: qualified/unqualified/pending';
40 41
 
42
+-- 2. 水质标准表
41 43
 CREATE TABLE IF NOT EXISTS prod_quality_standard (
42 44
     id              BIGSERIAL PRIMARY KEY,
43
-    standard_name   VARCHAR(100)    NOT NULL,
44
-    standard_code   VARCHAR(50)     NOT NULL DEFAULT 'GB5749-2022',
45
-    param_name      VARCHAR(50)     NOT NULL,                          -- 参数编码
46
-    param_label     VARCHAR(50),                                       -- 参数显示名
47
-    param_unit      VARCHAR(20),                                       -- 单位
48
-    min_value       NUMERIC(12,4),                                     -- 最小值 (NULL=无下限)
49
-    max_value       NUMERIC(12,4),                                     -- 最大值 (NULL=无上限)
50
-    water_type      VARCHAR(20)     NOT NULL DEFAULT 'all',            -- 适用水样类型
51
-    enabled         INTEGER         NOT NULL DEFAULT 1,
45
+    standard_name   VARCHAR(200)    NOT NULL,
46
+    standard_code   VARCHAR(100),
47
+    param_name      VARCHAR(100)    NOT NULL,
48
+    param_label     VARCHAR(100)    NOT NULL,
49
+    param_unit      VARCHAR(50),
50
+    min_value       NUMERIC(12,4),
51
+    max_value       NUMERIC(12,4),
52
+    water_type      VARCHAR(50)     NOT NULL,
53
+    enabled         BOOLEAN         NOT NULL DEFAULT TRUE,
52 54
     deleted         INTEGER         NOT NULL DEFAULT 0,
53
-    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
-    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
55
+    created_at      TIMESTAMP       NOT NULL DEFAULT NOW(),
56
+    updated_at      TIMESTAMP       NOT NULL DEFAULT NOW()
55 57
 );
56 58
 
57
-CREATE INDEX IF NOT EXISTS idx_quality_standard_code ON prod_quality_standard(standard_code);
59
+CREATE INDEX IF NOT EXISTS idx_quality_standard_water_type ON prod_quality_standard(water_type);
58 60
 CREATE INDEX IF NOT EXISTS idx_quality_standard_param ON prod_quality_standard(param_name);
61
+CREATE INDEX IF NOT EXISTS idx_quality_standard_enabled ON prod_quality_standard(enabled);
59 62
 CREATE INDEX IF NOT EXISTS idx_quality_standard_deleted ON prod_quality_standard(deleted);
60 63
 
61
-COMMENT ON TABLE prod_quality_standard IS '水质标准表 (基于GB5749-2022)';
62
-
63
-INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type)
64
-VALUES
65
-    ('生活饮用水卫生标准', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'treated'),
66
-    ('生活饮用水卫生标准', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 3.0, 'network'),
67
-    ('生活饮用水卫生标准', 'GB5749-2022', 'ph', 'pH', '', 6.5, 8.5, 'all'),
68
-    ('生活饮用水卫生标准', 'GB5749-2022', 'residual_chlorine', '余氯', 'mg/L', 0.3, 2.0, 'treated'),
69
-    ('生活饮用水卫生标准', 'GB5749-2022', 'residual_chlorine', '余氯', 'mg/L', 0.05, 2.0, 'network'),
70
-    ('生活饮用水卫生标准', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'all'),
71
-    ('生活饮用水卫生标准', 'GB5749-2022', 'odor', '嗅味', '级', NULL, 2.0, 'all'),
72
-    ('生活饮用水卫生标准', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'all'),
73
-    ('生活饮用水卫生标准', 'GB5749-2022', 'colony_count', '菌落总数', 'CFU/mL', NULL, 100.0, 'all')
74
-ON CONFLICT DO NOTHING;
64
+COMMENT ON TABLE prod_quality_standard IS '水质标准表';
65
+COMMENT ON COLUMN prod_quality_standard.water_type IS '水质类型: rawWater/factoryWater/pipeNetworkWater/endUserWater';
75 66
 
76 67
 -- 3. 水质检测计划表
77 68
 CREATE TABLE IF NOT EXISTS prod_quality_test_plan (
78 69
     id              BIGSERIAL PRIMARY KEY,
79
-    plan_name       VARCHAR(100)    NOT NULL,
80
-    test_type       VARCHAR(20)     NOT NULL DEFAULT 'routine',        -- routine/special
81
-    water_type      VARCHAR(20)     NOT NULL DEFAULT 'treated',
82
-    sampling_point  VARCHAR(100),
83
-    area            VARCHAR(50),
84
-    frequency       VARCHAR(20)     NOT NULL DEFAULT 'daily',          -- daily/weekly/monthly
85
-    test_params     VARCHAR(200),                                      -- 检测参数 (逗号分隔)
70
+    plan_name       VARCHAR(200)    NOT NULL,
71
+    test_type       VARCHAR(50)     NOT NULL DEFAULT 'routine',
72
+    water_type      VARCHAR(50)     NOT NULL,
73
+    sampling_point  VARCHAR(200),
74
+    area            VARCHAR(200),
75
+    frequency       VARCHAR(20)     NOT NULL DEFAULT 'daily',
76
+    test_params     TEXT,
86 77
     start_date      DATE            NOT NULL,
87
-    end_date        DATE,                                              -- NULL=长期
78
+    end_date        DATE,
88 79
     next_test_date  DATE,
89
-    status          VARCHAR(20)     NOT NULL DEFAULT 'active',         -- active/paused/completed
80
+    status          VARCHAR(20)     NOT NULL DEFAULT 'active',
90 81
     execution_count INTEGER         NOT NULL DEFAULT 0,
91
-    remark          VARCHAR(500),
82
+    remark          TEXT,
92 83
     deleted         INTEGER         NOT NULL DEFAULT 0,
93
-    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
94
-    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
84
+    created_at      TIMESTAMP       NOT NULL DEFAULT NOW(),
85
+    updated_at      TIMESTAMP       NOT NULL DEFAULT NOW()
95 86
 );
96 87
 
88
+CREATE INDEX IF NOT EXISTS idx_quality_plan_water_type ON prod_quality_test_plan(water_type);
97 89
 CREATE INDEX IF NOT EXISTS idx_quality_plan_status ON prod_quality_test_plan(status);
98
-CREATE INDEX IF NOT EXISTS idx_quality_plan_frequency ON prod_quality_test_plan(frequency);
99 90
 CREATE INDEX IF NOT EXISTS idx_quality_plan_next_date ON prod_quality_test_plan(next_test_date);
100 91
 CREATE INDEX IF NOT EXISTS idx_quality_plan_deleted ON prod_quality_test_plan(deleted);
101 92
 
102 93
 COMMENT ON TABLE prod_quality_test_plan IS '水质检测计划表';
103
-COMMENT ON COLUMN prod_quality_test_plan.frequency IS '检测频率: daily-日检/weekly-周检/monthly-月检';
104
-COMMENT ON COLUMN prod_quality_test_plan.status IS '计划状态: active-启用/paused-暂停/completed-已完成';
94
+COMMENT ON COLUMN prod_quality_test_plan.frequency IS '检测频率: daily/weekly/monthly';
95
+COMMENT ON COLUMN prod_quality_test_plan.status IS '状态: active/paused/expired';
96
+
97
+-- ============================================================
98
+-- GB5749-2022 默认标准数据
99
+-- ============================================================
100
+
101
+-- 原水 (rawWater) 标准
102
+INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES
103
+('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 4.0, 'rawWater', TRUE),
104
+('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'rawWater', TRUE),
105
+('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'rawWater', TRUE),
106
+('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 10000.0, 'rawWater', TRUE),
107
+('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 500.0, 'rawWater', TRUE);
108
+
109
+-- 出厂水 (factoryWater) 标准
110
+INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES
111
+('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'factoryWater', TRUE),
112
+('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'factoryWater', TRUE),
113
+('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.3, 4.0, 'factoryWater', TRUE),
114
+('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'factoryWater', TRUE),
115
+('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'factoryWater', TRUE),
116
+('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'factoryWater', TRUE);
117
+
118
+-- 管网水 (pipeNetworkWater) 标准
119
+INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES
120
+('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'pipeNetworkWater', TRUE),
121
+('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'pipeNetworkWater', TRUE),
122
+('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'pipeNetworkWater', TRUE),
123
+('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'pipeNetworkWater', TRUE),
124
+('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'pipeNetworkWater', TRUE),
125
+('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'pipeNetworkWater', TRUE);
126
+
127
+-- 末梢水 (endUserWater) 标准
128
+INSERT INTO prod_quality_standard (standard_name, standard_code, param_name, param_label, param_unit, min_value, max_value, water_type, enabled) VALUES
129
+('GB5749-2022', 'GB5749-2022', 'turbidity', '浊度', 'NTU', NULL, 1.0, 'endUserWater', TRUE),
130
+('GB5749-2022', 'GB5749-2022', 'ph', 'pH值', '', 6.5, 8.5, 'endUserWater', TRUE),
131
+('GB5749-2022', 'GB5749-2022', 'residualChlorine', '余氯', 'mg/L', 0.05, 4.0, 'endUserWater', TRUE),
132
+('GB5749-2022', 'GB5749-2022', 'color', '色度', '度', NULL, 15.0, 'endUserWater', TRUE),
133
+('GB5749-2022', 'GB5749-2022', 'ecoli', '大肠杆菌', 'CFU/100mL', NULL, 0.0, 'endUserWater', TRUE),
134
+('GB5749-2022', 'GB5749-2022', 'colonyCount', '菌落总数', 'CFU/mL', NULL, 100.0, 'endUserWater', TRUE);

+ 65
- 93
wm-production/src/main/resources/mapper/QualityTestRecordMapper.xml Прегледај датотеку

@@ -2,69 +2,55 @@
2 2
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3 3
 <mapper namespace="com.water.production.mapper.QualityTestRecordMapper">
4 4
 
5
-    <!-- 通用 WHERE 条件 -->
6
-    <sql id="queryConditions">
7
-        WHERE r.deleted = 0
8
-        <if test="testType != null and testType != ''">AND r.test_type = #{testType}</if>
9
-        <if test="waterType != null and waterType != ''">AND r.water_type = #{waterType}</if>
10
-        <if test="area != null and area != ''">AND r.area = #{area}</if>
11
-        <if test="samplingPoint != null and samplingPoint != ''">
12
-            AND r.sampling_point LIKE '%' || #{samplingPoint} || '%'
13
-        </if>
14
-        <if test="tester != null and tester != ''">
15
-            AND r.tester LIKE '%' || #{tester} || '%'
16
-        </if>
17
-        <if test="complianceStatus != null and complianceStatus != ''">
18
-            AND r.compliance_status = #{complianceStatus}
19
-        </if>
20
-        <if test="startDate != null and startDate != ''">AND r.test_date &gt;= #{startDate}::date</if>
21
-        <if test="endDate != null and endDate != ''">AND r.test_date &lt;= #{endDate}::date</if>
22
-        <if test="keyword != null and keyword != ''">
23
-            AND (r.sampling_point LIKE '%' || #{keyword} || '%'
24
-                 OR r.tester LIKE '%' || #{keyword} || '%'
25
-                 OR r.area LIKE '%' || #{keyword} || '%'
26
-                 OR r.remark LIKE '%' || #{keyword} || '%')
27
-        </if>
28
-    </sql>
29
-
30
-    <!-- 分页查询台账 -->
31 5
     <select id="selectRecordPage" resultType="java.util.Map">
32 6
         SELECT
33
-            r.id, r.test_type AS "testType", r.water_type AS "waterType",
34
-            r.sampling_point AS "samplingPoint", r.area, r.test_date AS "testDate",
35
-            r.test_time AS "testTime", r.tester, r.turbidity, r.ph,
36
-            r.residual_chlorine AS "residualChlorine", r.color, r.odor,
37
-            r.ecoli, r.colony_count AS "colonyCount",
38
-            r.compliance_status AS "complianceStatus",
39
-            r.unqualified_items AS "unqualifiedItems", r.remark,
40
-            r.created_at AS "createdAt", r.updated_at AS "updatedAt"
41
-        FROM prod_quality_test_record r
42
-        <include refid="queryConditions"/>
7
+            id, test_type, water_type, sampling_point, area, test_date, test_time,
8
+            tester, turbidity, ph, residual_chlorine, color, odor, ecoli, colony_count,
9
+            compliance_status, unqualified_items, remark, created_at, updated_at
10
+        FROM prod_quality_test_record
11
+        WHERE deleted = 0
12
+        <if test="testType != null and testType != ''">AND test_type = #{testType}</if>
13
+        <if test="waterType != null and waterType != ''">AND water_type = #{waterType}</if>
14
+        <if test="area != null and area != ''">AND area = #{area}</if>
15
+        <if test="samplingPoint != null and samplingPoint != ''">AND sampling_point = #{samplingPoint}</if>
16
+        <if test="tester != null and tester != ''">AND tester LIKE '%' || #{tester} || '%'</if>
17
+        <if test="complianceStatus != null and complianceStatus != ''">AND compliance_status = #{complianceStatus}</if>
18
+        <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
19
+        <if test="endDate != null and endDate != ''">AND test_date &lt;= #{endDate}::date</if>
20
+        <if test="keyword != null and keyword != ''">
21
+            AND (sampling_point LIKE '%' || #{keyword} || '%' OR tester LIKE '%' || #{keyword} || '%' OR remark LIKE '%' || #{keyword} || '%')
22
+        </if>
43 23
         <choose>
44
-            <when test="sortField != null and sortField == 'testDate'">
45
-                ORDER BY r.test_date
46
-                <if test="sortOrder != null and sortOrder == 'asc'">ASC</if>
47
-                <if test="sortOrder == null or sortOrder != 'asc'">DESC</if>
24
+            <when test="sortField != null and sortField != '' and sortOrder != null and sortOrder == 'asc'">
25
+                ORDER BY ${sortField} ASC
48 26
             </when>
49
-            <when test="sortField != null and sortField == 'tester'">
50
-                ORDER BY r.tester
51
-                <if test="sortOrder != null and sortOrder == 'asc'">ASC</if>
52
-                <if test="sortOrder == null or sortOrder != 'asc'">DESC</if>
27
+            <when test="sortField != null and sortField != '' and sortOrder != null and sortOrder == 'desc'">
28
+                ORDER BY ${sortField} DESC
53 29
             </when>
54
-            <otherwise>ORDER BY r.created_at DESC</otherwise>
30
+            <otherwise>ORDER BY created_at DESC</otherwise>
55 31
         </choose>
56 32
         OFFSET #{offset} LIMIT #{limit}
57 33
     </select>
58 34
 
59
-    <!-- 统计总数 -->
60 35
     <select id="countRecords" resultType="java.lang.Long">
61
-        SELECT COUNT(*) FROM prod_quality_test_record r
62
-        <include refid="queryConditions"/>
36
+        SELECT COUNT(*)
37
+        FROM prod_quality_test_record
38
+        WHERE deleted = 0
39
+        <if test="testType != null and testType != ''">AND test_type = #{testType}</if>
40
+        <if test="waterType != null and waterType != ''">AND water_type = #{waterType}</if>
41
+        <if test="area != null and area != ''">AND area = #{area}</if>
42
+        <if test="samplingPoint != null and samplingPoint != ''">AND sampling_point = #{samplingPoint}</if>
43
+        <if test="tester != null and tester != ''">AND tester LIKE '%' || #{tester} || '%'</if>
44
+        <if test="complianceStatus != null and complianceStatus != ''">AND compliance_status = #{complianceStatus}</if>
45
+        <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
46
+        <if test="endDate != null and endDate != ''">AND test_date &lt;= #{endDate}::date</if>
47
+        <if test="keyword != null and keyword != ''">
48
+            AND (sampling_point LIKE '%' || #{keyword} || '%' OR tester LIKE '%' || #{keyword} || '%' OR remark LIKE '%' || #{keyword} || '%')
49
+        </if>
63 50
     </select>
64 51
 
65
-    <!-- 按合格状态统计 -->
66 52
     <select id="statByComplianceStatus" resultType="java.util.Map">
67
-        SELECT compliance_status AS "status", COUNT(*) AS count
53
+        SELECT compliance_status AS status, COUNT(*) AS count
68 54
         FROM prod_quality_test_record
69 55
         WHERE deleted = 0
70 56
         <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
@@ -72,11 +58,12 @@
72 58
         GROUP BY compliance_status
73 59
     </select>
74 60
 
75
-    <!-- 按水样类型统计合格率 -->
76 61
     <select id="statRateByWaterType" resultType="java.util.Map">
77
-        SELECT water_type AS "waterType",
78
-               COUNT(*) AS total,
79
-               COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified
62
+        SELECT
63
+            water_type AS waterType,
64
+            COUNT(*) AS total,
65
+            COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
66
+            ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
80 67
         FROM prod_quality_test_record
81 68
         WHERE deleted = 0
82 69
         <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
@@ -84,11 +71,12 @@
84 71
         GROUP BY water_type
85 72
     </select>
86 73
 
87
-    <!-- 按区域统计合格率 -->
88 74
     <select id="statRateByArea" resultType="java.util.Map">
89
-        SELECT area,
90
-               COUNT(*) AS total,
91
-               COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified
75
+        SELECT
76
+            area,
77
+            COUNT(*) AS total,
78
+            COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
79
+            ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
92 80
         FROM prod_quality_test_record
93 81
         WHERE deleted = 0
94 82
         <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
@@ -96,55 +84,39 @@
96 84
         GROUP BY area
97 85
     </select>
98 86
 
99
-    <!-- 按指标统计不合格次数 -->
100 87
     <select id="statUnqualifiedByParam" resultType="java.util.Map">
101
-        SELECT param_name AS "paramName", COUNT(*) AS count
102
-        FROM (
103
-            SELECT jsonb_array_elements(
104
-                CASE
105
-                    WHEN unqualified_items IS NOT NULL AND unqualified_items != ''
106
-                    THEN unqualified_items::jsonb
107
-                    ELSE '[]'::jsonb
108
-                END
109
-            )->>'param' AS param_name
110
-            FROM prod_quality_test_record
111
-            WHERE deleted = 0 AND compliance_status = 'unqualified'
112
-            <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
113
-            <if test="endDate != null and endDate != ''">AND test_date &lt;= #{endDate}::date</if>
114
-        ) sub
115
-        WHERE param_name IS NOT NULL
116
-        GROUP BY param_name
88
+        SELECT
89
+            jsonb_array_elements_text(unqualified_items::jsonb) AS param,
90
+            COUNT(*) AS count
91
+        FROM prod_quality_test_record
92
+        WHERE deleted = 0 AND compliance_status = 'unqualified' AND unqualified_items IS NOT NULL
93
+        <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
94
+        <if test="endDate != null and endDate != ''">AND test_date &lt;= #{endDate}::date</if>
95
+        GROUP BY param
117 96
         ORDER BY count DESC
118 97
     </select>
119 98
 
120
-    <!-- 月度合格率趋势 -->
121 99
     <select id="statMonthlyTrend" resultType="java.util.Map">
122
-        SELECT TO_CHAR(test_date, 'YYYY-MM') AS month,
123
-               COUNT(*) AS total,
124
-               COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
125
-               ROUND(
126
-                   COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / COUNT(*), 1
127
-               ) AS rate
100
+        SELECT
101
+            TO_CHAR(test_date, 'YYYY-MM') AS month,
102
+            COUNT(*) AS total,
103
+            COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) AS qualified,
104
+            ROUND(COUNT(CASE WHEN compliance_status = 'qualified' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2) AS rate
128 105
         FROM prod_quality_test_record
129 106
         WHERE deleted = 0
130 107
         <if test="startDate != null and startDate != ''">AND test_date &gt;= #{startDate}::date</if>
131 108
         <if test="endDate != null and endDate != ''">AND test_date &lt;= #{endDate}::date</if>
132
-        GROUP BY TO_CHAR(test_date, 'YYYY-MM')
133
-        ORDER BY month ASC
109
+        GROUP BY month
110
+        ORDER BY month
134 111
     </select>
135 112
 
136
-    <!-- 各指标均值统计 -->
137 113
     <select id="statParamAvg" resultType="java.util.Map">
138 114
         SELECT
139
-            ROUND(AVG(turbidity), 2) AS "turbidityAvg",
140
-            ROUND(AVG(ph), 2) AS "phAvg",
141
-            ROUND(AVG(residual_chlorine), 3) AS "residualChlorineAvg",
142
-            ROUND(AVG(color), 2) AS "colorAvg",
143
-            ROUND(AVG(odor), 2) AS "odorAvg",
144
-            ROUND(AVG(ecoli), 2) AS "ecoliAvg",
145
-            ROUND(AVG(colony_count), 2) AS "colonyCountAvg"
115
+            ROUND(AVG(turbidity)::numeric, 4) AS avgTurbidity,
116
+            ROUND(AVG(ph)::numeric, 4) AS avgPh,
117
+            ROUND(AVG(residual_chlorine)::numeric, 4) AS avgResidualChlorine
146 118
         FROM prod_quality_test_record
147
-        WHERE deleted = 0
119
+        WHERE deleted = 0 AND compliance_status IS NOT NULL
148 120
     </select>
149 121
 
150 122
 </mapper>

+ 310
- 497
wm-production/src/test/java/com/water/production/service/QualityLedgerServiceTest.java Прегледај датотеку

@@ -1,586 +1,399 @@
1 1
 package com.water.production.service;
2 2
 
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
3 4
 import com.water.production.dto.QualityQueryRequest;
4 5
 import com.water.production.dto.QualityStatVO;
5 6
 import com.water.production.entity.QualityStandard;
6 7
 import com.water.production.entity.QualityTestPlan;
7 8
 import com.water.production.entity.QualityTestRecord;
9
+import com.water.production.mapper.QualityStandardMapper;
10
+import com.water.production.mapper.QualityTestPlanMapper;
11
+import com.water.production.mapper.QualityTestRecordMapper;
12
+import org.junit.jupiter.api.BeforeEach;
8 13
 import org.junit.jupiter.api.DisplayName;
9 14
 import org.junit.jupiter.api.Test;
15
+import org.junit.jupiter.api.extension.ExtendWith;
16
+import org.mockito.InjectMocks;
17
+import org.mockito.Mock;
18
+import org.mockito.junit.jupiter.MockitoExtension;
10 19
 
11
-import java.math.BigDecimal;
12
-import java.math.RoundingMode;
13 20
 import java.time.LocalDate;
14 21
 import java.time.LocalTime;
15 22
 import java.util.*;
16
-import java.util.stream.Collectors;
17 23
 
18 24
 import static org.junit.jupiter.api.Assertions.*;
25
+import static org.mockito.ArgumentMatchers.*;
26
+import static org.mockito.Mockito.*;
19 27
 
20
-/**
21
- * 水质检测台账单元测试
22
- * 覆盖实体构建、合格判定、统计计算、计划调度、查询筛选、CSV转义等
23
- */
28
+@ExtendWith(MockitoExtension.class)
24 29
 class QualityLedgerServiceTest {
25 30
 
26
-    // ========== 1. 实体完整性测试 ==========
31
+    @Mock
32
+    private QualityTestRecordMapper recordMapper;
33
+
34
+    @Mock
35
+    private QualityStandardService standardService;
36
+
37
+    @Mock
38
+    private QualityTestPlanMapper planMapper;
39
+
40
+    @Mock
41
+    private QualityStandardMapper standardMapper;
42
+
43
+    @InjectMocks
44
+    private QualityLedgerService ledgerService;
45
+
46
+    private QualityTestPlanService planService;
47
+
48
+    private QualityTestRecord sampleRecord;
49
+
50
+    @BeforeEach
51
+    void setUp() {
52
+        planService = new QualityTestPlanService(planMapper);
53
+
54
+        sampleRecord = new QualityTestRecord();
55
+        sampleRecord.setId(1L);
56
+        sampleRecord.setTestType("routine");
57
+        sampleRecord.setWaterType("factoryWater");
58
+        sampleRecord.setSamplingPoint("出厂水采样点A");
59
+        sampleRecord.setArea("城区A");
60
+        sampleRecord.setTestDate(LocalDate.now());
61
+        sampleRecord.setTestTime(LocalTime.of(10, 0));
62
+        sampleRecord.setTester("张三");
63
+        sampleRecord.setTurbidity(0.5);
64
+        sampleRecord.setPh(7.2);
65
+        sampleRecord.setResidualChlorine(0.8);
66
+        sampleRecord.setColor(5.0);
67
+        sampleRecord.setOdor("无");
68
+        sampleRecord.setEcoli(0.0);
69
+        sampleRecord.setColonyCount(20.0);
70
+    }
71
+
72
+    // ==================== 记录 CRUD 测试 ====================
27 73
 
28 74
     @Test
29
-    @DisplayName("QualityTestRecord 实体字段完整性")
30
-    void testQualityTestRecordEntity() {
31
-        QualityTestRecord record = new QualityTestRecord();
32
-        record.setId(1L);
33
-        record.setTestType("routine");
34
-        record.setWaterType("treated");
35
-        record.setSamplingPoint("出厂水口");
36
-        record.setArea("一体化水厂");
37
-        record.setTestDate(LocalDate.of(2026, 6, 14));
38
-        record.setTestTime(LocalTime.of(9, 30));
39
-        record.setTester("张三");
40
-        record.setTurbidity(new BigDecimal("0.5"));
41
-        record.setPh(new BigDecimal("7.2"));
42
-        record.setResidualChlorine(new BigDecimal("0.5"));
43
-        record.setColor(new BigDecimal("5"));
44
-        record.setOdor(new BigDecimal("0"));
45
-        record.setEcoli(BigDecimal.ZERO);
46
-        record.setColonyCount(new BigDecimal("12"));
47
-        record.setComplianceStatus("qualified");
48
-        record.setRemark("正常");
49
-
50
-        assertEquals(1L, record.getId());
51
-        assertEquals("routine", record.getTestType());
52
-        assertEquals("treated", record.getWaterType());
53
-        assertEquals("出厂水口", record.getSamplingPoint());
54
-        assertEquals(new BigDecimal("0.5"), record.getTurbidity());
55
-        assertEquals(new BigDecimal("7.2"), record.getPh());
56
-        assertEquals(new BigDecimal("0.5"), record.getResidualChlorine());
57
-        assertEquals("qualified", record.getComplianceStatus());
58
-        assertNotNull(record.getTestDate());
59
-        assertNotNull(record.getTestTime());
75
+    @DisplayName("1. 创建检测记录-自动判定合格")
76
+    void testCreateRecord_Qualified() {
77
+        when(standardService.getStandard("factoryWater", "turbidity"))
78
+                .thenReturn(buildStandard("turbidity", null, 1.0));
79
+        when(standardService.getStandard("factoryWater", "ph"))
80
+                .thenReturn(buildStandard("ph", 6.5, 8.5));
81
+        when(standardService.getStandard("factoryWater", "residualChlorine"))
82
+                .thenReturn(buildStandard("residualChlorine", 0.3, 4.0));
83
+        when(standardService.getStandard("factoryWater", "color"))
84
+                .thenReturn(buildStandard("color", null, 15.0));
85
+        when(standardService.getStandard("factoryWater", "ecoli"))
86
+                .thenReturn(buildStandard("ecoli", null, 0.0));
87
+        when(standardService.getStandard("factoryWater", "colonyCount"))
88
+                .thenReturn(buildStandard("colonyCount", null, 100.0));
89
+        when(recordMapper.insert(any())).thenReturn(1);
90
+
91
+        QualityTestRecord result = ledgerService.create(sampleRecord);
92
+
93
+        assertEquals("qualified", result.getComplianceStatus());
94
+        assertNull(result.getUnqualifiedItems());
95
+        verify(recordMapper).insert(sampleRecord);
60 96
     }
61 97
 
62 98
     @Test
63
-    @DisplayName("QualityStandard 实体字段完整性")
64
-    void testQualityStandardEntity() {
65
-        QualityStandard standard = new QualityStandard();
66
-        standard.setId(1L);
67
-        standard.setStandardName("生活饮用水卫生标准");
68
-        standard.setStandardCode("GB5749-2022");
69
-        standard.setParamName("turbidity");
70
-        standard.setParamLabel("浊度");
71
-        standard.setParamUnit("NTU");
72
-        standard.setMinValue(null);
73
-        standard.setMaxValue(new BigDecimal("1.0"));
74
-        standard.setWaterType("treated");
75
-        standard.setEnabled(1);
76
-
77
-        assertEquals("GB5749-2022", standard.getStandardCode());
78
-        assertEquals("turbidity", standard.getParamName());
79
-        assertNull(standard.getMinValue());
80
-        assertEquals(new BigDecimal("1.0"), standard.getMaxValue());
81
-        assertEquals("treated", standard.getWaterType());
82
-        assertEquals(1, standard.getEnabled());
99
+    @DisplayName("2. 创建检测记录-自动判定不合格")
100
+    void testCreateRecord_Unqualified() {
101
+        sampleRecord.setTurbidity(2.5); // 超标
102
+        sampleRecord.setEcoli(10.0); // 超标
103
+
104
+        when(standardService.getStandard("factoryWater", "turbidity"))
105
+                .thenReturn(buildStandard("turbidity", null, 1.0));
106
+        when(standardService.getStandard("factoryWater", "ph"))
107
+                .thenReturn(buildStandard("ph", 6.5, 8.5));
108
+        when(standardService.getStandard("factoryWater", "residualChlorine"))
109
+                .thenReturn(buildStandard("residualChlorine", 0.3, 4.0));
110
+        when(standardService.getStandard("factoryWater", "color"))
111
+                .thenReturn(buildStandard("color", null, 15.0));
112
+        when(standardService.getStandard("factoryWater", "ecoli"))
113
+                .thenReturn(buildStandard("ecoli", null, 0.0));
114
+        when(standardService.getStandard("factoryWater", "colonyCount"))
115
+                .thenReturn(buildStandard("colonyCount", null, 100.0));
116
+        when(recordMapper.insert(any())).thenReturn(1);
117
+
118
+        QualityTestRecord result = ledgerService.create(sampleRecord);
119
+
120
+        assertEquals("unqualified", result.getComplianceStatus());
121
+        assertNotNull(result.getUnqualifiedItems());
122
+        assertTrue(result.getUnqualifiedItems().contains("浊度"));
123
+        assertTrue(result.getUnqualifiedItems().contains("大肠杆菌"));
83 124
     }
84 125
 
85 126
     @Test
86
-    @DisplayName("QualityTestPlan 实体字段完整性")
87
-    void testQualityTestPlanEntity() {
88
-        QualityTestPlan plan = new QualityTestPlan();
89
-        plan.setId(1L);
90
-        plan.setPlanName("出厂水日检计划");
91
-        plan.setTestType("routine");
92
-        plan.setWaterType("treated");
93
-        plan.setSamplingPoint("出厂水口");
94
-        plan.setArea("一体化水厂");
95
-        plan.setFrequency("daily");
96
-        plan.setTestParams("turbidity,ph,residual_chlorine");
97
-        plan.setStartDate(LocalDate.of(2026, 6, 1));
98
-        plan.setEndDate(null);
99
-        plan.setNextTestDate(LocalDate.of(2026, 6, 14));
100
-        plan.setStatus("active");
101
-        plan.setExecutionCount(13);
102
-
103
-        assertEquals("daily", plan.getFrequency());
104
-        assertEquals("active", plan.getStatus());
105
-        assertEquals(13, plan.getExecutionCount());
106
-        assertNull(plan.getEndDate());
107
-        assertNotNull(plan.getNextTestDate());
108
-    }
127
+    @DisplayName("3. 创建检测记录-waterType为空时pending")
128
+    void testCreateRecord_PendingWhenNoWaterType() {
129
+        sampleRecord.setWaterType(null);
130
+        when(recordMapper.insert(any())).thenReturn(1);
109 131
 
110
-    // ========== 2. 合格判定逻辑测试 ==========
132
+        QualityTestRecord result = ledgerService.create(sampleRecord);
111 133
 
112
-    @Test
113
-    @DisplayName("GB5749-2022 合格判定逻辑 - 全部合格")
114
-    void testComplianceAllQualified() {
115
-        // 模拟 GB5749-2022 标准
116
-        List<QualityStandard> standards = buildDefaultStandards();
117
-
118
-        // 构建合格记录
119
-        QualityTestRecord record = new QualityTestRecord();
120
-        record.setWaterType("treated");
121
-        record.setTurbidity(new BigDecimal("0.5"));    // ≤1.0 ✓
122
-        record.setPh(new BigDecimal("7.2"));            // 6.5~8.5 ✓
123
-        record.setResidualChlorine(new BigDecimal("0.5")); // 0.3~2.0 ✓
124
-        record.setColor(new BigDecimal("5"));           // ≤15 ✓
125
-        record.setOdor(new BigDecimal("0"));            // ≤2 ✓
126
-        record.setEcoli(BigDecimal.ZERO);               // =0 ✓
127
-        record.setColonyCount(new BigDecimal("12"));    // ≤100 ✓
128
-
129
-        List<String> unqualified = evaluateCompliance(record, standards);
130
-        assertTrue(unqualified.isEmpty(), "所有指标在标准范围内应合格");
134
+        assertEquals("pending", result.getComplianceStatus());
131 135
     }
132 136
 
133 137
     @Test
134
-    @DisplayName("GB5749-2022 合格判定逻辑 - 多项超标")
135
-    void testComplianceMultipleUnqualified() {
136
-        List<QualityStandard> standards = buildDefaultStandards();
137
-
138
-        QualityTestRecord record = new QualityTestRecord();
139
-        record.setWaterType("treated");
140
-        record.setTurbidity(new BigDecimal("2.5"));    // >1.0 ✗
141
-        record.setPh(new BigDecimal("9.0"));            // >8.5 ✗
142
-        record.setResidualChlorine(new BigDecimal("0.1")); // <0.3 ✗
143
-        record.setColor(new BigDecimal("5"));           // ≤15 ✓
144
-        record.setOdor(new BigDecimal("0"));            // ≤2 ✓
145
-        record.setEcoli(BigDecimal.ZERO);               // =0 ✓
146
-        record.setColonyCount(new BigDecimal("12"));    // ≤100 ✓
147
-
148
-        List<String> unqualified = evaluateCompliance(record, standards);
149
-        assertEquals(3, unqualified.size(), "应有3项不合格: 浊度、pH、余氯");
150
-        assertTrue(unqualified.stream().anyMatch(s -> s.contains("turbidity")));
151
-        assertTrue(unqualified.stream().anyMatch(s -> s.contains("ph")));
152
-        assertTrue(unqualified.stream().anyMatch(s -> s.contains("residual_chlorine")));
138
+    @DisplayName("4. 查询检测记录分页")
139
+    void testQueryRecords() {
140
+        QualityQueryRequest request = new QualityQueryRequest();
141
+        request.setPageNum(1);
142
+        request.setPageSize(10);
143
+
144
+        List<Map<String, Object>> mockRecords = List.of(
145
+                Map.of("id", 1L, "testType", "routine"),
146
+                Map.of("id", 2L, "testType", "emergency")
147
+        );
148
+        when(recordMapper.selectRecordPage(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), eq(0), eq(10)))
149
+                .thenReturn(mockRecords);
150
+        when(recordMapper.countRecords(any(), any(), any(), any(), any(), any(), any(), any(), any()))
151
+                .thenReturn(2L);
152
+
153
+        Map<String, Object> result = ledgerService.queryRecords(request);
154
+
155
+        assertNotNull(result);
156
+        assertEquals(2L, result.get("total"));
157
+        assertEquals(1, result.get("pageNum"));
158
+        assertEquals(1L, result.get("totalPages"));
153 159
     }
154 160
 
155 161
     @Test
156
-    @DisplayName("GB5749-2022 合格判定 - 管网末梢水余氯标准不同")
157
-    void testComplianceNetworkWaterType() {
158
-        List<QualityStandard> standards = buildDefaultStandards();
159
-
160
-        QualityTestRecord record = new QualityTestRecord();
161
-        record.setWaterType("network");
162
-        record.setTurbidity(new BigDecimal("2.0"));    // ≤3.0 (管网标准) ✓
163
-        record.setPh(new BigDecimal("7.0"));            // 6.5~8.5 ✓
164
-        record.setResidualChlorine(new BigDecimal("0.1")); // 0.05~2.0 (管网标准) ✓
165
-        record.setColor(new BigDecimal("5"));           // ≤15 ✓
166
-        record.setOdor(new BigDecimal("0"));            // ≤2 ✓
167
-        record.setEcoli(BigDecimal.ZERO);
168
-        record.setColonyCount(new BigDecimal("50"));
169
-
170
-        List<String> unqualified = evaluateCompliance(record, standards);
171
-        assertTrue(unqualified.isEmpty(),
172
-                "管网末梢水浊度2.0应合格(标准≤3.0),余氯0.1应合格(标准≥0.05)");
162
+    @DisplayName("5. 获取记录详情")
163
+    void testGetById() {
164
+        when(recordMapper.selectById(1L)).thenReturn(sampleRecord);
165
+
166
+        QualityTestRecord result = ledgerService.getById(1L);
167
+
168
+        assertNotNull(result);
169
+        assertEquals(1L, result.getId());
170
+        assertEquals("routine", result.getTestType());
173 171
     }
174 172
 
175 173
     @Test
176
-    @DisplayName("合格判定 - null 值不参与判定")
177
-    void testComplianceNullValuesSkipped() {
178
-        List<QualityStandard> standards = buildDefaultStandards();
179
-
180
-        QualityTestRecord record = new QualityTestRecord();
181
-        record.setWaterType("treated");
182
-        record.setTurbidity(new BigDecimal("0.5"));
183
-        // 其他参数为 null
184
-        record.setPh(null);
185
-        record.setResidualChlorine(null);
186
-
187
-        List<String> unqualified = evaluateCompliance(record, standards);
188
-        assertTrue(unqualified.isEmpty(), "null值不应参与判定");
189
-    }
174
+    @DisplayName("6. 获取不存在记录抛异常")
175
+    void testGetById_NotFound() {
176
+        when(recordMapper.selectById(999L)).thenReturn(null);
190 177
 
191
-    // ========== 3. 统计计算逻辑测试 ==========
178
+        assertThrows(Exception.class, () -> ledgerService.getById(999L));
179
+    }
192 180
 
193 181
     @Test
194
-    @DisplayName("合格率计算")
195
-    void testQualifiedRateCalculation() {
196
-        long total = 100;
197
-        long qualified = 95;
198
-        long unqualified = 3;
199
-        long pending = 2;
200
-
201
-        BigDecimal rate = total > 0
202
-                ? BigDecimal.valueOf(qualified * 100.0 / total).setScale(1, RoundingMode.HALF_UP)
203
-                : BigDecimal.ZERO;
204
-
205
-        assertEquals(new BigDecimal("95.0"), rate);
206
-        assertEquals(total, qualified + unqualified + pending);
182
+    @DisplayName("7. 更新检测记录")
183
+    void testUpdateRecord() {
184
+        when(recordMapper.selectById(1L)).thenReturn(sampleRecord);
185
+        when(standardService.getStandard(anyString(), anyString())).thenReturn(null);
186
+        when(recordMapper.updateById(any())).thenReturn(1);
187
+        when(recordMapper.selectById(1L)).thenReturn(sampleRecord);
188
+
189
+        sampleRecord.setRemark("updated");
190
+        QualityTestRecord result = ledgerService.update(1L, sampleRecord);
191
+
192
+        assertNotNull(result);
193
+        verify(recordMapper).updateById(any());
207 194
     }
208 195
 
209 196
     @Test
210
-    @DisplayName("QualityStatVO 数据结构完整性")
211
-    void testStatVOStructure() {
212
-        QualityStatVO stat = new QualityStatVO();
213
-        stat.setTotalCount(200L);
214
-        stat.setQualifiedCount(190L);
215
-        stat.setUnqualifiedCount(8L);
216
-        stat.setPendingCount(2L);
217
-        stat.setQualifiedRate(new BigDecimal("95.0"));
218
-
219
-        Map<String, BigDecimal> rateByWaterType = new LinkedHashMap<>();
220
-        rateByWaterType.put("treated", new BigDecimal("97.5"));
221
-        rateByWaterType.put("network", new BigDecimal("92.3"));
222
-        stat.setRateByWaterType(rateByWaterType);
223
-
224
-        Map<String, Long> unqByParam = new LinkedHashMap<>();
225
-        unqByParam.put("turbidity", 5L);
226
-        unqByParam.put("residual_chlorine", 3L);
227
-        stat.setUnqualifiedByParam(unqByParam);
228
-
229
-        List<Map<String, Object>> trend = new ArrayList<>();
230
-        trend.add(Map.of("month", "2026-05", "rate", new BigDecimal("94.0")));
231
-        trend.add(Map.of("month", "2026-06", "rate", new BigDecimal("96.0")));
232
-        stat.setMonthlyTrend(trend);
233
-
234
-        assertEquals(200L, stat.getTotalCount());
235
-        assertEquals(2, stat.getRateByWaterType().size());
236
-        assertEquals(2, stat.getUnqualifiedByParam().size());
237
-        assertEquals(2, stat.getMonthlyTrend().size());
238
-        assertEquals("turbidity", stat.getUnqualifiedByParam().keySet().iterator().next());
239
-    }
197
+    @DisplayName("8. 删除检测记录")
198
+    void testDeleteRecord() {
199
+        when(recordMapper.selectById(1L)).thenReturn(sampleRecord);
200
+        when(recordMapper.deleteById(1L)).thenReturn(1);
240 201
 
241
-    // ========== 4. 查询筛选逻辑测试 ==========
202
+        ledgerService.delete(1L);
242 203
 
243
-    @Test
244
-    @DisplayName("QualityQueryRequest 默认值和筛选")
245
-    void testQueryRequestDefaults() {
246
-        QualityQueryRequest req = new QualityQueryRequest();
247
-        assertEquals(1, req.getPageNum());
248
-        assertEquals(20, req.getPageSize());
249
-        assertNull(req.getTestType());
250
-        assertNull(req.getWaterType());
251
-        assertNull(req.getArea());
252
-        assertNull(req.getComplianceStatus());
253
-        assertNull(req.getStartDate());
254
-        assertNull(req.getEndDate());
255
-        assertNull(req.getKeyword());
204
+        verify(recordMapper).deleteById(1L);
256 205
     }
257 206
 
258 207
     @Test
259
-    @DisplayName("多维度筛选逻辑")
260
-    void testMultiDimensionFilter() {
261
-        List<QualityTestRecord> records = buildMockRecords();
262
-
263
-        // 按水样类型筛选
264
-        List<QualityTestRecord> filtered = records.stream()
265
-                .filter(r -> "treated".equals(r.getWaterType()))
266
-                .collect(Collectors.toList());
267
-        assertEquals(3, filtered.size());
268
-
269
-        // 按合格状态筛选
270
-        filtered = records.stream()
271
-                .filter(r -> "qualified".equals(r.getComplianceStatus()))
272
-                .collect(Collectors.toList());
273
-        assertEquals(3, filtered.size());
274
-
275
-        // 按区域筛选
276
-        filtered = records.stream()
277
-                .filter(r -> "一体化水厂".equals(r.getArea()))
278
-                .collect(Collectors.toList());
279
-        assertEquals(2, filtered.size());
280
-
281
-        // 组合筛选: 出厂水 + 合格
282
-        filtered = records.stream()
283
-                .filter(r -> "treated".equals(r.getWaterType()) && "qualified".equals(r.getComplianceStatus()))
284
-                .collect(Collectors.toList());
285
-        assertEquals(2, filtered.size());
286
-
287
-        // 关键词搜索 (采样点)
288
-        String keyword = "出厂";
289
-        filtered = records.stream()
290
-                .filter(r -> r.getSamplingPoint() != null && r.getSamplingPoint().contains(keyword))
291
-                .collect(Collectors.toList());
292
-        assertEquals(2, filtered.size());
293
-    }
208
+    @DisplayName("9. 批量删除记录")
209
+    void testBatchDelete() {
210
+        List<Long> ids = List.of(1L, 2L, 3L);
211
+        when(recordMapper.deleteBatchIds(ids)).thenReturn(3);
294 212
 
295
-    @Test
296
-    @DisplayName("分页参数计算")
297
-    void testPaginationCalculation() {
298
-        int total = 55;
299
-        int pageSize = 20;
300
-        int pages = (int) Math.ceil((double) total / pageSize);
301
-        assertEquals(3, pages);
302
-
303
-        // 第2页偏移量
304
-        int offset = (2 - 1) * pageSize;
305
-        assertEquals(20, offset);
213
+        ledgerService.batchDelete(ids);
214
+
215
+        verify(recordMapper).deleteBatchIds(ids);
306 216
     }
307 217
 
308
-    // ========== 5. 检测计划调度测试 ==========
218
+    // ==================== 合格判定测试 ====================
309 219
 
310 220
     @Test
311
-    @DisplayName("检测计划频率计算 - 日检/周检/月检")
312
-    void testPlanFrequencyCalculation() {
313
-        LocalDate baseDate = LocalDate.of(2026, 6, 14);
314
-
315
-        // 日检
316
-        assertEquals(baseDate.plusDays(1), calculateNextDate(baseDate, "daily"));
317
-        // 周检
318
-        assertEquals(baseDate.plusWeeks(1), calculateNextDate(baseDate, "weekly"));
319
-        // 月检
320
-        assertEquals(baseDate.plusMonths(1), calculateNextDate(baseDate, "monthly"));
221
+    @DisplayName("10. 合格判定-pH偏低")
222
+    void testEvaluateCompliance_PhLow() {
223
+        sampleRecord.setPh(5.0);
224
+        when(standardService.getStandard("factoryWater", "turbidity")).thenReturn(buildStandard("turbidity", null, 1.0));
225
+        when(standardService.getStandard("factoryWater", "ph")).thenReturn(buildStandard("ph", 6.5, 8.5));
226
+        when(standardService.getStandard("factoryWater", "residualChlorine")).thenReturn(buildStandard("residualChlorine", 0.3, 4.0));
227
+        when(standardService.getStandard("factoryWater", "color")).thenReturn(buildStandard("color", null, 15.0));
228
+        when(standardService.getStandard("factoryWater", "ecoli")).thenReturn(buildStandard("ecoli", null, 0.0));
229
+        when(standardService.getStandard("factoryWater", "colonyCount")).thenReturn(buildStandard("colonyCount", null, 100.0));
230
+
231
+        ledgerService.evaluateCompliance(sampleRecord);
232
+
233
+        assertEquals("unqualified", sampleRecord.getComplianceStatus());
234
+        assertTrue(sampleRecord.getUnqualifiedItems().contains("pH"));
235
+        assertTrue(sampleRecord.getUnqualifiedItems().contains("偏低"));
321 236
     }
322 237
 
323 238
     @Test
324
-    @DisplayName("检测计划过期判定")
325
-    void testPlanExpiration() {
326
-        QualityTestPlan plan = new QualityTestPlan();
327
-        plan.setStartDate(LocalDate.of(2026, 6, 1));
328
-        plan.setEndDate(LocalDate.of(2026, 6, 30));
329
-        plan.setFrequency("daily");
330
-        plan.setStatus("active");
331
-
332
-        // 下次检测日期超出结束日期
333
-        LocalDate nextDate = LocalDate.of(2026, 7, 1);
334
-        boolean isExpired = plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate());
335
-        assertTrue(isExpired, "下次检测日期超出结束日期应判定为过期");
336
-
337
-        // 正常范围内
338
-        nextDate = LocalDate.of(2026, 6, 15);
339
-        isExpired = plan.getEndDate() != null && nextDate.isAfter(plan.getEndDate());
340
-        assertFalse(isExpired, "正常范围内不应过期");
239
+    @DisplayName("11. 合格判定-余氯偏高")
240
+    void testEvaluateCompliance_ChlorineHigh() {
241
+        sampleRecord.setResidualChlorine(5.0);
242
+        when(standardService.getStandard("factoryWater", "turbidity")).thenReturn(buildStandard("turbidity", null, 1.0));
243
+        when(standardService.getStandard("factoryWater", "ph")).thenReturn(buildStandard("ph", 6.5, 8.5));
244
+        when(standardService.getStandard("factoryWater", "residualChlorine")).thenReturn(buildStandard("residualChlorine", 0.3, 4.0));
245
+        when(standardService.getStandard("factoryWater", "color")).thenReturn(buildStandard("color", null, 15.0));
246
+        when(standardService.getStandard("factoryWater", "ecoli")).thenReturn(buildStandard("ecoli", null, 0.0));
247
+        when(standardService.getStandard("factoryWater", "colonyCount")).thenReturn(buildStandard("colonyCount", null, 100.0));
248
+
249
+        ledgerService.evaluateCompliance(sampleRecord);
250
+
251
+        assertEquals("unqualified", sampleRecord.getComplianceStatus());
252
+        assertTrue(sampleRecord.getUnqualifiedItems().contains("余氯"));
253
+        assertTrue(sampleRecord.getUnqualifiedItems().contains("偏高"));
341 254
     }
342 255
 
343 256
     @Test
344
-    @DisplayName("检测计划到期判定")
345
-    void testPlanDueCheck() {
346
-        LocalDate today = LocalDate.of(2026, 6, 14);
347
-
348
-        QualityTestPlan duePlan = new QualityTestPlan();
349
-        duePlan.setStatus("active");
350
-        duePlan.setNextTestDate(LocalDate.of(2026, 6, 14));
351
-        duePlan.setEndDate(null); // 长期
352
-
353
-        QualityTestPlan futurePlan = new QualityTestPlan();
354
-        futurePlan.setStatus("active");
355
-        futurePlan.setNextTestDate(LocalDate.of(2026, 6, 20));
356
-        futurePlan.setEndDate(null);
357
-
358
-        QualityTestPlan expiredPlan = new QualityTestPlan();
359
-        expiredPlan.setStatus("active");
360
-        expiredPlan.setNextTestDate(LocalDate.of(2026, 6, 10));
361
-        expiredPlan.setEndDate(LocalDate.of(2026, 6, 12)); // 已过期
362
-
363
-        List<QualityTestPlan> plans = List.of(duePlan, futurePlan, expiredPlan);
364
-
365
-        // 筛选到期计划: nextTestDate <= today AND (endDate IS NULL OR endDate >= today)
366
-        List<QualityTestPlan> duePlans = plans.stream()
367
-                .filter(p -> "active".equals(p.getStatus()))
368
-                .filter(p -> !p.getNextTestDate().isAfter(today))
369
-                .filter(p -> p.getEndDate() == null || !p.getEndDate().isBefore(today))
370
-                .collect(Collectors.toList());
371
-
372
-        assertEquals(1, duePlans.size(), "只有1个计划到期");
373
-        assertEquals(duePlan, duePlans.get(0));
257
+    @DisplayName("12. 合格判定-无标准时不判定")
258
+    void testEvaluateCompliance_NoStandard() {
259
+        sampleRecord.setWaterType("unknownType");
260
+        when(standardService.getStandard(eq("unknownType"), anyString())).thenReturn(null);
261
+
262
+        ledgerService.evaluateCompliance(sampleRecord);
263
+
264
+        assertEquals("qualified", sampleRecord.getComplianceStatus());
374 265
     }
375 266
 
376
-    // ========== 6. 数据导出格式测试 ==========
267
+    // ==================== 统计测试 ====================
377 268
 
378 269
     @Test
379
-    @DisplayName("检测类型格式化")
380
-    void testTestTypeFormatting() {
381
-        assertEquals("常规检测", formatTestType("routine"));
382
-        assertEquals("专项检测", formatTestType("special"));
383
-        assertEquals("投诉检测", formatTestType("complaint"));
384
-        assertEquals("", formatTestType(null));
270
+    @DisplayName("13. 获取统计数据")
271
+    void testGetStatistics() {
272
+        List<Map<String, Object>> statusStats = List.of(
273
+                Map.of("status", "qualified", "count", 80L),
274
+                Map.of("status", "unqualified", "count", 15L),
275
+                Map.of("status", "pending", "count", 5L)
276
+        );
277
+        when(recordMapper.statByComplianceStatus(any(), any())).thenReturn(statusStats);
278
+        when(recordMapper.statRateByWaterType(any(), any())).thenReturn(Collections.emptyList());
279
+        when(recordMapper.statRateByArea(any(), any())).thenReturn(Collections.emptyList());
280
+        when(recordMapper.statMonthlyTrend(any(), any())).thenReturn(Collections.emptyList());
281
+        when(recordMapper.statUnqualifiedByParam(any(), any())).thenReturn(Collections.emptyList());
282
+        when(recordMapper.statParamAvg()).thenReturn(Map.of(
283
+                "avgTurbidity", 0.45, "avgPh", 7.1, "avgResidualChlorine", 0.6
284
+        ));
285
+
286
+        QualityStatVO vo = ledgerService.getStatistics(null, null);
287
+
288
+        assertNotNull(vo);
289
+        assertEquals(100L, vo.getTotalRecords());
290
+        assertEquals(80L, vo.getQualifiedCount());
291
+        assertEquals(15L, vo.getUnqualifiedCount());
292
+        assertEquals(5L, vo.getPendingCount());
293
+        assertEquals(80.0, vo.getQualifiedRate());
385 294
     }
386 295
 
387 296
     @Test
388
-    @DisplayName("水样类型格式化")
389
-    void testWaterTypeFormatting() {
390
-        assertEquals("原水", formatWaterType("raw"));
391
-        assertEquals("出厂水", formatWaterType("treated"));
392
-        assertEquals("管网末梢水", formatWaterType("network"));
393
-        assertEquals("", formatWaterType(null));
297
+    @DisplayName("14. 统计-空数据")
298
+    void testGetStatistics_Empty() {
299
+        when(recordMapper.statByComplianceStatus(any(), any())).thenReturn(Collections.emptyList());
300
+        when(recordMapper.statRateByWaterType(any(), any())).thenReturn(Collections.emptyList());
301
+        when(recordMapper.statRateByArea(any(), any())).thenReturn(Collections.emptyList());
302
+        when(recordMapper.statMonthlyTrend(any(), any())).thenReturn(Collections.emptyList());
303
+        when(recordMapper.statUnqualifiedByParam(any(), any())).thenReturn(Collections.emptyList());
304
+        when(recordMapper.statParamAvg()).thenReturn(null);
305
+
306
+        QualityStatVO vo = ledgerService.getStatistics(null, null);
307
+
308
+        assertEquals(0L, vo.getTotalRecords());
309
+        assertEquals(0.0, vo.getQualifiedRate());
394 310
     }
395 311
 
312
+    // ==================== 辅助方法测试 ====================
313
+
396 314
     @Test
397
-    @DisplayName("合格状态格式化")
398
-    void testComplianceFormatting() {
399
-        assertEquals("合格", formatCompliance("qualified"));
400
-        assertEquals("不合格", formatCompliance("unqualified"));
401
-        assertEquals("待判定", formatCompliance("pending"));
402
-        assertEquals("待判定", formatCompliance(null));
403
-    }
315
+    @DisplayName("15. 获取区域列表")
316
+    void testGetAreaList() {
317
+        QualityTestRecord r1 = new QualityTestRecord();
318
+        r1.setArea("城区A");
319
+        QualityTestRecord r2 = new QualityTestRecord();
320
+        r2.setArea("城区B");
321
+        when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2));
404 322
 
405
-    // ========== Helper Methods ==========
406
-
407
-    private List<QualityStandard> buildDefaultStandards() {
408
-        List<QualityStandard> standards = new ArrayList<>();
409
-
410
-        // 浊度 - 出厂水 ≤1.0
411
-        QualityStandard s1 = new QualityStandard();
412
-        s1.setParamName("turbidity"); s1.setMinValue(null); s1.setMaxValue(new BigDecimal("1.0"));
413
-        s1.setWaterType("treated");
414
-        standards.add(s1);
415
-
416
-        // 浊度 - 管网 ≤3.0
417
-        QualityStandard s1n = new QualityStandard();
418
-        s1n.setParamName("turbidity"); s1n.setMinValue(null); s1n.setMaxValue(new BigDecimal("3.0"));
419
-        s1n.setWaterType("network");
420
-        standards.add(s1n);
421
-
422
-        // pH - 6.5~8.5
423
-        QualityStandard s2 = new QualityStandard();
424
-        s2.setParamName("ph"); s2.setMinValue(new BigDecimal("6.5")); s2.setMaxValue(new BigDecimal("8.5"));
425
-        s2.setWaterType("all");
426
-        standards.add(s2);
427
-
428
-        // 余氯 - 出厂水 0.3~2.0
429
-        QualityStandard s3 = new QualityStandard();
430
-        s3.setParamName("residual_chlorine"); s3.setMinValue(new BigDecimal("0.3"));
431
-        s3.setMaxValue(new BigDecimal("2.0")); s3.setWaterType("treated");
432
-        standards.add(s3);
433
-
434
-        // 余氯 - 管网 0.05~2.0
435
-        QualityStandard s3n = new QualityStandard();
436
-        s3n.setParamName("residual_chlorine"); s3n.setMinValue(new BigDecimal("0.05"));
437
-        s3n.setMaxValue(new BigDecimal("2.0")); s3n.setWaterType("network");
438
-        standards.add(s3n);
439
-
440
-        // 色度 ≤15
441
-        QualityStandard s4 = new QualityStandard();
442
-        s4.setParamName("color"); s4.setMinValue(null); s4.setMaxValue(new BigDecimal("15"));
443
-        s4.setWaterType("all");
444
-        standards.add(s4);
445
-
446
-        // 嗅味 ≤2
447
-        QualityStandard s5 = new QualityStandard();
448
-        s5.setParamName("odor"); s5.setMinValue(null); s5.setMaxValue(new BigDecimal("2"));
449
-        s5.setWaterType("all");
450
-        standards.add(s5);
451
-
452
-        // 大肠杆菌 =0
453
-        QualityStandard s6 = new QualityStandard();
454
-        s6.setParamName("ecoli"); s6.setMinValue(null); s6.setMaxValue(BigDecimal.ZERO);
455
-        s6.setWaterType("all");
456
-        standards.add(s6);
457
-
458
-        // 菌落总数 ≤100
459
-        QualityStandard s7 = new QualityStandard();
460
-        s7.setParamName("colony_count"); s7.setMinValue(null); s7.setMaxValue(new BigDecimal("100"));
461
-        s7.setWaterType("all");
462
-        standards.add(s7);
463
-
464
-        return standards;
465
-    }
323
+        List<String> areas = ledgerService.getAreaList();
466 324
 
467
-    /**
468
-     * 模拟合格判定逻辑 (不依赖 Spring 容器)
469
-     */
470
-    private List<String> evaluateCompliance(QualityTestRecord record, List<QualityStandard> standards) {
471
-        String waterType = record.getWaterType();
472
-        if (waterType == null) waterType = "treated";
473
-
474
-        List<String> unqualified = new ArrayList<>();
475
-        String wt = waterType;
476
-
477
-        checkParam("turbidity", record.getTurbidity(), wt, standards, unqualified);
478
-        checkParam("ph", record.getPh(), wt, standards, unqualified);
479
-        checkParam("residual_chlorine", record.getResidualChlorine(), wt, standards, unqualified);
480
-        checkParam("color", record.getColor(), wt, standards, unqualified);
481
-        checkParam("odor", record.getOdor(), wt, standards, unqualified);
482
-        checkParam("ecoli", record.getEcoli(), wt, standards, unqualified);
483
-        checkParam("colony_count", record.getColonyCount(), wt, standards, unqualified);
484
-
485
-        return unqualified;
325
+        assertNotNull(areas);
326
+        assertEquals(2, areas.size());
327
+        assertTrue(areas.contains("城区A"));
328
+        assertTrue(areas.contains("城区B"));
486 329
     }
487 330
 
488
-    private void checkParam(String paramName, BigDecimal value, String waterType,
489
-                            List<QualityStandard> standards, List<String> unqualified) {
490
-        if (value == null) return;
331
+    @Test
332
+    @DisplayName("16. 获取采样点列表")
333
+    void testGetSamplingPointList() {
334
+        QualityTestRecord r1 = new QualityTestRecord();
335
+        r1.setSamplingPoint("采样点A");
336
+        QualityTestRecord r2 = new QualityTestRecord();
337
+        r2.setSamplingPoint("采样点B");
338
+        when(recordMapper.selectList(any())).thenReturn(List.of(r1, r2));
491 339
 
492
-        QualityStandard standard = standards.stream()
493
-                .filter(s -> paramName.equals(s.getParamName()))
494
-                .filter(s -> waterType.equals(s.getWaterType()) || "all".equals(s.getWaterType()))
495
-                .findFirst().orElse(null);
340
+        List<String> points = ledgerService.getSamplingPointList();
496 341
 
497
-        if (standard == null) return;
342
+        assertNotNull(points);
343
+        assertEquals(2, points.size());
344
+    }
498 345
 
499
-        boolean isUnqualified = false;
500
-        if (standard.getMinValue() != null && value.compareTo(standard.getMinValue()) < 0) {
501
-            isUnqualified = true;
502
-        }
503
-        if (standard.getMaxValue() != null && value.compareTo(standard.getMaxValue()) > 0) {
504
-            isUnqualified = true;
505
-        }
346
+    // ==================== 计划测试 ====================
506 347
 
507
-        if (isUnqualified) {
508
-            unqualified.add("{\"param\":\"" + paramName + "\"}");
509
-        }
510
-    }
348
+    @Test
349
+    @DisplayName("17. 创建检测计划")
350
+    void testCreatePlan() {
351
+        QualityTestPlan plan = buildPlan();
352
+        when(planMapper.insert(any())).thenReturn(1);
353
+
354
+        QualityTestPlan result = planService.create(plan);
511 355
 
512
-    private LocalDate calculateNextDate(LocalDate current, String frequency) {
513
-        return switch (frequency) {
514
-            case "daily" -> current.plusDays(1);
515
-            case "weekly" -> current.plusWeeks(1);
516
-            case "monthly" -> current.plusMonths(1);
517
-            default -> current;
518
-        };
356
+        assertEquals("active", result.getStatus());
357
+        assertEquals(0, result.getExecutionCount());
358
+        verify(planMapper).insert(plan);
519 359
     }
520 360
 
521
-    private List<QualityTestRecord> buildMockRecords() {
522
-        List<QualityTestRecord> records = new ArrayList<>();
361
+    @Test
362
+    @DisplayName("18. 标记计划已执行-日检+1天")
363
+    void testMarkPlanExecuted() {
364
+        QualityTestPlan plan = buildPlan();
365
+        plan.setId(1L);
366
+        plan.setNextTestDate(LocalDate.of(2026, 6, 14));
367
+        plan.setExecutionCount(5);
368
+        when(planMapper.selectById(1L)).thenReturn(plan);
369
+        when(planMapper.updateById(any())).thenReturn(1);
523 370
 
524
-        QualityTestRecord r1 = new QualityTestRecord();
525
-        r1.setId(1L); r1.setWaterType("treated"); r1.setArea("一体化水厂");
526
-        r1.setSamplingPoint("出厂水口"); r1.setComplianceStatus("qualified");
527
-        r1.setTestDate(LocalDate.of(2026, 6, 14));
528
-        records.add(r1);
371
+        QualityTestPlan result = planService.markExecuted(1L);
529 372
 
530
-        QualityTestRecord r2 = new QualityTestRecord();
531
-        r2.setId(2L); r2.setWaterType("treated"); r2.setArea("一体化水厂");
532
-        r2.setSamplingPoint("出厂水口"); r2.setComplianceStatus("qualified");
533
-        r2.setTestDate(LocalDate.of(2026, 6, 13));
534
-        records.add(r2);
535
-
536
-        QualityTestRecord r3 = new QualityTestRecord();
537
-        r3.setId(3L); r3.setWaterType("network"); r3.setArea("管网一区");
538
-        r3.setSamplingPoint("末梢点A"); r3.setComplianceStatus("unqualified");
539
-        r3.setTestDate(LocalDate.of(2026, 6, 14));
540
-        records.add(r3);
541
-
542
-        QualityTestRecord r4 = new QualityTestRecord();
543
-        r4.setId(4L); r4.setWaterType("treated"); r4.setArea("二水厂");
544
-        r4.setSamplingPoint("出厂水口"); r4.setComplianceStatus("qualified");
545
-        r4.setTestDate(LocalDate.of(2026, 6, 12));
546
-        records.add(r4);
547
-
548
-        QualityTestRecord r5 = new QualityTestRecord();
549
-        r5.setId(5L); r5.setWaterType("network"); r5.setArea("管网一区");
550
-        r5.setSamplingPoint("末梢点B"); r5.setComplianceStatus("unqualified");
551
-        r5.setTestDate(LocalDate.of(2026, 6, 11));
552
-        records.add(r5);
553
-
554
-        return records;
373
+        assertEquals(6, result.getExecutionCount());
374
+        assertEquals(LocalDate.of(2026, 6, 15), result.getNextTestDate());
555 375
     }
556 376
 
557
-    private String formatTestType(String type) {
558
-        if (type == null) return "";
559
-        return switch (type) {
560
-            case "routine" -> "常规检测";
561
-            case "special" -> "专项检测";
562
-            case "complaint" -> "投诉检测";
563
-            default -> type;
564
-        };
565
-    }
377
+    // ==================== 辅助方法 ====================
566 378
 
567
-    private String formatWaterType(String type) {
568
-        if (type == null) return "";
569
-        return switch (type) {
570
-            case "raw" -> "原水";
571
-            case "treated" -> "出厂水";
572
-            case "network" -> "管网末梢水";
573
-            default -> type;
574
-        };
379
+    private QualityStandard buildStandard(String paramName, Double min, Double max) {
380
+        QualityStandard s = new QualityStandard();
381
+        s.setParamName(paramName);
382
+        s.setMinValue(min);
383
+        s.setMaxValue(max);
384
+        s.setEnabled(true);
385
+        return s;
575 386
     }
576 387
 
577
-    private String formatCompliance(String status) {
578
-        if (status == null) return "待判定";
579
-        return switch (status) {
580
-            case "qualified" -> "合格";
581
-            case "unqualified" -> "不合格";
582
-            case "pending" -> "待判定";
583
-            default -> status;
584
-        };
388
+    private QualityTestPlan buildPlan() {
389
+        QualityTestPlan plan = new QualityTestPlan();
390
+        plan.setPlanName("日检计划");
391
+        plan.setTestType("routine");
392
+        plan.setWaterType("factoryWater");
393
+        plan.setSamplingPoint("出厂水采样点A");
394
+        plan.setArea("城区A");
395
+        plan.setFrequency("daily");
396
+        plan.setStartDate(LocalDate.now());
397
+        return plan;
585 398
     }
586 399
 }