瀏覽代碼

1、PC端-绩效分析报告
2、APP端-统计分析
3、APP端-能力对比-质控分析

chenshudong 1 月之前
父節點
當前提交
5ef3639726
共有 39 個文件被更改,包括 5205 次插入23 次删除
  1. 43 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuizAccuracyAnalysisController.java
  2. 1687 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceDimensionController.java
  3. 871 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceMetricsController.java
  4. 36 0
      airport-check/src/main/java/com/sundot/airport/check/domain/CheckEfficiencyDto.java
  5. 36 0
      airport-check/src/main/java/com/sundot/airport/check/domain/CheckEfficiencyRankDto.java
  6. 21 0
      airport-check/src/main/java/com/sundot/airport/check/service/ICheckEfficiencyService.java
  7. 8 0
      airport-check/src/main/java/com/sundot/airport/check/service/ICheckLargeScreenService.java
  8. 153 0
      airport-check/src/main/java/com/sundot/airport/check/service/impl/CheckEfficiencyServiceImpl.java
  9. 56 0
      airport-check/src/main/java/com/sundot/airport/check/service/impl/CheckLargeScreenServiceImpl.java
  10. 7 0
      airport-common/src/main/java/com/sundot/airport/common/core/domain/BaseLargeScreenQueryParamDto.java
  11. 67 0
      airport-common/src/main/java/com/sundot/airport/common/core/domain/BoxPlotDataDto.java
  12. 42 0
      airport-common/src/main/java/com/sundot/airport/common/core/domain/GroupedBoxPlotDataDto.java
  13. 87 0
      airport-common/src/main/java/com/sundot/airport/common/dto/CategoryAccuracyItemDto.java
  14. 36 0
      airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceDimensionParamDto.java
  15. 51 0
      airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceDimensionResult.java
  16. 39 0
      airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceMetricsParamDto.java
  17. 57 0
      airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceMetricsResultDto.java
  18. 75 0
      airport-common/src/main/java/com/sundot/airport/common/dto/QuizAccuracyAnalysisResultDto.java
  19. 105 0
      airport-common/src/main/java/com/sundot/airport/common/dto/TimeBasedPerformanceResult.java
  20. 44 0
      airport-common/src/main/java/com/sundot/airport/common/utils/DeptUtils.java
  21. 104 0
      airport-common/src/main/java/com/sundot/airport/common/utils/GeometricMeanUtils.java
  22. 29 0
      airport-common/src/main/java/com/sundot/airport/common/utils/LargeScreenDateUtils.java
  23. 15 2
      airport-exam/src/main/java/com/sundot/airport/exam/mapper/DailyTaskDetailMapper.java
  24. 44 2
      airport-exam/src/main/java/com/sundot/airport/exam/service/IAccuracyStatisticsService.java
  25. 280 12
      airport-exam/src/main/java/com/sundot/airport/exam/service/impl/AccuracyStatisticsServiceImpl.java
  26. 29 7
      airport-exam/src/main/resources/mapper/exam/DailyTaskDetailMapper.xml
  27. 25 0
      airport-item/src/main/java/com/sundot/airport/item/domain/ItemLargeScreenWorkDurationDto.java
  28. 51 0
      airport-item/src/main/java/com/sundot/airport/item/domain/SeizureEfficiencyDto.java
  29. 45 0
      airport-item/src/main/java/com/sundot/airport/item/domain/SeizureEfficiencyRankDto.java
  30. 57 0
      airport-item/src/main/java/com/sundot/airport/item/domain/dto/ConcealmentPositionTop1DTO.java
  31. 51 0
      airport-item/src/main/java/com/sundot/airport/item/domain/dto/ProhibitedItemsTop3DTO.java
  32. 64 0
      airport-item/src/main/java/com/sundot/airport/item/mapper/ItemLargeScreenMapper.java
  33. 2 0
      airport-item/src/main/java/com/sundot/airport/item/mapper/SeizureReportMapper.java
  34. 423 0
      airport-item/src/main/java/com/sundot/airport/item/service/SeizureDistributionService.java
  35. 21 0
      airport-item/src/main/java/com/sundot/airport/item/service/SeizureEfficiencyService.java
  36. 28 0
      airport-item/src/main/java/com/sundot/airport/item/service/SeizureReportService.java
  37. 269 0
      airport-item/src/main/java/com/sundot/airport/item/service/impl/SeizureEfficiencyServiceImpl.java
  38. 130 0
      airport-item/src/main/resources/mapper/item/ItemLargeScreenMapper.xml
  39. 17 0
      airport-item/src/main/resources/mapper/item/SeizureReportMapper.xml

+ 43 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuizAccuracyAnalysisController.java

@@ -0,0 +1,43 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.common.core.domain.AjaxResult;
4
+import com.sundot.airport.common.dto.QuizAccuracyAnalysisResultDto;
5
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.format.annotation.DateTimeFormat;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.RequestMapping;
10
+import org.springframework.web.bind.annotation.RequestParam;
11
+import org.springframework.web.bind.annotation.RestController;
12
+
13
+import java.text.SimpleDateFormat;
14
+import java.util.Date;
15
+
16
+/**
17
+ * 抽问抽答正确率分析接口
18
+ */
19
+@RestController
20
+@RequestMapping("/exam/quiz/accuracy-analysis")
21
+public class QuizAccuracyAnalysisController {
22
+
23
+    @Autowired
24
+    private IAccuracyStatisticsService accuracyStatisticsService;
25
+
26
+    /**
27
+     * 获取抽问抽答正确率分析
28
+     * 返回全站平均正确率、各题目分类正确率,并标注最高/薄弱分类
29
+     *
30
+     * @param startDate 开始日期(yyyy-MM-dd)
31
+     * @param endDate   结束日期(yyyy-MM-dd)
32
+     * @return 正确率分析结果
33
+     */
34
+    @GetMapping
35
+    public AjaxResult getQuizAccuracyAnalysis(
36
+            @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
37
+            @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
38
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
39
+        QuizAccuracyAnalysisResultDto result = accuracyStatisticsService.getQuizAccuracyAnalysis(
40
+                sdf.format(startDate), sdf.format(endDate));
41
+        return AjaxResult.success(result);
42
+    }
43
+}

File diff suppressed because it is too large
+ 1687 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceDimensionController.java


+ 871 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceMetricsController.java

@@ -0,0 +1,871 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import com.sundot.airport.check.domain.CheckEfficiencyDto;
4
+import com.sundot.airport.check.domain.CheckEfficiencyRankDto;
5
+import com.sundot.airport.check.service.ICheckEfficiencyService;
6
+import com.sundot.airport.common.core.domain.AjaxResult;
7
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
8
+import com.sundot.airport.common.core.domain.GroupedBoxPlotDataDto;
9
+import com.sundot.airport.common.core.domain.entity.SysDept;
10
+import com.sundot.airport.common.core.domain.entity.SysUser;
11
+import com.sundot.airport.common.dto.PerformanceMetricsParamDto;
12
+import com.sundot.airport.common.dto.PerformanceMetricsResultDto;
13
+import com.sundot.airport.common.enums.DeptTypeEnum;
14
+import com.sundot.airport.common.enums.RoleTypeEnum;
15
+import com.sundot.airport.common.exception.ServiceException;
16
+import com.sundot.airport.common.utils.DeptUtils;
17
+import com.sundot.airport.common.utils.GeometricMeanUtils;
18
+import com.sundot.airport.common.utils.StringUtils;
19
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
20
+import com.sundot.airport.item.domain.SeizureEfficiencyDto;
21
+import com.sundot.airport.item.domain.SeizureEfficiencyRankDto;
22
+import com.sundot.airport.item.domain.dto.ConcealmentPositionTop1DTO;
23
+import com.sundot.airport.item.domain.dto.ProhibitedItemsTop3DTO;
24
+import com.sundot.airport.item.mapper.ItemLargeScreenMapper;
25
+import com.sundot.airport.item.service.SeizureDistributionService;
26
+import com.sundot.airport.item.service.SeizureEfficiencyService;
27
+import com.sundot.airport.system.mapper.SysDeptMapper;
28
+import com.sundot.airport.system.mapper.SysUserMapper;
29
+import com.sundot.airport.system.service.ISysDeptService;
30
+import lombok.extern.slf4j.Slf4j;
31
+import org.springframework.beans.factory.annotation.Autowired;
32
+import org.springframework.format.annotation.DateTimeFormat;
33
+import org.springframework.web.bind.annotation.*;
34
+
35
+import java.math.BigDecimal;
36
+import java.math.RoundingMode;
37
+import java.text.SimpleDateFormat;
38
+import java.util.*;
39
+import java.util.stream.Collectors;
40
+
41
+import static com.sundot.airport.common.core.domain.AjaxResult.success;
42
+import static com.sundot.airport.common.utils.SecurityUtils.getDeptId;
43
+
44
+/**
45
+ * 绩效指标Controller
46
+ *
47
+ * @author ruoyi
48
+ * @date 2025-07-25
49
+ */
50
+@Slf4j
51
+@RestController
52
+@RequestMapping("/item/performance")
53
+public class PerformanceMetricsController {
54
+
55
+
56
+    @Autowired
57
+    private SeizureEfficiencyService seizureEfficiencyService;
58
+
59
+    @Autowired
60
+    private ICheckEfficiencyService checkEfficiencyService;
61
+
62
+
63
+    @Autowired
64
+    private SeizureDistributionService seizureDistributionService;
65
+
66
+    @Autowired
67
+    private ISysDeptService sysDeptService;
68
+
69
+
70
+    @Autowired
71
+    private SysUserMapper userMapper;
72
+
73
+    @Autowired
74
+    private SysDeptMapper deptMapper;
75
+
76
+    @Autowired
77
+    private IAccuracyStatisticsService accuracyStatisticsService;
78
+
79
+    @Autowired
80
+    private ItemLargeScreenMapper itemLargeScreenMapper;
81
+
82
+    //先写死,后期可能会变
83
+    private static final double a = 3;
84
+    private static final double b = 5;
85
+    private static final double c = 2;
86
+
87
+
88
+    /**
89
+     * 查询查获效率统计
90
+     */
91
+    @GetMapping("/list")
92
+    public AjaxResult list(BaseLargeScreenQueryParamDto dto) {
93
+        SeizureEfficiencyDto result = seizureEfficiencyService.getSeizureEfficiency(dto);
94
+        return success(result);
95
+    }
96
+
97
+    /**
98
+     * 获取查获数量分布直方图数据
99
+     *
100
+     * @return 直方图数据列表,每个元素包含:组下限、组上限、该组内安检员人数
101
+     */
102
+    @GetMapping("/histogram")
103
+    public AjaxResult getSeizureHistogramData(BaseLargeScreenQueryParamDto dto) {
104
+        // 获取当前用户所在站点ID
105
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
106
+        if (topSiteId == null) {
107
+            return AjaxResult.error("无法找到有效的站点信息");
108
+        }
109
+        dto.setInspectStationId(topSiteId);
110
+        // 计算查获数量分布
111
+        List<SeizureDistributionService.SeizureHistogramData> result =
112
+                seizureDistributionService.calculateSeizureDistribution(dto);
113
+
114
+        return success(result);
115
+    }
116
+
117
+
118
+    /**
119
+     * 获取查获数量箱线图数据
120
+     *
121
+     * @return 箱线图数据,包含最小观察值、Q1、中位数、Q3、最大观察值、离群点等信息
122
+     */
123
+    @GetMapping("/boxplot")
124
+    public AjaxResult getSeizureBoxPlotData(BaseLargeScreenQueryParamDto dto) {
125
+        // 获取当前用户所在站点ID
126
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
127
+        if (topSiteId == null) {
128
+            return AjaxResult.error("无法找到有效的站点信息");
129
+        }
130
+        dto.setInspectStationId(topSiteId);
131
+        // 计算查获数量箱线图数据
132
+        List<GroupedBoxPlotDataDto> result = seizureDistributionService.calculateBoxPlotData(dto);
133
+
134
+        return success(result);
135
+    }
136
+
137
+    /**
138
+     * 查询全站查获违禁品 TOP3
139
+     *
140
+     * @param startDate 查询开始时间
141
+     *                  endDate 查询结束时间
142
+     * @return 违禁品 TOP3 数据,包含物品名称、查获数量、占比
143
+     */
144
+    @GetMapping("/prohibited-top3")
145
+    public AjaxResult getProhibitedItemsTop3(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
146
+                                             @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
147
+        try {
148
+            BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
149
+            dto.setStartDate(startDate);
150
+            dto.setEndDate(endDate);
151
+            // 获取当前用户所在站点 ID
152
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
153
+            if (topSiteId == null) {
154
+                return AjaxResult.error("无法找到有效的站点信息");
155
+            }
156
+            dto.setInspectStationId(topSiteId);
157
+
158
+            // 查询违禁品 TOP3
159
+            List<ProhibitedItemsTop3DTO> top3List = itemLargeScreenMapper.selectProhibitedItemsTop3(dto);
160
+
161
+            // 计算总数和占比
162
+            BigDecimal totalQuantity = itemLargeScreenMapper.getTotalSum(dto);
163
+            for (int i = 0; i < top3List.size(); i++) {
164
+                ProhibitedItemsTop3DTO item = top3List.get(i);
165
+                item.setRank(i + 1);
166
+
167
+                // 计算占比
168
+                if (totalQuantity.compareTo(BigDecimal.ZERO) > 0) {
169
+                    BigDecimal percentage = item.getQuantity()
170
+                            .multiply(new BigDecimal("100"))
171
+                            .divide(totalQuantity, 2, RoundingMode.HALF_UP);
172
+                    item.setPercentage(percentage);
173
+                } else {
174
+                    item.setPercentage(BigDecimal.ZERO);
175
+                }
176
+            }
177
+
178
+            return success(top3List);
179
+        } catch (Exception e) {
180
+            log.error("查询全站查获违禁品 TOP3 失败:{}", e.getMessage());
181
+            return AjaxResult.error("查询失败:" + e.getMessage());
182
+        }
183
+    }
184
+
185
+    /**
186
+     * 查询隐匿重点部位 Top1
187
+     *
188
+     * @param startDate 查询开始时间
189
+     *                  endDate 查询结束时间
190
+     * @return 隐匿重点部位 Top1 数据
191
+     */
192
+    @GetMapping("/concealment-position-top1")
193
+    public AjaxResult getConcealmentPositionTop1(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
194
+                                                 @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
195
+        try {
196
+            BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
197
+            dto.setStartDate(startDate);
198
+            dto.setEndDate(endDate);
199
+            // 获取当前用户所在站点 ID
200
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
201
+            if (topSiteId == null) {
202
+                return AjaxResult.error("无法找到有效的站点信息");
203
+            }
204
+            dto.setInspectStationId(topSiteId);
205
+
206
+            // 查询隐匿重点部位 Top1
207
+            List<ConcealmentPositionTop1DTO> top1List = itemLargeScreenMapper.selectConcealmentPositionTop1(dto);
208
+
209
+            if (top1List == null || top1List.isEmpty()) {
210
+                return success(new ArrayList<>());
211
+            }
212
+
213
+            return success(top1List);
214
+        } catch (Exception e) {
215
+            log.error("查询隐匿重点部位 Top1 失败:{}", e.getMessage());
216
+            return AjaxResult.error("查询失败:" + e.getMessage());
217
+        }
218
+    }
219
+
220
+    /**
221
+     * 绩效指标列表
222
+     *
223
+     * @param param 查询参数
224
+     * @return 组织/姓名、查获效率、巡检合格率、培训得分、总分
225
+     */
226
+    @PostMapping("/metrics")
227
+    public AjaxResult getPerformanceMetrics(@RequestBody PerformanceMetricsParamDto param) {
228
+        try {
229
+            // 参数校验和默认值设置
230
+            validateAndSetDefaults(param);
231
+
232
+            List<PerformanceMetricsResultDto> result = calculatePerformanceMetrics(param);
233
+
234
+            // 填充科室和班级信息
235
+            enrichDeptAndClassInfo(result, param);
236
+
237
+            sortAndRank(result, param);
238
+
239
+            return success(result);
240
+        } catch (Exception e) {
241
+            return AjaxResult.error("获取绩效指标失败: " + e.getMessage());
242
+        }
243
+    }
244
+
245
+
246
+    /**
247
+     * 填充科室和班级信息
248
+     */
249
+    private void enrichDeptAndClassInfo(List<PerformanceMetricsResultDto> result, PerformanceMetricsParamDto param) {
250
+        // 只处理人员维度和班组维度
251
+        if (param.getDimension() == null || param.getDimension() == 3) {
252
+            return;
253
+        }
254
+
255
+
256
+        if (result == null || result.isEmpty()) {
257
+            return;
258
+        }
259
+
260
+        // 收集所有需要查询的 ID
261
+        Set<Long> ids = result.stream()
262
+                .map(PerformanceMetricsResultDto::getId)
263
+                .collect(Collectors.toSet());
264
+
265
+        // 预加载这些 ID 的信息
266
+        Map<Long, SysUser> userMap = new HashMap<>();
267
+        Map<Long, SysDept> deptMap = new HashMap<>();
268
+
269
+        if (param.getDimension() == 1) {
270
+            // 人员维度:加载用户和部门信息
271
+            for (Long id : ids) {
272
+                SysUser user = userMapper.selectUserById(id);
273
+                if (user != null) {
274
+                    userMap.put(id, user);
275
+                }
276
+            }
277
+            // 加载所有部门信息用于查找班组
278
+            List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId())));
279
+            for (SysDept dept : allDepts) {
280
+                deptMap.put(dept.getDeptId(), dept);
281
+            }
282
+        } else if (param.getDimension() == 2 || param.getDimension() == 3) {
283
+            // 班组维度:加载部门信息
284
+            List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId())));
285
+            for (SysDept dept : allDepts) {
286
+                deptMap.put(dept.getDeptId(), dept);
287
+            }
288
+        }
289
+
290
+        // 为所有时间点的数据填充科室和班级信息
291
+        for (PerformanceMetricsResultDto item : result) {
292
+            if (param.getDimension() == 1) {
293
+                // 人员维度
294
+                SysUser user = userMap.get(item.getId());
295
+                if (user != null) {
296
+                    Long deptId = user.getDeptId();
297
+                    if (deptId != null) {
298
+                        SysDept dept = deptMap.get(deptId);
299
+                        if (dept != null) {
300
+                            item.setClassName(dept.getDeptName());
301
+                            if (dept.getParentId() != null) {
302
+                                SysDept parentDept = deptMap.get(dept.getParentId());
303
+                                if (parentDept != null) {
304
+                                    item.setDeptName(parentDept.getDeptName());
305
+                                    if (parentDept.getParentId() != null) {
306
+                                        SysDept brigadeDept = deptMap.get(parentDept.getParentId());
307
+                                        if (brigadeDept != null) {
308
+                                            item.setBrigadeName(brigadeDept.getDeptName());
309
+                                        }
310
+                                    }
311
+                                }
312
+                            }
313
+                        }
314
+                    }
315
+                }
316
+            } else if (param.getDimension() == 2) {
317
+                // 班组维度
318
+                SysDept dept = deptMap.get(item.getId());
319
+                if (dept != null) {
320
+                    if (dept.getParentId() != null) {
321
+                        SysDept parentDept = deptMap.get(dept.getParentId());
322
+                        if (parentDept != null) {
323
+                            item.setDeptName(parentDept.getDeptName());
324
+                            if (parentDept.getParentId() != null) {
325
+                                SysDept brigadeDept = deptMap.get(parentDept.getParentId());
326
+                                if (brigadeDept != null) {
327
+                                    item.setBrigadeName(brigadeDept.getDeptName());
328
+                                }
329
+                            }
330
+                        }
331
+                    }
332
+                }
333
+            } else if (param.getDimension() == 3) {
334
+                // 科室维度
335
+                SysDept dept = deptMap.get(item.getId());
336
+                if (dept != null) {
337
+                    if (dept.getParentId() != null) {
338
+                        SysDept parentDept = deptMap.get(dept.getParentId());
339
+                        if (parentDept != null) {
340
+                            item.setBrigadeName(parentDept.getDeptName());
341
+                        }
342
+                    }
343
+                }
344
+            }
345
+        }
346
+    }
347
+
348
+    /**
349
+     * 参数校验和设置默认值
350
+     */
351
+    private void validateAndSetDefaults(PerformanceMetricsParamDto param) {
352
+        if (param.getDimension() == null) {
353
+            param.setDimension(1); // 默认个人维度
354
+        }
355
+        if (param.getSortOrder() == null) {
356
+            param.setSortOrder(2); // 默认降序
357
+        }
358
+    }
359
+
360
+    /**
361
+     * 计算绩效指标
362
+     */
363
+    private List<PerformanceMetricsResultDto> calculatePerformanceMetrics(PerformanceMetricsParamDto param) {
364
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
365
+
366
+        if (param.getDimension() == 1) {
367
+            // 个人维度
368
+            result = calculatePersonalMetrics(param);
369
+        } else if (param.getDimension() == 2) {
370
+            // 班组维度
371
+            result = calculateTeamMetrics(param);
372
+        } else if (param.getDimension() == 3) {
373
+            // 科室维度
374
+            result = calculateDepartmentMetrics(param);
375
+        } else if (param.getDimension() == 4) {
376
+            // 大队维度
377
+            result = calculateBrigadeMetrics(param);
378
+        }
379
+
380
+        return result;
381
+    }
382
+
383
+    /**
384
+     * 计算查获效率
385
+     *
386
+     * @param param
387
+     * @return
388
+     */
389
+    private List<SeizureEfficiencyRankDto> calculateSeizureEfficiency(PerformanceMetricsParamDto param) {
390
+        List<SeizureEfficiencyRankDto> rankList = new ArrayList<>();
391
+
392
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
393
+        dto.setStartDate(param.getStartTime());
394
+        dto.setEndDate(param.getEndTime());
395
+        SeizureEfficiencyDto result = seizureEfficiencyService.getSeizureEfficiency(dto);
396
+        if (param.getDimension() == 1) {
397
+            // 个人维度
398
+            rankList = result.getIndividualRankList();
399
+        } else if (param.getDimension() == 2) {
400
+            // 班组维度
401
+            rankList = result.getTeamRankList();
402
+        } else if (param.getDimension() == 3) {
403
+            // 科室维度
404
+            rankList = result.getDepartmentRankList();
405
+        } else if (param.getDimension() == 4) {
406
+            // 大队维度
407
+            rankList = result.getBrigadeRankList();
408
+        }
409
+        return rankList;
410
+    }
411
+
412
+
413
+    /**
414
+     * 计算个人绩效指标
415
+     */
416
+    private List<PerformanceMetricsResultDto> calculatePersonalMetrics(PerformanceMetricsParamDto param) {
417
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
418
+
419
+        // 获取当前用户所在站点ID
420
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
421
+        if (topSiteId == null) {
422
+            throw new ServiceException("无法找到有效的站点信息");
423
+        }
424
+        List<SysUser> sysUserTempList = userMapper.selectUserByDeptId(topSiteId);
425
+        List<SysUser> users = sysUserTempList.stream().filter(item -> item.getRoles().stream().anyMatch(sysRole -> RoleTypeEnum.banzuzhang.getCode().equals(sysRole.getRoleKey()) || RoleTypeEnum.SecurityCheck.getCode().equals(sysRole.getRoleKey()))).collect(Collectors.toList());
426
+
427
+        if (users.isEmpty()) return result;
428
+
429
+        //查获效率
430
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
431
+        //巡检合格率
432
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
433
+
434
+        // 批量查询抽问抽答正确率
435
+        List<Long> userIds = users.stream().map(SysUser::getUserId).collect(Collectors.toList());
436
+        String startDate = formatDate(param.getStartTime());
437
+        String endDate = formatDate(param.getEndTime());
438
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getUsersAccuracyMap(userIds, startDate, endDate);
439
+
440
+        // 批量查询指标数据
441
+        List<MetricData> metricDataList = new ArrayList<>();
442
+        for (SysUser user : users) {
443
+            try {
444
+                MetricData data = new MetricData();
445
+                data.id = user.getUserId();
446
+                data.name = user.getNickName();
447
+                data.seizureEfficiency = queryPersonalSeizureEfficiency(user.getUserId(), rankList);
448
+                data.inspectionPassRate = queryPersonalInspectionRate(user.getUserId(), checkRankList);
449
+                data.trainingScore = queryPersonalTrainingScore(user.getUserId(), accuracyMap);
450
+                metricDataList.add(data);
451
+            } catch (Exception e) {
452
+                log.error("查询用户{}指标失败:{}", user.getUserName(), e.getMessage());
453
+            }
454
+        }
455
+
456
+        // 集体处理
457
+        return normalizeAndCreateResult(metricDataList);
458
+    }
459
+
460
+    /**
461
+     * 计算班组绩效指标
462
+     */
463
+    private List<PerformanceMetricsResultDto> calculateTeamMetrics(PerformanceMetricsParamDto param) {
464
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
465
+        // 查询所有班组
466
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
467
+        List<SysDept> teams = sysDeptService.selectChildrenDeptById(topSiteId).stream()
468
+                .filter(dept -> DeptTypeEnum.TEAMS.getCode().equals(dept.getDeptType()))
469
+                .collect(Collectors.toList());
470
+
471
+        if (teams.isEmpty()) return result;
472
+
473
+        //查获效率
474
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
475
+        //巡检合格率
476
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
477
+
478
+        // 批量查询抽问抽答正确率
479
+        List<Long> teamIds = teams.stream().map(SysDept::getDeptId).collect(Collectors.toList());
480
+        String startDate = formatDate(param.getStartTime());
481
+        String endDate = formatDate(param.getEndTime());
482
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getTeamsAccuracyMap(teamIds, startDate, endDate);
483
+
484
+        // 批量查询指标数据
485
+        List<MetricData> metricDataList = new ArrayList<>();
486
+        for (SysDept team : teams) {
487
+            try {
488
+                MetricData data = new MetricData();
489
+                data.id = team.getDeptId();
490
+                data.name = team.getDeptName();
491
+                data.seizureEfficiency = queryTeamSeizureEfficiency(team.getDeptId(), rankList);
492
+                data.inspectionPassRate = queryTeamInspectionRate(team.getDeptId(), checkRankList);
493
+                data.trainingScore = queryTeamTrainingScore(team.getDeptId(), accuracyMap);
494
+                metricDataList.add(data);
495
+            } catch (Exception e) {
496
+                log.error("查询班组{}指标失败:{}", team.getDeptName(), e.getMessage());
497
+            }
498
+        }
499
+
500
+        // 集体处理
501
+        return normalizeAndCreateResult(metricDataList);
502
+    }
503
+
504
+    /**
505
+     * 计算科室绩效指标
506
+     */
507
+    private List<PerformanceMetricsResultDto> calculateDepartmentMetrics(PerformanceMetricsParamDto param) {
508
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
509
+
510
+        // 查询所有科室
511
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
512
+        List<SysDept> departments = sysDeptService.selectChildrenDeptById(topSiteId).stream()
513
+                .filter(dept -> DeptTypeEnum.MANAGER.getCode().equals(dept.getDeptType()))
514
+                .collect(Collectors.toList());
515
+
516
+        if (departments.isEmpty()) return result;
517
+
518
+        //查获效率
519
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
520
+        //巡检合格率
521
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
522
+
523
+        // 批量查询抽问抽答正确率
524
+        List<Long> deptIds = departments.stream().map(SysDept::getDeptId).collect(Collectors.toList());
525
+        String startDate = formatDate(param.getStartTime());
526
+        String endDate = formatDate(param.getEndTime());
527
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getDeptsAccuracyMap(deptIds, startDate, endDate);
528
+
529
+        // 批量查询指标数据
530
+        List<MetricData> metricDataList = new ArrayList<>();
531
+        for (SysDept dept : departments) {
532
+            try {
533
+                MetricData data = new MetricData();
534
+                data.id = dept.getDeptId();
535
+                data.name = dept.getDeptName();
536
+                data.seizureEfficiency = queryDepartmentSeizureEfficiency(dept.getDeptId(), rankList);
537
+                data.inspectionPassRate = queryDepartmentInspectionRate(dept.getDeptId(), checkRankList);
538
+                data.trainingScore = queryDepartmentTrainingScore(dept.getDeptId(), accuracyMap);
539
+                metricDataList.add(data);
540
+            } catch (Exception e) {
541
+                log.error("查询科室{}指标失败:{}", dept.getDeptName(), e.getMessage());
542
+            }
543
+        }
544
+
545
+        // 集体处理
546
+        return normalizeAndCreateResult(metricDataList);
547
+    }
548
+
549
+    /**
550
+     * 计算大队绩效指标
551
+     */
552
+    private List<PerformanceMetricsResultDto> calculateBrigadeMetrics(PerformanceMetricsParamDto param) {
553
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
554
+
555
+        // 查询所有科室
556
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
557
+        List<SysDept> brigades = sysDeptService.selectChildrenDeptById(topSiteId).stream()
558
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
559
+                .collect(Collectors.toList());
560
+
561
+        if (brigades.isEmpty()) return result;
562
+
563
+        //查获效率
564
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
565
+        //巡检合格率
566
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
567
+
568
+        // 批量查询抽问抽答正确率
569
+        List<Long> deptIds = brigades.stream().map(SysDept::getDeptId).collect(Collectors.toList());
570
+        String startDate = formatDate(param.getStartTime());
571
+        String endDate = formatDate(param.getEndTime());
572
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getDeptsAccuracyMap(deptIds, startDate, endDate);
573
+
574
+        // 批量查询指标数据
575
+        List<MetricData> metricDataList = new ArrayList<>();
576
+        for (SysDept dept : brigades) {
577
+            try {
578
+                MetricData data = new MetricData();
579
+                data.id = dept.getDeptId();
580
+                data.name = dept.getDeptName();
581
+                data.seizureEfficiency = queryDepartmentSeizureEfficiency(dept.getDeptId(), rankList);
582
+                data.inspectionPassRate = queryDepartmentInspectionRate(dept.getDeptId(), checkRankList);
583
+                data.trainingScore = queryDepartmentTrainingScore(dept.getDeptId(), accuracyMap);
584
+                metricDataList.add(data);
585
+            } catch (Exception e) {
586
+                log.error("查询科室{}指标失败:{}", dept.getDeptName(), e.getMessage());
587
+            }
588
+        }
589
+
590
+        // 集体处理
591
+        return normalizeAndCreateResult(metricDataList);
592
+    }
593
+
594
+    /**
595
+     * 集体处理
596
+     */
597
+    private List<PerformanceMetricsResultDto> normalizeAndCreateResult(List<MetricData> dataList) {
598
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
599
+
600
+        if (dataList.isEmpty()) return result;
601
+
602
+        // 标准化各项指标
603
+        List<NormalizedData> normalizedList = new ArrayList<>();
604
+        for (MetricData data : dataList) {
605
+            NormalizedData normalized = new NormalizedData();
606
+            normalized.id = data.id;
607
+            normalized.name = data.name;
608
+            normalized.seizureEfficiency = normalizeValue(data.seizureEfficiency, "w1");
609
+            normalized.inspectionPassRate = normalizeValue(data.inspectionPassRate, "w2");
610
+            normalized.trainingScore = normalizeValue(data.trainingScore, "w3");
611
+            // 总分 = 查获效率 + 巡检合格率 + 抽问抽答正确率
612
+            normalized.totalScore = normalized.seizureEfficiency
613
+                    .add(normalized.inspectionPassRate)
614
+                    .add(normalized.trainingScore);
615
+            normalizedList.add(normalized);
616
+        }
617
+
618
+        // 转换为结果对象
619
+        for (NormalizedData data : normalizedList) {
620
+            PerformanceMetricsResultDto dto = new PerformanceMetricsResultDto();
621
+            dto.setId(data.id);
622
+            dto.setName(data.name);
623
+            dto.setSeizureEfficiency(data.seizureEfficiency);
624
+            dto.setInspectionPassRate(data.inspectionPassRate);
625
+            dto.setTrainingScore(data.trainingScore);
626
+            dto.setTotalScore(data.totalScore);
627
+            result.add(dto);
628
+        }
629
+
630
+        return result;
631
+    }
632
+
633
+    /**
634
+     * 标准化值处理
635
+     */
636
+    private BigDecimal normalizeValue(BigDecimal value, String w) {
637
+        BigDecimal bigDecimal = value != null ? value : BigDecimal.ZERO;
638
+        GeometricMeanUtils.CalculationResult calculationResult = GeometricMeanUtils.calculateWithWeights(a, b, c);
639
+        double d = 0;
640
+        if ("w1".equals(w)) {
641
+            d = calculationResult.getW1();
642
+        }
643
+        if ("w2".equals(w)) {
644
+            d = calculationResult.getW2();
645
+        }
646
+        if ("w3".equals(w)) {
647
+            d = calculationResult.getW3();
648
+        }
649
+        return bigDecimal.multiply(new BigDecimal(d)).setScale(4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
650
+    }
651
+
652
+    /**
653
+     * 排序和设置排名
654
+     */
655
+    private void sortAndRank(List<PerformanceMetricsResultDto> result, PerformanceMetricsParamDto param) {
656
+        // 排序
657
+        String sortField = param.getSortField();
658
+        if (sortField == null) sortField = "totalScore";
659
+
660
+        Comparator<PerformanceMetricsResultDto> comparator;
661
+        switch (sortField) {
662
+            case "seizureEfficiency":
663
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getSeizureEfficiency,
664
+                        Comparator.nullsFirst(BigDecimal::compareTo));
665
+                break;
666
+            case "inspectionPassRate":
667
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getInspectionPassRate,
668
+                        Comparator.nullsFirst(BigDecimal::compareTo));
669
+                break;
670
+            case "trainingScore":
671
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getTrainingScore,
672
+                        Comparator.nullsFirst(BigDecimal::compareTo));
673
+                break;
674
+            default: // totalScore
675
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getTotalScore,
676
+                        Comparator.nullsFirst(BigDecimal::compareTo));
677
+                break;
678
+        }
679
+
680
+        if (Integer.valueOf(2).equals(param.getSortOrder())) {
681
+            comparator = comparator.reversed(); // 降序
682
+        }
683
+
684
+        result.sort(comparator);
685
+
686
+        // 设置排名
687
+        for (int i = 0; i < result.size(); i++) {
688
+            result.get(i).setRank(i + 1);
689
+        }
690
+    }
691
+
692
+    /**
693
+     * 实现个人查获效率查询
694
+     * 个人查获效率=(个人查获效率-个人最低查获效率)/(个人最高查获效率-个人最低查获效率)
695
+     */
696
+    private BigDecimal queryPersonalSeizureEfficiency(Long userId, List<SeizureEfficiencyRankDto> rankList) {
697
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(userId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
698
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
699
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
700
+        BigDecimal difference = max.subtract(min);
701
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
702
+        return normalized;
703
+    }
704
+
705
+    /**
706
+     * 实现个人巡检合格率查询
707
+     *
708
+     * @param userId
709
+     * @param rankList
710
+     * @return
711
+     */
712
+    private BigDecimal queryPersonalInspectionRate(Long userId, List<CheckEfficiencyRankDto> rankList) {
713
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(userId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
714
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
715
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
716
+        BigDecimal difference = max.subtract(min);
717
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
718
+        return normalized;
719
+    }
720
+
721
+    /**
722
+     * 实现个人抽问抽答正确率查询
723
+     * 归一化公式:(当前值 - min) / (max - min)
724
+     */
725
+    private BigDecimal queryPersonalTrainingScore(Long userId, Map<Long, BigDecimal> accuracyMap) {
726
+        return normalizeFromMap(userId, accuracyMap);
727
+    }
728
+
729
+    /**
730
+     * 实现班组查获效率查询
731
+     * 组织查获效率=(组织查获效率-组织最低查获效率)/(组织最高查获效率-组织最低查获效率)
732
+     */
733
+    private BigDecimal queryTeamSeizureEfficiency(Long teamId, List<SeizureEfficiencyRankDto> rankList) {
734
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(teamId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
735
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
736
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
737
+        BigDecimal difference = max.subtract(min);
738
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
739
+        return normalized;
740
+    }
741
+
742
+    /**
743
+     * 实现班组巡检合格率查询
744
+     */
745
+    private BigDecimal queryTeamInspectionRate(Long teamId, List<CheckEfficiencyRankDto> rankList) {
746
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(teamId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
747
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
748
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
749
+        BigDecimal difference = max.subtract(min);
750
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
751
+        return normalized;
752
+    }
753
+
754
+    /**
755
+     * 实现班组抽问抽答正确率查询
756
+     * 归一化公式:(当前值 - min) / (max - min)
757
+     */
758
+    private BigDecimal queryTeamTrainingScore(Long teamId, Map<Long, BigDecimal> accuracyMap) {
759
+        return normalizeFromMap(teamId, accuracyMap);
760
+    }
761
+
762
+    /**
763
+     * 实现科室查获效率查询
764
+     * 组织查获效率=(组织查获效率-组织最低查获效率)/(组织最高查获效率-组织最低查获效率)
765
+     */
766
+    private BigDecimal queryDepartmentSeizureEfficiency(Long deptId, List<SeizureEfficiencyRankDto> rankList) {
767
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(deptId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
768
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
769
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
770
+        BigDecimal difference = max.subtract(min);
771
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
772
+        return normalized;
773
+    }
774
+
775
+    /**
776
+     * 实现科室巡检合格率查询
777
+     */
778
+    private BigDecimal queryDepartmentInspectionRate(Long deptId, List<CheckEfficiencyRankDto> rankList) {
779
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(deptId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
780
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
781
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
782
+        BigDecimal difference = max.subtract(min);
783
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
784
+        return normalized;
785
+    }
786
+
787
+    /**
788
+     * 实现科室抽问抽答正确率查询
789
+     * 归一化公式:(当前值 - min) / (max - min)
790
+     */
791
+    private BigDecimal queryDepartmentTrainingScore(Long deptId, Map<Long, BigDecimal> accuracyMap) {
792
+        return normalizeFromMap(deptId, accuracyMap);
793
+    }
794
+
795
+    /**
796
+     * 从正确率Map中归一化指定ID的值
797
+     * 归一化公式:(当前值 - min) / (max - min),与查获效率的归一化逻辑一致
798
+     */
799
+    private BigDecimal normalizeFromMap(Long id, Map<Long, BigDecimal> accuracyMap) {
800
+        if (accuracyMap == null || accuracyMap.isEmpty()) {
801
+            return BigDecimal.ZERO;
802
+        }
803
+        BigDecimal value = accuracyMap.getOrDefault(id, BigDecimal.ZERO);
804
+        BigDecimal min = accuracyMap.values().stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
805
+        BigDecimal max = accuracyMap.values().stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
806
+        BigDecimal difference = max.subtract(min);
807
+        return difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : value.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
808
+    }
809
+
810
+    /**
811
+     * Date转yyyy-MM-dd字符串
812
+     */
813
+    private String formatDate(Date date) {
814
+        if (date == null) {
815
+            return null;
816
+        }
817
+        return new SimpleDateFormat("yyyy-MM-dd").format(date);
818
+    }
819
+
820
+    /**
821
+     * 指标数据内部类
822
+     */
823
+    private static class MetricData {
824
+        Long id;
825
+        String name;
826
+        BigDecimal seizureEfficiency;
827
+        BigDecimal inspectionPassRate;
828
+        BigDecimal trainingScore;
829
+    }
830
+
831
+    /**
832
+     * 标准化数据内部类
833
+     */
834
+    private static class NormalizedData {
835
+        Long id;
836
+        String name;
837
+        BigDecimal seizureEfficiency;
838
+        BigDecimal inspectionPassRate;
839
+        BigDecimal trainingScore;
840
+        BigDecimal totalScore;
841
+    }
842
+
843
+    /**
844
+     * 计算巡检合格率
845
+     *
846
+     * @param param
847
+     * @return
848
+     */
849
+    private List<CheckEfficiencyRankDto> calculateCheckEfficiency(PerformanceMetricsParamDto param) {
850
+        List<CheckEfficiencyRankDto> rankList = new ArrayList<>();
851
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
852
+        dto.setStartDate(param.getStartTime());
853
+        dto.setEndDate(param.getEndTime());
854
+        CheckEfficiencyDto result = checkEfficiencyService.getCheckEfficiency(dto);
855
+        if (param.getDimension() == 1) {
856
+            // 个人维度
857
+            rankList = result.getIndividualRankList();
858
+        } else if (param.getDimension() == 2) {
859
+            // 班组维度
860
+            rankList = result.getTeamRankList();
861
+        } else if (param.getDimension() == 3) {
862
+            // 科室维度
863
+            rankList = result.getDepartmentRankList();
864
+        } else if (param.getDimension() == 3) {
865
+            // 大队维度
866
+            rankList = result.getBrigadeRankList();
867
+        }
868
+        return rankList;
869
+    }
870
+
871
+}

+ 36 - 0
airport-check/src/main/java/com/sundot/airport/check/domain/CheckEfficiencyDto.java

@@ -0,0 +1,36 @@
1
+package com.sundot.airport.check.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.util.List;
6
+
7
+/**
8
+ * 巡检合格率统计DTO
9
+ *
10
+ * @author ruoyi
11
+ * @date 2025-07-25
12
+ */
13
+@Data
14
+public class CheckEfficiencyDto {
15
+
16
+    /**
17
+     * 大队巡检合格率排行
18
+     */
19
+    private List<CheckEfficiencyRankDto> brigadeRankList;
20
+
21
+    /**
22
+     * 科室巡检合格率排行
23
+     */
24
+    private List<CheckEfficiencyRankDto> departmentRankList;
25
+
26
+    /**
27
+     * 班组巡检合格率排行
28
+     */
29
+    private List<CheckEfficiencyRankDto> teamRankList;
30
+
31
+    /**
32
+     * 安检员巡检合格率排行
33
+     */
34
+    private List<CheckEfficiencyRankDto> individualRankList;
35
+
36
+}

+ 36 - 0
airport-check/src/main/java/com/sundot/airport/check/domain/CheckEfficiencyRankDto.java

@@ -0,0 +1,36 @@
1
+package com.sundot.airport.check.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+
7
+/**
8
+ * 巡检合格率排行DTO
9
+ *
10
+ * @author ruoyi
11
+ * @date 2025-07-25
12
+ */
13
+@Data
14
+public class CheckEfficiencyRankDto {
15
+
16
+    /**
17
+     * ID(科室ID/班组ID/个人ID)
18
+     */
19
+    private Long id;
20
+
21
+    /**
22
+     * 名称(科室名称/班组名称/个人姓名)
23
+     */
24
+    private String name;
25
+
26
+    /**
27
+     * 巡检合格率
28
+     */
29
+    private BigDecimal passRate;
30
+
31
+    /**
32
+     * 排名
33
+     */
34
+    private Integer rank;
35
+
36
+}

+ 21 - 0
airport-check/src/main/java/com/sundot/airport/check/service/ICheckEfficiencyService.java

@@ -0,0 +1,21 @@
1
+package com.sundot.airport.check.service;
2
+
3
+import com.sundot.airport.check.domain.CheckEfficiencyDto;
4
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
5
+
6
+/**
7
+ * 巡检合格率Service接口
8
+ *
9
+ * @author ruoyi
10
+ * @date 2025-07-25
11
+ */
12
+public interface ICheckEfficiencyService {
13
+
14
+    /**
15
+     * 查询巡检合格率统计
16
+     *
17
+     * @param dto 查询参数
18
+     * @return 巡检合格率统计结果
19
+     */
20
+    public CheckEfficiencyDto getCheckEfficiency(BaseLargeScreenQueryParamDto dto);
21
+}

+ 8 - 0
airport-check/src/main/java/com/sundot/airport/check/service/ICheckLargeScreenService.java

@@ -181,6 +181,14 @@ public interface ICheckLargeScreenService {
181 181
     public List<CheckLargeScreenHomePageItemDto> getCheckLargeScreenHomePageItemDtoList(BaseLargeScreenQueryParamDto dto);
182 182
 
183 183
     /**
184
+     * 巡检合格率全量数据-为空时默认合格率
185
+     *
186
+     * @param dto 查询参数
187
+     * @return 巡检合格率全量数据
188
+     */
189
+    public List<CheckLargeScreenHomePageItemDto> getCheckLargeScreenHomePageItemDtoListAndNull(BaseLargeScreenQueryParamDto dto);
190
+
191
+    /**
184 192
      * 首页-巡检-巡检合格率排名
185 193
      *
186 194
      * @param dto 首页查询参数

+ 153 - 0
airport-check/src/main/java/com/sundot/airport/check/service/impl/CheckEfficiencyServiceImpl.java

@@ -0,0 +1,153 @@
1
+package com.sundot.airport.check.service.impl;
2
+
3
+import cn.hutool.core.collection.CollUtil;
4
+import cn.hutool.core.util.StrUtil;
5
+import com.sundot.airport.check.domain.CheckEfficiencyDto;
6
+import com.sundot.airport.check.domain.CheckEfficiencyRankDto;
7
+import com.sundot.airport.check.service.ICheckEfficiencyService;
8
+import com.sundot.airport.check.service.ICheckLargeScreenService;
9
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
10
+import com.sundot.airport.common.core.domain.CheckLargeScreenHomePageItemDto;
11
+import com.sundot.airport.common.core.domain.entity.SysDept;
12
+import com.sundot.airport.common.core.domain.entity.SysUser;
13
+import com.sundot.airport.common.enums.DeptType;
14
+import com.sundot.airport.common.enums.RoleTypeEnum;
15
+import com.sundot.airport.common.utils.DeptUtils;
16
+import com.sundot.airport.common.utils.SecurityUtils;
17
+import com.sundot.airport.system.service.ISysDeptService;
18
+import com.sundot.airport.system.service.ISysUserService;
19
+import org.springframework.beans.factory.annotation.Autowired;
20
+import org.springframework.stereotype.Service;
21
+
22
+import java.math.BigDecimal;
23
+import java.math.RoundingMode;
24
+import java.util.Arrays;
25
+import java.util.List;
26
+import java.util.stream.Collectors;
27
+
28
+/**
29
+ * 巡检合格率Service业务层处理
30
+ *
31
+ * @author ruoyi
32
+ * @date 2025-07-25
33
+ */
34
+@Service
35
+public class CheckEfficiencyServiceImpl implements ICheckEfficiencyService {
36
+
37
+    @Autowired
38
+    private ISysUserService sysUserService;
39
+
40
+    @Autowired
41
+    private ISysDeptService sysDeptService;
42
+
43
+    @Autowired
44
+    private ICheckLargeScreenService iCheckLargeScreenService;
45
+
46
+
47
+    /**
48
+     * 查询巡检合格率统计
49
+     *
50
+     * @param dto 查询参数
51
+     * @return 巡检合格率统计结果
52
+     */
53
+    @Override
54
+    public CheckEfficiencyDto getCheckEfficiency(BaseLargeScreenQueryParamDto dto) {
55
+        CheckEfficiencyDto result = new CheckEfficiencyDto();
56
+        List<CheckLargeScreenHomePageItemDto> itemListAll = iCheckLargeScreenService.getCheckLargeScreenHomePageItemDtoListAndNull(dto);
57
+//        List<SysDept> deptList = sysDeptService.selectDeptInfoAll(new SysDept());
58
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(SecurityUtils.getDeptId()));
59
+        List<SysDept> deptList = sysDeptService.selectChildrenDeptById(topSiteId);
60
+
61
+        // 获取大队效率排行
62
+        List<SysDept> brigadeDeptList = deptList.stream().filter(item -> StrUtil.equals(DeptType.BRIGADE.getCode(), item.getDeptType())).collect(Collectors.toList());
63
+        List<CheckEfficiencyRankDto> brigadeSeizureList = brigadeDeptList
64
+                .stream()
65
+                .map(item -> {
66
+                    CheckEfficiencyRankDto rankDto = new CheckEfficiencyRankDto();
67
+                    rankDto.setId(item.getDeptId());
68
+                    rankDto.setName(item.getDeptName());
69
+                    rankDto.setPassRate(calculate(itemListAll, item.getDeptId()));
70
+                    return rankDto;
71
+                })
72
+                .collect(Collectors.toList());
73
+        brigadeSeizureList.sort((a, b) -> b.getPassRate().compareTo(a.getPassRate()));
74
+        for (int i = 0; i < brigadeSeizureList.size(); i++) {
75
+            brigadeSeizureList.get(i).setRank(i + 1);
76
+        }
77
+        result.setBrigadeRankList(brigadeSeizureList);
78
+
79
+        // 获取科室效率排行
80
+        List<SysDept> departmentDeptList = deptList.stream().filter(item -> StrUtil.equals(DeptType.MANAGER.getCode(), item.getDeptType())).collect(Collectors.toList());
81
+        List<CheckEfficiencyRankDto> departmentSeizureList = departmentDeptList
82
+                .stream()
83
+                .map(item -> {
84
+                    CheckEfficiencyRankDto rankDto = new CheckEfficiencyRankDto();
85
+                    rankDto.setId(item.getDeptId());
86
+                    rankDto.setName(item.getDeptName());
87
+                    rankDto.setPassRate(calculate(itemListAll, item.getDeptId()));
88
+                    return rankDto;
89
+                })
90
+                .collect(Collectors.toList());
91
+        departmentSeizureList.sort((a, b) -> b.getPassRate().compareTo(a.getPassRate()));
92
+        for (int i = 0; i < departmentSeizureList.size(); i++) {
93
+            departmentSeizureList.get(i).setRank(i + 1);
94
+        }
95
+        result.setDepartmentRankList(departmentSeizureList);
96
+
97
+        // 获取班组效率排行
98
+        List<SysDept> teamDeptList = deptList.stream().filter(item -> StrUtil.equals(DeptType.TEAMS.getCode(), item.getDeptType())).collect(Collectors.toList());
99
+        List<CheckEfficiencyRankDto> teamSeizureList = teamDeptList
100
+                .stream()
101
+                .map(item -> {
102
+                    CheckEfficiencyRankDto rankDto = new CheckEfficiencyRankDto();
103
+                    rankDto.setId(item.getDeptId());
104
+                    rankDto.setName(item.getDeptName());
105
+                    rankDto.setPassRate(calculate(itemListAll, item.getDeptId()));
106
+                    return rankDto;
107
+                })
108
+                .collect(Collectors.toList());
109
+        teamSeizureList.sort((a, b) -> b.getPassRate().compareTo(a.getPassRate()));
110
+        for (int i = 0; i < teamSeizureList.size(); i++) {
111
+            teamSeizureList.get(i).setRank(i + 1);
112
+        }
113
+        result.setTeamRankList(teamSeizureList);
114
+
115
+        // 获取个人效率排行
116
+        List<CheckEfficiencyRankDto> individualSeizureList = itemListAll
117
+                .stream()
118
+                .map(item -> {
119
+                    CheckEfficiencyRankDto rankDto = new CheckEfficiencyRankDto();
120
+                    rankDto.setId(item.getId());
121
+                    rankDto.setName(item.getName());
122
+                    rankDto.setPassRate(item.getPassRate());
123
+                    return rankDto;
124
+                })
125
+                .collect(Collectors.toList());
126
+        individualSeizureList.sort((a, b) -> b.getPassRate().compareTo(a.getPassRate()));
127
+        for (int i = 0; i < individualSeizureList.size(); i++) {
128
+            individualSeizureList.get(i).setRank(i + 1);
129
+        }
130
+        result.setIndividualRankList(individualSeizureList);
131
+
132
+        return result;
133
+    }
134
+
135
+    /**
136
+     * 计算部门通过率
137
+     *
138
+     * @param itemListAll 全量数据
139
+     * @param deptId      部门ID
140
+     * @return 部门通过率
141
+     */
142
+    private BigDecimal calculate(List<CheckLargeScreenHomePageItemDto> itemListAll, Long deptId) {
143
+        if (CollUtil.isEmpty(itemListAll)) {
144
+            return BigDecimal.ONE;
145
+        }
146
+        List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), deptId);
147
+        List<Long> userIdList = sysUserList.stream().map(SysUser::getUserId).collect(Collectors.toList());
148
+        List<CheckLargeScreenHomePageItemDto> itemDtoList = itemListAll.stream().filter(item -> userIdList.contains(item.getId())).collect(Collectors.toList());
149
+        BigDecimal reduce = itemDtoList.stream().map(CheckLargeScreenHomePageItemDto::getPassRate).reduce(BigDecimal.ZERO, BigDecimal::add);
150
+        return CollUtil.isEmpty(itemDtoList) ? BigDecimal.ONE : reduce.divide(new BigDecimal(itemDtoList.size()), 4, RoundingMode.HALF_UP);
151
+    }
152
+
153
+}

+ 56 - 0
airport-check/src/main/java/com/sundot/airport/check/service/impl/CheckLargeScreenServiceImpl.java

@@ -2480,6 +2480,62 @@ public class CheckLargeScreenServiceImpl implements ICheckLargeScreenService {
2480 2480
     }
2481 2481
 
2482 2482
     /**
2483
+     * 巡检合格率全量数据-为空时默认合格率
2484
+     *
2485
+     * @param dto 查询参数
2486
+     * @return 巡检合格率全量数据
2487
+     */
2488
+    @Override
2489
+    public List<CheckLargeScreenHomePageItemDto> getCheckLargeScreenHomePageItemDtoListAndNull(BaseLargeScreenQueryParamDto dto) {
2490
+        List<CheckLargeScreenHomePageItemDto> result = new ArrayList<>();
2491
+        String key = CacheConstants.USER_CHECK_PASS_RATE_KEY + dto.getStartDate() + ":" + dto.getEndDate() + ":" + dto.getSpecifiedDate();
2492
+        List<CheckLargeScreenHomePageItemDto> cacheList = redisCache.getCacheList(key);
2493
+        if (CollUtil.isNotEmpty(cacheList)) {
2494
+            result.addAll(cacheList);
2495
+        } else {
2496
+            List<LargeScreenHomePageUserInfoSqlDto> userInfoListAll = sysUserService.homePageUserInfo();
2497
+            if (CollUtil.isEmpty(userInfoListAll)) {
2498
+                return Collections.emptyList();
2499
+            }
2500
+            List<CheckLargeScreenHomePageCheckRecordSqlDto> checkRecordListAll = checkLargeScreenMapper.homePageCheckRecord(dto);
2501
+            if (CollUtil.isEmpty(checkRecordListAll)) {
2502
+                userInfoListAll.forEach(userInfoSqlDto -> {
2503
+                    CheckLargeScreenHomePageItemDto itemDto = new CheckLargeScreenHomePageItemDto();
2504
+                    itemDto.setId(userInfoSqlDto.getUserId());
2505
+                    itemDto.setName(userInfoSqlDto.getNickName());
2506
+                    itemDto.setPassRate(BigDecimal.ONE);
2507
+                    result.add(itemDto);
2508
+                });
2509
+            } else {
2510
+                userInfoListAll.forEach(userInfoSqlDto -> {
2511
+                    CheckLargeScreenHomePageItemDto itemDto = new CheckLargeScreenHomePageItemDto();
2512
+                    itemDto.setId(userInfoSqlDto.getUserId());
2513
+                    itemDto.setName(userInfoSqlDto.getNickName());
2514
+                    List<CheckLargeScreenHomePageCheckRecordSqlDto> departmentList = checkRecordListAll.stream().filter(checkRecordSqlDto -> StrUtil.equals(checkRecordSqlDto.getCheckedLevel(), CheckLevelEnum.DEPARTMENT_LEVEL.getCode()) && ObjUtil.equals(userInfoSqlDto.getDepartmentId(), checkRecordSqlDto.getCheckedDepartmentId())).collect(Collectors.toList());
2515
+                    long departmentTotal = departmentList.stream().map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2516
+                    long departmentUnqualifiedNum = departmentList.stream().filter(item -> ObjUtil.equals(userInfoSqlDto.getUserId(), item.getUserId())).map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2517
+                    long departmentQualifiedNum = departmentTotal - departmentUnqualifiedNum;
2518
+                    List<CheckLargeScreenHomePageCheckRecordSqlDto> teamList = checkRecordListAll.stream().filter(checkRecordSqlDto -> StrUtil.equals(checkRecordSqlDto.getCheckedLevel(), CheckLevelEnum.TEAM_LEVEL.getCode()) && ObjUtil.equals(userInfoSqlDto.getTeamId(), checkRecordSqlDto.getCheckedTeamId())).collect(Collectors.toList());
2519
+                    long teamTotal = teamList.stream().map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2520
+                    long teamUnqualifiedNum = teamList.stream().filter(item -> ObjUtil.equals(userInfoSqlDto.getUserId(), item.getUserId())).map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2521
+                    long teamQualifiedNum = teamTotal - teamUnqualifiedNum;
2522
+                    List<CheckLargeScreenHomePageCheckRecordSqlDto> personalList = checkRecordListAll.stream().filter(checkRecordSqlDto -> StrUtil.equals(checkRecordSqlDto.getCheckedLevel(), CheckLevelEnum.PERSONNEL_LEVEL.getCode()) && ObjUtil.equals(userInfoSqlDto.getUserId(), checkRecordSqlDto.getCheckedPersonnelId())).collect(Collectors.toList());
2523
+                    long personalTotal = personalList.stream().map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2524
+                    long personalUnqualifiedNum = personalList.stream().filter(item -> ObjUtil.equals(userInfoSqlDto.getUserId(), item.getUserId())).map(CheckLargeScreenHomePageCheckRecordSqlDto::getId).distinct().count();
2525
+                    long personalQualifiedNum = personalTotal - personalUnqualifiedNum;
2526
+                    long total = personalTotal + teamTotal + departmentTotal;
2527
+                    long qualifiedNum = departmentQualifiedNum + teamQualifiedNum + personalQualifiedNum;
2528
+                    itemDto.setPassRate(total == 0 ? BigDecimal.ONE : new BigDecimal(qualifiedNum).divide(new BigDecimal(total), 4, RoundingMode.HALF_UP));
2529
+                    result.add(itemDto);
2530
+                });
2531
+            }
2532
+            redisCache.setCacheList(key, result);
2533
+            redisCache.expire(key, 10, TimeUnit.MINUTES);
2534
+        }
2535
+        return result;
2536
+    }
2537
+
2538
+    /**
2483 2539
      * 首页-巡检-巡检合格率排名
2484 2540
      *
2485 2541
      * @param dto 首页查询参数

+ 7 - 0
airport-common/src/main/java/com/sundot/airport/common/core/domain/BaseLargeScreenQueryParamDto.java

@@ -3,6 +3,7 @@ package com.sundot.airport.common.core.domain;
3 3
 import com.fasterxml.jackson.annotation.JsonFormat;
4 4
 import io.swagger.annotations.ApiModelProperty;
5 5
 import lombok.Data;
6
+import org.springframework.format.annotation.DateTimeFormat;
6 7
 
7 8
 import java.io.Serializable;
8 9
 import java.util.Date;
@@ -21,18 +22,21 @@ public class BaseLargeScreenQueryParamDto implements Serializable {
21 22
      * 指定日期
22 23
      */
23 24
     @JsonFormat(pattern = "yyyy-MM-dd")
25
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
24 26
     private Date specifiedDate;
25 27
 
26 28
     /**
27 29
      * 开始日期
28 30
      */
29 31
     @JsonFormat(pattern = "yyyy-MM-dd")
32
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
30 33
     private Date startDate;
31 34
 
32 35
     /**
33 36
      * 结束日期
34 37
      */
35 38
     @JsonFormat(pattern = "yyyy-MM-dd")
39
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
36 40
     private Date endDate;
37 41
 
38 42
     /**
@@ -108,4 +112,7 @@ public class BaseLargeScreenQueryParamDto implements Serializable {
108 112
      */
109 113
     @ApiModelProperty("数据来源:individual=个人数据,team=班组数据")
110 114
     private String dataSource;
115
+
116
+    @ApiModelProperty("站id")
117
+    private Long inspectStationId;
111 118
 }

+ 67 - 0
airport-common/src/main/java/com/sundot/airport/common/core/domain/BoxPlotDataDto.java

@@ -0,0 +1,67 @@
1
+package com.sundot.airport.common.core.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+import java.util.List;
7
+
8
+/**
9
+ * 箱线图数据DTO
10
+ *
11
+ * @author ruoyi
12
+ * @date 2026-02-24
13
+ */
14
+@Data
15
+public class BoxPlotDataDto {
16
+
17
+    /**
18
+     * 最小观察值(Q1 -  IQR)
19
+     */
20
+    private BigDecimal minObservation;
21
+
22
+    /**
23
+     * 第一四分位数(Q1)
24
+     */
25
+    private BigDecimal q1;
26
+
27
+    /**
28
+     * 中位数(Q2)
29
+     */
30
+    private BigDecimal median;
31
+
32
+    /**
33
+     * 第三四分位数(Q3)
34
+     */
35
+    private BigDecimal q3;
36
+
37
+    /**
38
+     * 最大观察值(Q3 +  IQR)
39
+     */
40
+    private BigDecimal maxObservation;
41
+
42
+    /**
43
+     * 箱线图下限(真实最小值与最小观察值的较大值)
44
+     */
45
+    private BigDecimal lowerBound;
46
+
47
+    /**
48
+     * 箱线图上限(真实最大值与最大观察值的较小值)
49
+     */
50
+    private BigDecimal upperBound;
51
+
52
+    /**
53
+     * 离群点列表(超出上下限的数据)
54
+     */
55
+    private List<BigDecimal> outliers;
56
+
57
+    /**
58
+     * 数据总数
59
+     */
60
+    private Integer totalCount;
61
+
62
+    /**
63
+     * 非离群点数据列表(用于绘制箱体)
64
+     */
65
+    private List<BigDecimal> nonOutliers;
66
+
67
+}

+ 42 - 0
airport-common/src/main/java/com/sundot/airport/common/core/domain/GroupedBoxPlotDataDto.java

@@ -0,0 +1,42 @@
1
+package com.sundot.airport.common.core.domain;
2
+
3
+/**
4
+ * 分组箱线图数据DTO
5
+ * 用于存储按科室分组的箱线图数据
6
+ */
7
+public class GroupedBoxPlotDataDto {
8
+
9
+    /**
10
+     * 分组名称(科室名称)
11
+     */
12
+    private String groupName;
13
+
14
+    /**
15
+     * 箱线图数据
16
+     */
17
+    private BoxPlotDataDto boxPlotData;
18
+
19
+    public GroupedBoxPlotDataDto() {
20
+    }
21
+
22
+    public GroupedBoxPlotDataDto(String groupName, BoxPlotDataDto boxPlotData) {
23
+        this.groupName = groupName;
24
+        this.boxPlotData = boxPlotData;
25
+    }
26
+
27
+    public String getGroupName() {
28
+        return groupName;
29
+    }
30
+
31
+    public void setGroupName(String groupName) {
32
+        this.groupName = groupName;
33
+    }
34
+
35
+    public BoxPlotDataDto getBoxPlotData() {
36
+        return boxPlotData;
37
+    }
38
+
39
+    public void setBoxPlotData(BoxPlotDataDto boxPlotData) {
40
+        this.boxPlotData = boxPlotData;
41
+    }
42
+}

+ 87 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/CategoryAccuracyItemDto.java

@@ -0,0 +1,87 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import java.math.BigDecimal;
4
+
5
+/**
6
+ * 题目分类正确率条目DTO
7
+ */
8
+public class CategoryAccuracyItemDto {
9
+
10
+    /**
11
+     * 分类名称(如:通道面貌类、勤务组织类等)
12
+     */
13
+    private String categoryName;
14
+
15
+    /**
16
+     * 正确题数
17
+     */
18
+    private Integer correctCount;
19
+
20
+    /**
21
+     * 总题数
22
+     */
23
+    private Integer totalCount;
24
+
25
+    /**
26
+     * 正确率(百分制,如:90.00)
27
+     */
28
+    private BigDecimal correctRate;
29
+
30
+    /**
31
+     * 标签:最高 / 薄弱 / 一般
32
+     */
33
+    private String label;
34
+
35
+    /**
36
+     * 颜色:green(最高)/ red(薄弱)/ normal(一般)
37
+     */
38
+    private String color;
39
+
40
+    public String getCategoryName() {
41
+        return categoryName;
42
+    }
43
+
44
+    public void setCategoryName(String categoryName) {
45
+        this.categoryName = categoryName;
46
+    }
47
+
48
+    public Integer getCorrectCount() {
49
+        return correctCount;
50
+    }
51
+
52
+    public void setCorrectCount(Integer correctCount) {
53
+        this.correctCount = correctCount;
54
+    }
55
+
56
+    public Integer getTotalCount() {
57
+        return totalCount;
58
+    }
59
+
60
+    public void setTotalCount(Integer totalCount) {
61
+        this.totalCount = totalCount;
62
+    }
63
+
64
+    public BigDecimal getCorrectRate() {
65
+        return correctRate;
66
+    }
67
+
68
+    public void setCorrectRate(BigDecimal correctRate) {
69
+        this.correctRate = correctRate;
70
+    }
71
+
72
+    public String getLabel() {
73
+        return label;
74
+    }
75
+
76
+    public void setLabel(String label) {
77
+        this.label = label;
78
+    }
79
+
80
+    public String getColor() {
81
+        return color;
82
+    }
83
+
84
+    public void setColor(String color) {
85
+        this.color = color;
86
+    }
87
+}

+ 36 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceDimensionParamDto.java

@@ -0,0 +1,36 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonFormat;
4
+import io.swagger.annotations.ApiModel;
5
+import io.swagger.annotations.ApiModelProperty;
6
+import lombok.Data;
7
+import org.springframework.format.annotation.DateTimeFormat;
8
+
9
+import java.util.Date;
10
+
11
+@Data
12
+@ApiModel("PC绩效指标查询参数DTO")
13
+public class PerformanceDimensionParamDto {
14
+
15
+    @ApiModelProperty("统计维度:1-人员、2-班级、3-科级、4-大队")
16
+    private Integer dimension;
17
+
18
+    @ApiModelProperty("绩效维度:seizureEfficiency-查获效率、inspectionPassRate-巡检合格率、trainingScore-培训得分、totalScore-总分")
19
+    private String resultField;
20
+
21
+    @ApiModelProperty("排序结果:1-升序(由小到大)、2-降序(由大到小)")
22
+    private Integer sortOrder;
23
+
24
+    @ApiModelProperty("开始时间")
25
+    @JsonFormat(pattern = "yyyy-MM-dd")
26
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
27
+    private Date startTime;
28
+
29
+    @ApiModelProperty("结束时间")
30
+    @JsonFormat(pattern = "yyyy-MM-dd")
31
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
32
+    private Date endTime;
33
+
34
+    @ApiModelProperty("维度(总分/进步,只要个体和班级有  1-总分 2-进步)")
35
+    private Integer dimensionType;
36
+}

+ 51 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceDimensionResult.java

@@ -0,0 +1,51 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import io.swagger.annotations.ApiModel;
4
+import io.swagger.annotations.ApiModelProperty;
5
+import lombok.Data;
6
+
7
+import java.math.BigDecimal;
8
+
9
+/**
10
+ * 绩效维度统计结果DTO
11
+ * 用于返回"得分/环比"格式的数据,如"84/+3%"
12
+ *
13
+ * @author wangxx
14
+ */
15
+@Data
16
+@ApiModel("绩效维度统计结果DTO")
17
+public class PerformanceDimensionResult {
18
+
19
+    @ApiModelProperty("组织/姓名ID")
20
+    private Long id;
21
+
22
+    @ApiModelProperty("组织/姓名")
23
+    private String name;
24
+
25
+    @ApiModelProperty("查获效率(标准化后)")
26
+    private BigDecimal seizureEfficiency;
27
+
28
+    @ApiModelProperty("巡检合格率(标准化后)")
29
+    private BigDecimal inspectionPassRate;
30
+
31
+    @ApiModelProperty("培训得分(标准化后)")
32
+    private BigDecimal trainingScore;
33
+
34
+    @ApiModelProperty("总分(查获效率+巡检合格率+培训得分)")
35
+    private BigDecimal totalScore;
36
+
37
+    @ApiModelProperty("查获效率显示格式:得分/环比,如\"84/+3%\"")
38
+    private String seizureEfficiencyDisplay;
39
+
40
+    @ApiModelProperty("巡检合格率显示格式:得分/环比,如\"81/+1%\"")
41
+    private String inspectionPassRateDisplay;
42
+
43
+    @ApiModelProperty("培训得分显示格式:得分/环比,如\"90/+1%\"")
44
+    private String trainingScoreDisplay;
45
+
46
+    @ApiModelProperty("总分显示格式:得分/环比")
47
+    private String totalScoreDisplay;
48
+
49
+    @ApiModelProperty("排名")
50
+    private Integer rank;
51
+}

+ 39 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceMetricsParamDto.java

@@ -0,0 +1,39 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonFormat;
4
+import io.swagger.annotations.ApiModel;
5
+import io.swagger.annotations.ApiModelProperty;
6
+import lombok.Data;
7
+import org.springframework.format.annotation.DateTimeFormat;
8
+
9
+import java.util.Date;
10
+
11
+/**
12
+ * 绩效指标查询参数DTO
13
+ *
14
+ * @author sundot
15
+ * @date 2026-02-25
16
+ */
17
+@Data
18
+@ApiModel("绩效指标查询参数DTO")
19
+public class PerformanceMetricsParamDto {
20
+
21
+    @ApiModelProperty("统计维度:1-人员、2-班级、3-科级、4-大队级")
22
+    private Integer dimension;
23
+
24
+    @ApiModelProperty("排序方式:seizureEfficiency-查获效率、inspectionPassRate-巡检合格率、trainingScore-培训得分、totalScore-总分")
25
+    private String sortField;
26
+
27
+    @ApiModelProperty("排序结果:1-升序、2-降序")
28
+    private Integer sortOrder;
29
+
30
+    @ApiModelProperty("开始时间")
31
+    @JsonFormat(pattern = "yyyy-MM-dd")
32
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
33
+    private Date startTime;
34
+
35
+    @ApiModelProperty("结束时间")
36
+    @JsonFormat(pattern = "yyyy-MM-dd")
37
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
38
+    private Date endTime;
39
+}

+ 57 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/PerformanceMetricsResultDto.java

@@ -0,0 +1,57 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import io.swagger.annotations.ApiModel;
4
+import io.swagger.annotations.ApiModelProperty;
5
+import lombok.Data;
6
+
7
+import java.math.BigDecimal;
8
+
9
+/**
10
+ * 绩效指标结果DTO
11
+ *
12
+ * @author sundot
13
+ * @date 2026-02-25
14
+ */
15
+@Data
16
+@ApiModel("绩效指标结果DTO")
17
+public class PerformanceMetricsResultDto {
18
+
19
+    @ApiModelProperty("组织/部门id")
20
+    private Long id;
21
+
22
+    @ApiModelProperty("组织/姓名")
23
+    private String name;
24
+
25
+    @ApiModelProperty("查获效率")
26
+    private BigDecimal seizureEfficiency;
27
+
28
+    @ApiModelProperty("巡检合格率")
29
+    private BigDecimal inspectionPassRate;
30
+
31
+    @ApiModelProperty("培训得分")
32
+    private BigDecimal trainingScore;
33
+
34
+    @ApiModelProperty("总分(查获效率+巡检合格率+抽问抽答正确率)")
35
+    private BigDecimal totalScore;
36
+
37
+    @ApiModelProperty("排名")
38
+    private Integer rank;
39
+
40
+    /**
41
+     * 大队名称(当维度为科室或班级或人员)
42
+     */
43
+    @ApiModelProperty("大队名称")
44
+    private String brigadeName;
45
+
46
+    /**
47
+     * 科室名称(当维度为班级或人员)
48
+     */
49
+    @ApiModelProperty("科室名称")
50
+    private String deptName;
51
+
52
+    /**
53
+     * 班级名称(当维度为人员)
54
+     */
55
+    @ApiModelProperty("班级名称")
56
+    private String className;
57
+}

+ 75 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/QuizAccuracyAnalysisResultDto.java

@@ -0,0 +1,75 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import java.math.BigDecimal;
4
+import java.util.List;
5
+
6
+/**
7
+ * 抽问抽答正确率分析结果DTO
8
+ */
9
+public class QuizAccuracyAnalysisResultDto {
10
+
11
+    /**
12
+     * 全站平均正确率(百分制,如:80.00)
13
+     */
14
+    private BigDecimal avgCorrectRate;
15
+
16
+    /**
17
+     * 各分类正确率列表
18
+     */
19
+    private List<CategoryAccuracyItemDto> categoryList;
20
+
21
+    /**
22
+     * 掌握程度最好的分类名称(正确率最高)
23
+     */
24
+    private String bestCategoryName;
25
+
26
+    /**
27
+     * 知识点薄弱的分类名称(正确率最低)
28
+     */
29
+    private String weakCategoryName;
30
+
31
+    /**
32
+     * 注释说明,如:[通道面貌类]掌握程度最好,[勤务组织类]知识点薄弱
33
+     */
34
+    private String note;
35
+
36
+    public BigDecimal getAvgCorrectRate() {
37
+        return avgCorrectRate;
38
+    }
39
+
40
+    public void setAvgCorrectRate(BigDecimal avgCorrectRate) {
41
+        this.avgCorrectRate = avgCorrectRate;
42
+    }
43
+
44
+    public List<CategoryAccuracyItemDto> getCategoryList() {
45
+        return categoryList;
46
+    }
47
+
48
+    public void setCategoryList(List<CategoryAccuracyItemDto> categoryList) {
49
+        this.categoryList = categoryList;
50
+    }
51
+
52
+    public String getBestCategoryName() {
53
+        return bestCategoryName;
54
+    }
55
+
56
+    public void setBestCategoryName(String bestCategoryName) {
57
+        this.bestCategoryName = bestCategoryName;
58
+    }
59
+
60
+    public String getWeakCategoryName() {
61
+        return weakCategoryName;
62
+    }
63
+
64
+    public void setWeakCategoryName(String weakCategoryName) {
65
+        this.weakCategoryName = weakCategoryName;
66
+    }
67
+
68
+    public String getNote() {
69
+        return note;
70
+    }
71
+
72
+    public void setNote(String note) {
73
+        this.note = note;
74
+    }
75
+}

+ 105 - 0
airport-common/src/main/java/com/sundot/airport/common/dto/TimeBasedPerformanceResult.java

@@ -0,0 +1,105 @@
1
+package com.sundot.airport.common.dto;
2
+
3
+import io.swagger.annotations.ApiModel;
4
+import io.swagger.annotations.ApiModelProperty;
5
+import lombok.Data;
6
+
7
+import java.math.BigDecimal;
8
+import java.util.List;
9
+
10
+/**
11
+ * 基于时间维度的绩效统计结果DTO
12
+ * 数据结构:时间 -> [对象名称, 数值] 列表
13
+ */
14
+@Data
15
+@ApiModel("基于时间维度的绩效统计结果")
16
+public class TimeBasedPerformanceResult {
17
+
18
+    /**
19
+     * 时间轴标签列表
20
+     */
21
+    @ApiModelProperty("时间轴标签列表")
22
+    private List<String> timeAxis;
23
+
24
+    /**
25
+     * 时间维度数据列表
26
+     */
27
+    @ApiModelProperty("时间维度数据列表")
28
+    private List<TimeData> timeDataList;
29
+
30
+    /**
31
+     * 排序字段
32
+     */
33
+    @ApiModelProperty("绩效维度")
34
+    private String resultField;
35
+
36
+    /**
37
+     * 是否为进步维度
38
+     */
39
+    @ApiModelProperty("是否为进步维度")
40
+    private Boolean isProgress;
41
+
42
+    /**
43
+     * 时间数据内部类
44
+     */
45
+    @Data
46
+    @ApiModel("时间数据")
47
+    public static class TimeData {
48
+        /**
49
+         * 时间标签(如:2024-01、2024-01-01~2024-01-07等)
50
+         */
51
+        @ApiModelProperty("时间标签")
52
+        private String timeLabel;
53
+
54
+        /**
55
+         * 该时间点下的数据项列表
56
+         */
57
+        @ApiModelProperty("数据项列表")
58
+        private List<DataItem> dataItems;
59
+    }
60
+
61
+    /**
62
+     * 数据项内部类
63
+     */
64
+    @Data
65
+    @ApiModel("数据项")
66
+    public static class DataItem {
67
+        @ApiModelProperty("数据项 ID")
68
+        private Long id;
69
+        /**
70
+         * 对象名称(人员姓名/科室名称/班组名称)
71
+         */
72
+        @ApiModelProperty("对象名称")
73
+        private String name;
74
+
75
+        /**
76
+         * 数值
77
+         */
78
+        @ApiModelProperty("数值")
79
+        private BigDecimal value;
80
+
81
+        /**
82
+         * 显示格式(如:84/+3%)
83
+         */
84
+        @ApiModelProperty("显示格式")
85
+        private String displayValue;
86
+
87
+        /**
88
+         * 大队名称(当维度为科室或班级或人员时填写)
89
+         */
90
+        @ApiModelProperty("大队名称")
91
+        private String brigadeName;
92
+
93
+        /**
94
+         * 科室名称(当维度为班级或人员时填写)
95
+         */
96
+        @ApiModelProperty("科室名称")
97
+        private String deptName;
98
+
99
+        /**
100
+         * 班级名称(当维度为人员时填写)
101
+         */
102
+        @ApiModelProperty("班级名称")
103
+        private String className;
104
+    }
105
+}

+ 44 - 0
airport-common/src/main/java/com/sundot/airport/common/utils/DeptUtils.java

@@ -0,0 +1,44 @@
1
+package com.sundot.airport.common.utils;
2
+
3
+import com.sundot.airport.common.core.domain.entity.SysDept;
4
+
5
+/**
6
+ * 部门工具类
7
+ *
8
+ * @author sundot
9
+ */
10
+public class DeptUtils {
11
+
12
+
13
+    /**
14
+     * 获取顶级站点ID - 通用版本
15
+     *
16
+     * @return 站点ID
17
+     */
18
+    public static Long getTopSiteId(SysDept sysDept) {
19
+        if (sysDept == null) {
20
+            return null;
21
+        }
22
+
23
+        // 向上查找父级直到站点
24
+        String ancestors = sysDept.getAncestors();
25
+        if (ancestors != null && !ancestors.isEmpty()) {
26
+            String[] ancestorIds = ancestors.split(",");
27
+            if (ancestorIds.length > 0) {
28
+                try {
29
+                    // 如果包含0排除
30
+                    if ("0".equals(ancestorIds[0]) && ancestorIds.length > 1) {
31
+                        return Long.parseLong(ancestorIds[1]);
32
+                    }
33
+                    if (ancestorIds.length == 1 && "0".equals(ancestorIds[0])) {
34
+                        return sysDept.getDeptId();
35
+                    }
36
+                } catch (NumberFormatException e) {
37
+                    return 0L;
38
+                }
39
+            }
40
+        }
41
+
42
+        return null;
43
+    }
44
+}

+ 104 - 0
airport-common/src/main/java/com/sundot/airport/common/utils/GeometricMeanUtils.java

@@ -0,0 +1,104 @@
1
+package com.sundot.airport.common.utils;
2
+
3
+/**
4
+ * 几何平均数计算工具类
5
+ * - g1 = ∛(1 × a × b)
6
+ * - g2 = ∛(1/a × 1 × c)
7
+ * - g3 = ∛(1/b × 1/2 × 1/c)
8
+ */
9
+public class GeometricMeanUtils {
10
+
11
+    /**
12
+     * 计算几何平均数总和
13
+     *
14
+     * @param a 变量a
15
+     * @param b 变量b
16
+     * @param c 变量c
17
+     * @return 总和值
18
+     */
19
+    public static double calculateSum(double a, double b, double c) {
20
+        double g1 = Math.cbrt(1.0 * a * b);
21
+        double g2 = Math.cbrt((1.0 / a) * 1.0 * c);
22
+        double g3 = Math.cbrt((1.0 / b) * (1.0 / c) * 1.0);
23
+
24
+        return g1 + g2 + g3;
25
+    }
26
+
27
+    /**
28
+     * 计算几何平均数及其权重
29
+     *
30
+     * @param a 变量a
31
+     * @param b 变量b
32
+     * @param c 变量c
33
+     * @return 包含总和和各权重的计算结果
34
+     */
35
+    public static CalculationResult calculateWithWeights(double a, double b, double c) {
36
+        double g1 = Math.cbrt(1.0 * a * b);
37
+        double g2 = Math.cbrt((1.0 / a) * 1.0 * c);
38
+        double g3 = Math.cbrt((1.0 / b) * (1.0 / c) * 1.0);
39
+
40
+        double total = g1 + g2 + g3;
41
+        double w1 = total > 0 ? g1 / total : 0;
42
+        double w2 = total > 0 ? g2 / total : 0;
43
+        double w3 = total > 0 ? g3 / total : 0;
44
+
45
+        return new CalculationResult(total, g1, g2, g3, w1, w2, w3);
46
+    }
47
+
48
+    /**
49
+     * 计算结果封装类
50
+     */
51
+    public static class CalculationResult {
52
+        private final double total;
53
+        private final double g1;
54
+        private final double g2;
55
+        private final double g3;
56
+        private final double w1;
57
+        private final double w2;
58
+        private final double w3;
59
+
60
+        public CalculationResult(double total, double g1, double g2, double g3, double w1, double w2, double w3) {
61
+            this.total = total;
62
+            this.g1 = g1;
63
+            this.g2 = g2;
64
+            this.g3 = g3;
65
+            this.w1 = w1;
66
+            this.w2 = w2;
67
+            this.w3 = w3;
68
+        }
69
+
70
+        public double getTotal() {
71
+            return total;
72
+        }
73
+
74
+        public double getG1() {
75
+            return g1;
76
+        }
77
+
78
+        public double getG2() {
79
+            return g2;
80
+        }
81
+
82
+        public double getG3() {
83
+            return g3;
84
+        }
85
+
86
+        public double getW1() {
87
+            return w1;
88
+        }
89
+
90
+        public double getW2() {
91
+            return w2;
92
+        }
93
+
94
+        public double getW3() {
95
+            return w3;
96
+        }
97
+
98
+        @Override
99
+        public String toString() {
100
+            return String.format("总和: %.6f, g1: %.6f, g2: %.6f, g3: %.6f, w1: %.6f, w2: %.6f, w3: %.6f",
101
+                    total, g1, g2, g3, w1, w2, w3);
102
+        }
103
+    }
104
+}

+ 29 - 0
airport-common/src/main/java/com/sundot/airport/common/utils/LargeScreenDateUtils.java

@@ -51,4 +51,33 @@ public class LargeScreenDateUtils {
51 51
         return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
52 52
     }
53 53
 
54
+    /**
55
+     * 日期范围类
56
+     */
57
+    public static class DateRange {
58
+        private Date startDate;
59
+        private Date endDate;
60
+
61
+        public DateRange(Date startDate, Date endDate) {
62
+            this.startDate = startDate;
63
+            this.endDate = endDate;
64
+        }
65
+
66
+        public Date getStartDate() {
67
+            return startDate;
68
+        }
69
+
70
+        public void setStartDate(Date startDate) {
71
+            this.startDate = startDate;
72
+        }
73
+
74
+        public Date getEndDate() {
75
+            return endDate;
76
+        }
77
+
78
+        public void setEndDate(Date endDate) {
79
+            this.endDate = endDate;
80
+        }
81
+    }
82
+
54 83
 }

+ 15 - 2
airport-exam/src/main/java/com/sundot/airport/exam/mapper/DailyTaskDetailMapper.java

@@ -1,6 +1,7 @@
1 1
 package com.sundot.airport.exam.mapper;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.sundot.airport.common.dto.CategoryAccuracyItemDto;
4 5
 import com.sundot.airport.common.dto.ModuleStatisticsDTO;
5 6
 import com.sundot.airport.exam.domain.DailyTaskDetail;
6 7
 import org.apache.ibatis.annotations.Mapper;
@@ -23,9 +24,9 @@ public interface DailyTaskDetailMapper extends BaseMapper<DailyTaskDetail> {
23 24
     /**
24 25
      * 统计用户在指定时间范围内的错题数(按二级分类)
25 26
      *
26
-     * @param userId 用户ID
27
+     * @param userId    用户ID
27 28
      * @param startDate 开始日期
28
-     * @param endDate 结束日期
29
+     * @param endDate   结束日期
29 30
      * @return 错题统计列表
30 31
      */
31 32
     List<ModuleStatisticsDTO> countWrongAnswersByModule(
@@ -33,4 +34,16 @@ public interface DailyTaskDetailMapper extends BaseMapper<DailyTaskDetail> {
33 34
             @Param("startDate") Date startDate,
34 35
             @Param("endDate") Date endDate
35 36
     );
37
+
38
+    /**
39
+     * 按题目分类统计全站正确率(用于抽问抽答正确率分析)
40
+     *
41
+     * @param startDate 开始日期(yyyy-MM-dd字符串)
42
+     * @param endDate   结束日期(yyyy-MM-dd字符串)
43
+     * @return 各分类正确率列表
44
+     */
45
+    List<CategoryAccuracyItemDto> selectCorrectRateByCategory(
46
+            @Param("startDate") String startDate,
47
+            @Param("endDate") String endDate
48
+    );
36 49
 }

+ 44 - 2
airport-exam/src/main/java/com/sundot/airport/exam/service/IAccuracyStatisticsService.java

@@ -2,12 +2,14 @@ package com.sundot.airport.exam.service;
2 2
 
3 3
 import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
4 4
 import com.sundot.airport.common.core.domain.DailyTaskAccuracyRankingDto;
5
+import com.sundot.airport.common.dto.QuizAccuracyAnalysisResultDto;
5 6
 import com.sundot.airport.exam.dto.AccuracyAggregateStatisticsDTO;
6 7
 import com.sundot.airport.exam.dto.AccuracyStatisticsDTO;
7 8
 import com.sundot.airport.exam.dto.AccuracyStatisticsQueryDTO;
8 9
 
9 10
 import java.math.BigDecimal;
10 11
 import java.util.List;
12
+import java.util.Map;
11 13
 
12 14
 /**
13 15
  * <b>功能名:</b>IAccuracyStatisticsService<br>
@@ -29,8 +31,8 @@ public interface IAccuracyStatisticsService {
29 31
     /**
30 32
      * 获取指定用户的正确率统计
31 33
      *
32
-     * @param userId    用户ID
33
-     * @param query     查询参数
34
+     * @param userId 用户ID
35
+     * @param query  查询参数
34 36
      * @return 正确率统计结果
35 37
      */
36 38
     AccuracyStatisticsDTO getUserAccuracyStatistics(Long userId, AccuracyStatisticsQueryDTO query);
@@ -79,4 +81,44 @@ public interface IAccuracyStatisticsService {
79 81
      * 清理部门缓存
80 82
      */
81 83
     void clearDeptCache();
84
+
85
+    /**
86
+     * 批量获取多个用户的抽问抽答正确率
87
+     *
88
+     * @param userIds   用户ID列表
89
+     * @param startDate 开始日期(yyyy-MM-dd)
90
+     * @param endDate   结束日期(yyyy-MM-dd)
91
+     * @return userId -> 正确率(百分制,如 85.50)
92
+     */
93
+    Map<Long, BigDecimal> getUsersAccuracyMap(List<Long> userIds, String startDate, String endDate);
94
+
95
+    /**
96
+     * 批量获取多个班组的抽问抽答正确率
97
+     *
98
+     * @param teamIds   班组ID列表
99
+     * @param startDate 开始日期(yyyy-MM-dd)
100
+     * @param endDate   结束日期(yyyy-MM-dd)
101
+     * @return teamId -> 正确率(百分制,如 85.50)
102
+     */
103
+    Map<Long, BigDecimal> getTeamsAccuracyMap(List<Long> teamIds, String startDate, String endDate);
104
+
105
+    /**
106
+     * 批量获取多个部门的抽问抽答正确率
107
+     *
108
+     * @param deptIds   科室ID列表
109
+     * @param startDate 开始日期(yyyy-MM-dd)
110
+     * @param endDate   结束日期(yyyy-MM-dd)
111
+     * @return deptId -> 正确率(百分制,如 85.50)
112
+     */
113
+    Map<Long, BigDecimal> getDeptsAccuracyMap(List<Long> deptIds, String startDate, String endDate);
114
+
115
+    /**
116
+     * 获取抽问抽答正确率分析(全站各分类正确率)
117
+     * 返回全站平均正确率、各分类正确率及最高/薄弱标注
118
+     *
119
+     * @param startDate 开始日期(yyyy-MM-dd)
120
+     * @param endDate   结束日期(yyyy-MM-dd)
121
+     * @return 正确率分析结果
122
+     */
123
+    QuizAccuracyAnalysisResultDto getQuizAccuracyAnalysis(String startDate, String endDate);
82 124
 }

+ 280 - 12
airport-exam/src/main/java/com/sundot/airport/exam/service/impl/AccuracyStatisticsServiceImpl.java

@@ -9,6 +9,8 @@ import com.sundot.airport.common.core.domain.DailyTaskAccuracyRankingItemDto;
9 9
 import com.sundot.airport.common.core.domain.SysHomeReportDetailDto;
10 10
 import com.sundot.airport.common.core.domain.entity.SysDept;
11 11
 import com.sundot.airport.common.core.domain.entity.SysUser;
12
+import com.sundot.airport.common.dto.CategoryAccuracyItemDto;
13
+import com.sundot.airport.common.dto.QuizAccuracyAnalysisResultDto;
12 14
 import com.sundot.airport.common.enums.DeptTypeEnum;
13 15
 import com.sundot.airport.common.enums.HomePageQueryEnum;
14 16
 import com.sundot.airport.common.exception.ServiceException;
@@ -66,6 +68,9 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
66 68
     @Autowired
67 69
     private ISysUserService userService;
68 70
 
71
+    @Autowired
72
+    private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
73
+
69 74
     /**
70 75
      * 部门数据缓存(请求级别)
71 76
      */
@@ -128,13 +133,22 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
128 133
 
129 134
             String[] ancestorIds = ancestors.split(",");
130 135
             if (ancestorIds.length >= 2) {
131
-                try { hierarchy.stationId = Long.parseLong(ancestorIds[1].trim()); } catch (NumberFormatException ignored) {}
136
+                try {
137
+                    hierarchy.stationId = Long.parseLong(ancestorIds[1].trim());
138
+                } catch (NumberFormatException ignored) {
139
+                }
132 140
             }
133 141
             if (ancestorIds.length >= 3) {
134
-                try { hierarchy.brigadeId = Long.parseLong(ancestorIds[2].trim()); } catch (NumberFormatException ignored) {}
142
+                try {
143
+                    hierarchy.brigadeId = Long.parseLong(ancestorIds[2].trim());
144
+                } catch (NumberFormatException ignored) {
145
+                }
135 146
             }
136 147
             if (ancestorIds.length >= 4) {
137
-                try { hierarchy.managerId = Long.parseLong(ancestorIds[3].trim()); } catch (NumberFormatException ignored) {}
148
+                try {
149
+                    hierarchy.managerId = Long.parseLong(ancestorIds[3].trim());
150
+                } catch (NumberFormatException ignored) {
151
+                }
138 152
             }
139 153
 
140 154
             if (StrUtil.equals(DeptType.STATION.getCode(), deptType)) {
@@ -975,10 +989,10 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
975 989
     /**
976 990
      * 获取部门的正确率排名(支持dataSource参数,适配美兰4级组织结构)
977 991
      * dataSource=individual时:
978
-     *   - 班组(TEAMS):返回班组长的个人排名
979
-     *   - 主管(MANAGER):返回主管在大队中的排名
980
-     *   - 大队(BRIGADE):返回大队在站级中的排名
981
-     *   - 站级(STATION):返回站级数据
992
+     * - 班组(TEAMS):返回班组长的个人排名
993
+     * - 主管(MANAGER):返回主管在大队中的排名
994
+     * - 大队(BRIGADE):返回大队在站级中的排名
995
+     * - 站级(STATION):返回站级数据
982 996
      * dataSource=team或空时:返回部门排名(默认)
983 997
      */
984 998
     private DailyTaskAccuracyRankingDto getDeptAccuracyRanking(Long deptId, String dataSource, Date startDate, Date endDate) {
@@ -2078,7 +2092,8 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
2078 2092
     /**
2079 2093
      * 批量计算多个班组的正确率数量(性能优化核心方法)
2080 2094
      * 一次查询所有班组的数据,然后在内存中分组统计
2081
-     * @return Map<班组ID, long[]{正确数, 总数}>
2095
+     *
2096
+     * @return Map<班组ID, long [ ] { 正确数, 总数 }>
2082 2097
      */
2083 2098
     private Map<Long, long[]> batchCalculateAccuracyCountsForTeams(List<Long> teamIds, Date startDate, Date endDate) {
2084 2099
         Map<Long, long[]> result = new HashMap<>();
@@ -2244,9 +2259,10 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
2244 2259
 
2245 2260
     /**
2246 2261
      * 将TeamAccuracyInfo列表转换为RankingItem列表
2262
+     *
2247 2263
      * @param rankings 排名列表(已按正确率降序排序)
2248
-     * @param limit 取多少个
2249
-     * @param fromTop true=从头取(Top),false=从尾取(Bottom)
2264
+     * @param limit    取多少个
2265
+     * @param fromTop  true=从头取(Top),false=从尾取(Bottom)
2250 2266
      */
2251 2267
     private List<AccuracyStatisticsDTO.RankingItem> convertToRankingItems(List<TeamAccuracyInfo> rankings, int limit, boolean fromTop) {
2252 2268
         List<AccuracyStatisticsDTO.RankingItem> result = new ArrayList<>();
@@ -2287,9 +2303,10 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
2287 2303
 
2288 2304
     /**
2289 2305
      * 将主管排名列表转换为RankingItem列表,名称格式为"大队名+主管名"
2306
+     *
2290 2307
      * @param rankings 排名列表(已按正确率降序排序)
2291
-     * @param limit 取多少个
2292
-     * @param fromTop true=从头取(Top),false=从尾取(Bottom)
2308
+     * @param limit    取多少个
2309
+     * @param fromTop  true=从头取(Top),false=从尾取(Bottom)
2293 2310
      */
2294 2311
     private List<AccuracyStatisticsDTO.RankingItem> convertToManagerRankingItems(List<TeamAccuracyInfo> rankings, int limit, boolean fromTop) {
2295 2312
         List<AccuracyStatisticsDTO.RankingItem> result = new ArrayList<>();
@@ -2375,4 +2392,255 @@ public class AccuracyStatisticsServiceImpl implements IAccuracyStatisticsService
2375 2392
         String userName;
2376 2393
         BigDecimal accuracy;
2377 2394
     }
2395
+
2396
+    @Override
2397
+    public Map<Long, BigDecimal> getUsersAccuracyMap(List<Long> userIds, String startDate, String endDate) {
2398
+        DateRange dateRange = new DateRange();
2399
+        dateRange.startDate = startDate;
2400
+        dateRange.endDate = endDate;
2401
+        return calculateUsersAccuracyMap(userIds, dateRange);
2402
+    }
2403
+
2404
+    @Override
2405
+    public Map<Long, BigDecimal> getTeamsAccuracyMap(List<Long> teamIds, String startDate, String endDate) {
2406
+        DateRange dateRange = new DateRange();
2407
+        dateRange.startDate = startDate;
2408
+        dateRange.endDate = endDate;
2409
+        return calculateTeamsAccuracyMap(teamIds, dateRange);
2410
+    }
2411
+
2412
+    @Override
2413
+    public Map<Long, BigDecimal> getDeptsAccuracyMap(List<Long> deptIds, String startDate, String endDate) {
2414
+        DateRange dateRange = new DateRange();
2415
+        dateRange.startDate = startDate;
2416
+        dateRange.endDate = endDate;
2417
+        return calculateDeptsAccuracyMap(deptIds, dateRange);
2418
+    }
2419
+
2420
+    @Override
2421
+    public QuizAccuracyAnalysisResultDto getQuizAccuracyAnalysis(String startDate, String endDate) {
2422
+        QuizAccuracyAnalysisResultDto result = new QuizAccuracyAnalysisResultDto();
2423
+
2424
+        // 查询各分类正确率
2425
+        List<CategoryAccuracyItemDto> categoryList = dailyTaskDetailMapper.selectCorrectRateByCategory(startDate, endDate);
2426
+        if (categoryList == null || categoryList.isEmpty()) {
2427
+            result.setAvgCorrectRate(BigDecimal.ZERO);
2428
+            result.setCategoryList(Collections.emptyList());
2429
+            result.setNote("");
2430
+            return result;
2431
+        }
2432
+
2433
+        // 计算全站平均正确率(所有分类合并计算)
2434
+        int totalCorrect = categoryList.stream().mapToInt(c -> c.getCorrectCount() == null ? 0 : c.getCorrectCount()).sum();
2435
+        int totalCount = categoryList.stream().mapToInt(c -> c.getTotalCount() == null ? 0 : c.getTotalCount()).sum();
2436
+        BigDecimal avgRate = totalCount > 0
2437
+                ? BigDecimal.valueOf(totalCorrect * 100.0 / totalCount).setScale(2, RoundingMode.HALF_UP)
2438
+                : BigDecimal.ZERO;
2439
+        result.setAvgCorrectRate(avgRate);
2440
+
2441
+        // 找出正确率最高和最低的分类
2442
+        CategoryAccuracyItemDto best = categoryList.stream()
2443
+                .max(Comparator.comparing(c -> c.getCorrectRate() == null ? BigDecimal.ZERO : c.getCorrectRate()))
2444
+                .orElse(null);
2445
+        CategoryAccuracyItemDto weak = categoryList.stream()
2446
+                .min(Comparator.comparing(c -> c.getCorrectRate() == null ? BigDecimal.ZERO : c.getCorrectRate()))
2447
+                .orElse(null);
2448
+
2449
+        // 标注标签和颜色(best和weak相同时均标最高)
2450
+        for (CategoryAccuracyItemDto item : categoryList) {
2451
+            if (best != null && item.getCategoryName() != null && item.getCategoryName().equals(best.getCategoryName())) {
2452
+                item.setLabel("最高");
2453
+                item.setColor("green");
2454
+            } else if (weak != null && item.getCategoryName() != null && item.getCategoryName().equals(weak.getCategoryName())) {
2455
+                item.setLabel("薄弱");
2456
+                item.setColor("red");
2457
+            } else {
2458
+                item.setLabel("一般");
2459
+                item.setColor("normal");
2460
+            }
2461
+        }
2462
+
2463
+        result.setCategoryList(categoryList);
2464
+        result.setBestCategoryName(best != null ? best.getCategoryName() : null);
2465
+        result.setWeakCategoryName(weak != null ? weak.getCategoryName() : null);
2466
+
2467
+        // 生成备注说明
2468
+        String bestName = best != null ? best.getCategoryName() : null;
2469
+        String weakName = weak != null ? weak.getCategoryName() : null;
2470
+        if (bestName != null && weakName != null && !bestName.equals(weakName)) {
2471
+            result.setNote(String.format("[%s]掌握程度最好,[%s]知识点薄弱", bestName, weakName));
2472
+        } else if (bestName != null) {
2473
+            result.setNote(String.format("[%s]掌握程度最好", bestName));
2474
+        }
2475
+
2476
+        return result;
2477
+    }
2478
+
2479
+    /**
2480
+     * 计算多个用户的正确率Map
2481
+     */
2482
+    private Map<Long, BigDecimal> calculateUsersAccuracyMap(List<Long> userIds, DateRange dateRange) {
2483
+        Map<Long, BigDecimal> result = new HashMap<>();
2484
+        for (Long userId : userIds) {
2485
+            AccuracyData data = calculateUserAccuracy(userId, dateRange);
2486
+            result.put(userId, data.accuracy);
2487
+        }
2488
+        return result;
2489
+    }
2490
+
2491
+    /**
2492
+     * 计算多个班组的正确率Map
2493
+     */
2494
+    private Map<Long, BigDecimal> calculateTeamsAccuracyMap(List<Long> teamIds, DateRange dateRange) {
2495
+        Map<Long, BigDecimal> result = new HashMap<>();
2496
+        for (Long teamId : teamIds) {
2497
+            List<Long> teamUserIds = getTeamUserIds(teamId);
2498
+            AccuracyData data = calculateGroupAccuracy(teamUserIds, dateRange);
2499
+            result.put(teamId, data.accuracy);
2500
+        }
2501
+        return result;
2502
+    }
2503
+
2504
+    /**
2505
+     * 计算多个科级的正确率Map
2506
+     */
2507
+    private Map<Long, BigDecimal> calculateDeptsAccuracyMap(List<Long> deptIds, DateRange dateRange) {
2508
+        Map<Long, BigDecimal> result = new HashMap<>();
2509
+        for (Long deptId : deptIds) {
2510
+            List<Long> deptUserIds = getDeptInspectorsAndLeaders(deptId);
2511
+            AccuracyData data = calculateGroupAccuracy(deptUserIds, dateRange);
2512
+            result.put(deptId, data.accuracy);
2513
+        }
2514
+        return result;
2515
+    }
2516
+
2517
+    /**
2518
+     * 计算个人正确率
2519
+     */
2520
+    private AccuracyData calculateUserAccuracy(Long userId, DateRange dateRange) {
2521
+        // 查询用户在时间范围内已完成的任务
2522
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
2523
+        taskWrapper.eq(DailyTask::getDtUserId, userId);
2524
+        taskWrapper.ge(DailyTask::getDtBusinessDate, java.sql.Date.valueOf(dateRange.startDate));
2525
+        taskWrapper.le(DailyTask::getDtBusinessDate, java.sql.Date.valueOf(dateRange.endDate));
2526
+        taskWrapper.eq(DailyTask::getDtStatus, "COMPLETED");
2527
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
2528
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
2529
+
2530
+        if (tasks.isEmpty()) {
2531
+            return new AccuracyData(BigDecimal.ZERO, 0, 0);
2532
+        }
2533
+
2534
+        // 查询所有题目明细
2535
+        List<String> taskIds = tasks.stream().map(DailyTask::getDtId).collect(Collectors.toList());
2536
+        LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
2537
+        detailWrapper.in(DailyTaskDetail::getDtdTaskId, taskIds);
2538
+        detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
2539
+        detailWrapper.isNotNull(DailyTaskDetail::getDtdIsCorrect);
2540
+        List<DailyTaskDetail> details = dailyTaskDetailMapper.selectList(detailWrapper);
2541
+
2542
+        int totalCount = details.size();
2543
+        int correctCount = (int) details.stream()
2544
+                .filter(d -> Boolean.TRUE.equals(d.getDtdIsCorrect()))
2545
+                .count();
2546
+
2547
+        BigDecimal accuracy = totalCount > 0
2548
+                ? new BigDecimal(correctCount).multiply(new BigDecimal(100))
2549
+                .divide(new BigDecimal(totalCount), 2, RoundingMode.HALF_UP)
2550
+                : BigDecimal.ZERO;
2551
+
2552
+        return new AccuracyData(accuracy, correctCount, totalCount);
2553
+    }
2554
+
2555
+    /**
2556
+     * 计算群组正确率
2557
+     */
2558
+    private AccuracyData calculateGroupAccuracy(List<Long> userIds, DateRange dateRange) {
2559
+        if (userIds.isEmpty()) {
2560
+            return new AccuracyData(BigDecimal.ZERO, 0, 0);
2561
+        }
2562
+
2563
+        // 查询所有用户在时间范围内已完成的任务
2564
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
2565
+        taskWrapper.in(DailyTask::getDtUserId, userIds);
2566
+        taskWrapper.ge(DailyTask::getDtBusinessDate, java.sql.Date.valueOf(dateRange.startDate));
2567
+        taskWrapper.le(DailyTask::getDtBusinessDate, java.sql.Date.valueOf(dateRange.endDate));
2568
+        taskWrapper.eq(DailyTask::getDtStatus, "COMPLETED");
2569
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
2570
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
2571
+
2572
+        if (tasks.isEmpty()) {
2573
+            return new AccuracyData(BigDecimal.ZERO, 0, 0);
2574
+        }
2575
+
2576
+        // 查询所有题目明细
2577
+        List<String> taskIds = tasks.stream().map(DailyTask::getDtId).collect(Collectors.toList());
2578
+        LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
2579
+        detailWrapper.in(DailyTaskDetail::getDtdTaskId, taskIds);
2580
+        detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
2581
+        detailWrapper.isNotNull(DailyTaskDetail::getDtdIsCorrect);
2582
+        List<DailyTaskDetail> details = dailyTaskDetailMapper.selectList(detailWrapper);
2583
+
2584
+        int totalCount = details.size();
2585
+        int correctCount = (int) details.stream()
2586
+                .filter(d -> Boolean.TRUE.equals(d.getDtdIsCorrect()))
2587
+                .count();
2588
+
2589
+        BigDecimal accuracy = totalCount > 0
2590
+                ? new BigDecimal(correctCount).multiply(new BigDecimal(100))
2591
+                .divide(new BigDecimal(totalCount), 2, RoundingMode.HALF_UP)
2592
+                : BigDecimal.ZERO;
2593
+
2594
+        return new AccuracyData(accuracy, correctCount, totalCount);
2595
+    }
2596
+
2597
+    /**
2598
+     * 获取班组所有成员ID列表
2599
+     * 注意:使用直接SQL查询,绕过@DataScope数据权限过滤,以获取全部用户数据用于统计
2600
+     */
2601
+    private List<Long> getTeamUserIds(Long teamId) {
2602
+        String sql = "SELECT user_id FROM sys_user WHERE dept_id = ? AND del_flag = '0'";
2603
+        return jdbcTemplate.queryForList(sql, Long.class, teamId);
2604
+    }
2605
+
2606
+
2607
+    /**
2608
+     * 获取科级下所有安检员和班组长ID列表
2609
+     * 使用 selectUserListByRoleKeyAndDeptId 方法一次性查询,避免循环读库
2610
+     */
2611
+    private List<Long> getDeptInspectorsAndLeaders(Long deptId) {
2612
+        // 使用角色标识查询,一次性获取该部门(包含子部门)下所有安检员和班组长
2613
+        List<SysUser> users = userService.selectUserListByRoleKeyAndDeptId(
2614
+                Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
2615
+                deptId
2616
+        );
2617
+        return users.stream()
2618
+                .map(SysUser::getUserId)
2619
+                .distinct()
2620
+                .collect(Collectors.toList());
2621
+    }
2622
+
2623
+    /**
2624
+     * 时间范围
2625
+     */
2626
+    private static class DateRange {
2627
+        String startDate;
2628
+        String endDate;
2629
+    }
2630
+
2631
+    /**
2632
+     * 正确率数据
2633
+     */
2634
+    private static class AccuracyData {
2635
+        BigDecimal accuracy;     // 正确率
2636
+        Integer correctCount;    // 正确题数
2637
+        Integer totalCount;      // 总题数
2638
+
2639
+        AccuracyData(BigDecimal accuracy, Integer correctCount, Integer totalCount) {
2640
+            this.accuracy = accuracy;
2641
+            this.correctCount = correctCount;
2642
+            this.totalCount = totalCount;
2643
+        }
2644
+    }
2645
+
2378 2646
 }

+ 29 - 7
airport-exam/src/main/resources/mapper/exam/DailyTaskDetailMapper.xml

@@ -1,17 +1,16 @@
1 1
 <?xml version="1.0" encoding="UTF-8" ?>
2 2
 <!DOCTYPE mapper
3
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 5
 <mapper namespace="com.sundot.airport.exam.mapper.DailyTaskDetailMapper">
6 6
 
7 7
     <!-- 统计用户在指定时间范围内的错题数(按二级分类) -->
8 8
     <select id="countWrongAnswersByModule" resultType="com.sundot.airport.common.dto.ModuleStatisticsDTO">
9
-        SELECT
10
-            dtd.dtd_module_id AS moduleId,
11
-            dtd.dtd_module_name AS moduleName,
12
-            COUNT(*) AS wrongCount
9
+        SELECT dtd.dtd_module_id   AS moduleId,
10
+               dtd.dtd_module_name AS moduleName,
11
+               COUNT(*)            AS wrongCount
13 12
         FROM edu_cs_daily_task_detail dtd
14
-        INNER JOIN edu_cs_daily_task dt ON dtd.dtd_task_id = dt.dt_id
13
+                 INNER JOIN edu_cs_daily_task dt ON dtd.dtd_task_id = dt.dt_id
15 14
         WHERE dt.dt_user_id = #{userId}
16 15
           AND dt.dt_business_date &gt;= #{startDate}
17 16
           AND dt.dt_business_date &lt;= #{endDate}
@@ -21,4 +20,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
21 20
         GROUP BY dtd.dtd_module_id, dtd.dtd_module_name
22 21
     </select>
23 22
 
23
+    <!-- 按题目分类统计全站正确率(关联edu_cs_ques和edu_cs_ques_cat) -->
24
+    <select id="selectCorrectRateByCategory" resultType="com.sundot.airport.common.dto.CategoryAccuracyItemDto">
25
+        SELECT qc.qc_name                                              AS categoryName,
26
+               SUM(CASE WHEN dtd.dtd_is_correct = 1 THEN 1 ELSE 0 END) AS correctCount,
27
+               COUNT(*)                                                AS totalCount,
28
+               ROUND(
29
+                       SUM(CASE WHEN dtd.dtd_is_correct = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*),
30
+                       2
31
+               )                                                       AS correctRate
32
+        FROM edu_cs_daily_task_detail dtd
33
+                 INNER JOIN edu_cs_daily_task dt ON dtd.dtd_task_id = dt.dt_id
34
+                 INNER JOIN edu_cs_ques q ON dtd.dtd_ques_id = q.qu_id
35
+                 INNER JOIN edu_cs_ques_cat qc ON q.qc_id = qc.qc_id
36
+        WHERE dt.dt_business_date &gt;= #{startDate}
37
+          AND dt.dt_business_date &lt;= #{endDate}
38
+          AND dtd.dtd_is_correct IS NOT NULL
39
+          AND dt.del_status = 0
40
+          AND dtd.del_status = 0
41
+          AND q.del_status = 0
42
+          AND qc.del_status = 0
43
+        GROUP BY qc.qc_id, qc.qc_name
44
+        ORDER BY correctRate DESC
45
+    </select>
24 46
 </mapper>

+ 25 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/ItemLargeScreenWorkDurationDto.java

@@ -0,0 +1,25 @@
1
+package com.sundot.airport.item.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+
7
+/**
8
+ * 在岗时长DTO
9
+ *
10
+ * @author ruoyi
11
+ * @date 2025-07-08
12
+ */
13
+@Data
14
+public class ItemLargeScreenWorkDurationDto {
15
+
16
+    /**
17
+     * 用户ID
18
+     */
19
+    private Long id;
20
+
21
+    /**
22
+     * 工作时长(分钟)
23
+     */
24
+    private BigDecimal workDuration;
25
+}

+ 51 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/SeizureEfficiencyDto.java

@@ -0,0 +1,51 @@
1
+package com.sundot.airport.item.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+import java.util.List;
7
+
8
+/**
9
+ * 查获效率统计DTO
10
+ *
11
+ * @author ruoyi
12
+ * @date 2025-07-25
13
+ */
14
+@Data
15
+public class SeizureEfficiencyDto {
16
+
17
+    /**
18
+     * 总查获数量
19
+     */
20
+    private BigDecimal totalSeizureCount;
21
+
22
+    /**
23
+     * 总在岗时长(分钟)
24
+     */
25
+    private BigDecimal totalWorkDuration;
26
+
27
+    /**
28
+     * 查获效率(查获数量/在岗时长)
29
+     */
30
+    private BigDecimal efficiency;
31
+
32
+    /**
33
+     * 大队查获效率排行
34
+     */
35
+    private List<SeizureEfficiencyRankDto> brigadeRankList;
36
+
37
+    /**
38
+     * 科室查获效率排行
39
+     */
40
+    private List<SeizureEfficiencyRankDto> departmentRankList;
41
+
42
+    /**
43
+     * 班组查获效率排行
44
+     */
45
+    private List<SeizureEfficiencyRankDto> teamRankList;
46
+
47
+    /**
48
+     * 安检员查获效率排行
49
+     */
50
+    private List<SeizureEfficiencyRankDto> individualRankList;
51
+}

+ 45 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/SeizureEfficiencyRankDto.java

@@ -0,0 +1,45 @@
1
+package com.sundot.airport.item.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+
7
+/**
8
+ * 查获效率排行DTO
9
+ *
10
+ * @author ruoyi
11
+ * @date 2025-07-25
12
+ */
13
+@Data
14
+public class SeizureEfficiencyRankDto {
15
+
16
+    /**
17
+     * ID(科室ID/班组ID/个人ID)
18
+     */
19
+    private Long id;
20
+
21
+    /**
22
+     * 名称(科室名称/班组名称/个人姓名)
23
+     */
24
+    private String name;
25
+
26
+    /**
27
+     * 查获数量
28
+     */
29
+    private BigDecimal seizureCount;
30
+
31
+    /**
32
+     * 在岗时长(分钟)
33
+     */
34
+    private BigDecimal workDuration;
35
+
36
+    /**
37
+     * 效率(查获数量/在岗时长)
38
+     */
39
+    private BigDecimal efficiency;
40
+
41
+    /**
42
+     * 排名
43
+     */
44
+    private Integer rank;
45
+}

+ 57 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/dto/ConcealmentPositionTop1DTO.java

@@ -0,0 +1,57 @@
1
+package com.sundot.airport.item.domain.dto;
2
+
3
+import com.sundot.airport.common.annotation.Excel;
4
+import io.swagger.annotations.ApiModel;
5
+import io.swagger.annotations.ApiModelProperty;
6
+import lombok.Data;
7
+
8
+import java.io.Serializable;
9
+
10
+/**
11
+ * 隐匿重点部位 Top1 统计DTO
12
+ * 统计隐匿夹带违禁品被查获最多的部位(一级和二级)
13
+ *
14
+ * @author ruoyi
15
+ * @date 2026-03-09
16
+ */
17
+@Data
18
+@ApiModel("隐匿重点部位 Top1 统计DTO")
19
+public class ConcealmentPositionTop1DTO implements Serializable {
20
+
21
+    private static final long serialVersionUID = 1L;
22
+
23
+    /**
24
+     * 检查部位编码(一级)
25
+     */
26
+    @ApiModelProperty("检查部位编码(一级)")
27
+    @Excel(name = "检查部位编码(一级)")
28
+    private String checkPositionCodeOne;
29
+
30
+    /**
31
+     * 检查部位名称(一级)
32
+     */
33
+    @ApiModelProperty("检查部位名称(一级)")
34
+    @Excel(name = "检查部位名称(一级)")
35
+    private String checkPositionNameOne;
36
+
37
+    /**
38
+     * 检查部位编码(二级)
39
+     */
40
+    @ApiModelProperty("检查部位编码(二级)")
41
+    @Excel(name = "检查部位编码(二级)")
42
+    private String checkPositionCodeTwo;
43
+
44
+    /**
45
+     * 检查部位名称(二级)
46
+     */
47
+    @ApiModelProperty("检查部位名称(二级)")
48
+    @Excel(name = "检查部位名称(二级)")
49
+    private String checkPositionNameTwo;
50
+
51
+    /**
52
+     * 查获数量
53
+     */
54
+    @ApiModelProperty("查获数量")
55
+    @Excel(name = "查获数量")
56
+    private Integer quantity;
57
+}

+ 51 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/dto/ProhibitedItemsTop3DTO.java

@@ -0,0 +1,51 @@
1
+package com.sundot.airport.item.domain.dto;
2
+
3
+import io.swagger.annotations.ApiModel;
4
+import io.swagger.annotations.ApiModelProperty;
5
+import lombok.Data;
6
+
7
+import java.io.Serializable;
8
+import java.math.BigDecimal;
9
+
10
+/**
11
+ * 查获违禁品 TOP3 统计 DTO
12
+ *
13
+ * @author ruoyi
14
+ * @date 2026-03-06
15
+ */
16
+@Data
17
+@ApiModel("查获违禁品 TOP3 统计 DTO")
18
+public class ProhibitedItemsTop3DTO implements Serializable {
19
+
20
+    private static final long serialVersionUID = 1L;
21
+
22
+    /**
23
+     * 物品分类编码
24
+     */
25
+    @ApiModelProperty("物品分类编码")
26
+    private String categoryCode;
27
+
28
+    /**
29
+     * 物品分类名称
30
+     */
31
+    @ApiModelProperty("物品分类名称")
32
+    private String categoryName;
33
+
34
+    /**
35
+     * 查获数量
36
+     */
37
+    @ApiModelProperty("查获数量")
38
+    private BigDecimal quantity;
39
+
40
+    /**
41
+     * 占比(百分比)
42
+     */
43
+    @ApiModelProperty("占比(%)")
44
+    private BigDecimal percentage;
45
+
46
+    /**
47
+     * 排名
48
+     */
49
+    @ApiModelProperty("排名")
50
+    private Integer rank;
51
+}

+ 64 - 0
airport-item/src/main/java/com/sundot/airport/item/mapper/ItemLargeScreenMapper.java

@@ -2,6 +2,9 @@ package com.sundot.airport.item.mapper;
2 2
 
3 3
 import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
4 4
 import com.sundot.airport.item.domain.*;
5
+import com.sundot.airport.item.domain.dto.ConcealmentPositionTop1DTO;
6
+import com.sundot.airport.item.domain.dto.ProhibitedItemsTop3DTO;
7
+import org.apache.ibatis.annotations.Param;
5 8
 
6 9
 import java.math.BigDecimal;
7 10
 import java.util.List;
@@ -133,4 +136,65 @@ public interface ItemLargeScreenMapper {
133 136
      * @return 按天统计的查获数据
134 137
      */
135 138
     List<ItemLargeScreenDailyTrendDto> dailyTrend(BaseLargeScreenQueryParamDto dto);
139
+
140
+    /**
141
+     * 获取在岗时长
142
+     *
143
+     * @param dto 查询参数
144
+     * @return 在岗时长(分钟)
145
+     */
146
+    BigDecimal getWorkDuration(BaseLargeScreenQueryParamDto dto);
147
+
148
+    /**
149
+     * 获取大队在岗时长
150
+     *
151
+     * @param dto 查询参数
152
+     * @return 大队在岗时长(分钟)
153
+     */
154
+    BigDecimal getBrigadeWorkDuration(BaseLargeScreenQueryParamDto dto);
155
+
156
+    /**
157
+     * 获取科室在岗时长
158
+     *
159
+     * @param dto 查询参数
160
+     * @return 科室在岗时长(分钟)
161
+     */
162
+    BigDecimal getDepartmentWorkDuration(BaseLargeScreenQueryParamDto dto);
163
+
164
+    /**
165
+     * 批量获取个人在岗时长
166
+     *
167
+     * @param startDate 开始日期
168
+     * @param endDate   结束日期
169
+     * @param userIds   用户ID列表
170
+     * @return 用户ID到工作时长的映射
171
+     */
172
+    List<ItemLargeScreenWorkDurationDto> getBatchIndividualWorkDuration(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("userIds") List<Long> userIds);
173
+
174
+    /**
175
+     * 批量获取班组在岗时长
176
+     *
177
+     * @param startDate 开始日期
178
+     * @param endDate   结束日期
179
+     * @param teamIds   班组ID列表
180
+     * @return 班组ID到工作时长的映射
181
+     */
182
+    List<ItemLargeScreenWorkDurationDto> getBatchTeamWorkDuration(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("teamIds") List<Long> teamIds);
183
+
184
+    /**
185
+     * 查询全站查获违禁品 TOP3
186
+     *
187
+     * @param dto 查询参数
188
+     */
189
+    List<ProhibitedItemsTop3DTO> selectProhibitedItemsTop3(BaseLargeScreenQueryParamDto dto);
190
+
191
+    /**
192
+     * 查询隐匿重点部位 Top1
193
+     * 统计隐匿夹带(is_active_concealment=1)的违禁品被查获最多的部位(一级和二级)
194
+     *
195
+     * @param dto 查询参数
196
+     * @return 隐匿重点部位 Top1,包含一级部位、二级部位、查获数量
197
+     */
198
+    List<ConcealmentPositionTop1DTO> selectConcealmentPositionTop1(BaseLargeScreenQueryParamDto dto);
199
+
136 200
 }

+ 2 - 0
airport-item/src/main/java/com/sundot/airport/item/mapper/SeizureReportMapper.java

@@ -410,4 +410,6 @@ public interface SeizureReportMapper {
410 410
      * @return 首页-巡检单
411 411
      */
412 412
     public List<ItemLargeScreenHomePageSeizureReportSqlDto> homePageSeizureReport(BaseLargeScreenQueryParamDto dto);
413
+
414
+    List<ItemLargeScreenHomePageSeizureReportSqlDto> homePageSeizureReportBrigadeId(BaseLargeScreenQueryParamDto dto);
413 415
 }

+ 423 - 0
airport-item/src/main/java/com/sundot/airport/item/service/SeizureDistributionService.java

@@ -0,0 +1,423 @@
1
+package com.sundot.airport.item.service;
2
+
3
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
4
+import com.sundot.airport.common.core.domain.BoxPlotDataDto;
5
+import com.sundot.airport.common.core.domain.GroupedBoxPlotDataDto;
6
+import com.sundot.airport.common.core.domain.SeizureLargeScreenHomePageItemDto;
7
+import com.sundot.airport.common.core.domain.entity.SysDept;
8
+import com.sundot.airport.common.enums.DeptTypeEnum;
9
+import com.sundot.airport.common.utils.DateUtils;
10
+import com.sundot.airport.item.mapper.SeizureReportMapper;
11
+import com.sundot.airport.system.service.ISysDeptService;
12
+import com.sundot.airport.system.service.ISysUserService;
13
+import org.springframework.beans.factory.annotation.Autowired;
14
+import org.springframework.stereotype.Service;
15
+
16
+import java.math.BigDecimal;
17
+import java.util.ArrayList;
18
+import java.util.Collections;
19
+import java.util.Date;
20
+import java.util.HashMap;
21
+import java.util.List;
22
+import java.util.Map;
23
+import java.util.Objects;
24
+import java.util.stream.Collectors;
25
+
26
+/**
27
+ * 查获数量分布服务
28
+ * 实现直方图数据计算逻辑
29
+ *
30
+ * @author ruoyi
31
+ * @date 2026-02-24
32
+ */
33
+@Service
34
+public class SeizureDistributionService {
35
+
36
+
37
+    @Autowired
38
+    private SeizureReportService seizureReportService;
39
+
40
+    @Autowired
41
+    private ISysDeptService sysDeptService;
42
+
43
+    @Autowired
44
+    private ISysUserService sysUserService;
45
+
46
+    @Autowired
47
+    private SeizureReportMapper seizureReportMapper;
48
+
49
+
50
+    /**
51
+     * 计算查获数量分布直方图数据
52
+     * 分组规则:
53
+     * 1. 确定数据的极差(R) = 最大值 - 最小值
54
+     * 2. 确定组距(h) = R/7
55
+     * 4. 按界限值分组统计查获数量
56
+     *
57
+     * @return 直方图数据列表,每个元素包含:组下限、组上限、该组内安检员人数
58
+     */
59
+    public List<SeizureHistogramData> calculateSeizureDistribution(BaseLargeScreenQueryParamDto dto) {
60
+        // 获取全站范围内所有安检员的查获数量数据
61
+        List<SeizureLargeScreenHomePageItemDto> allData = getSeizureDataForStation(dto.getDeptId(), dto.getStartDate(), dto.getEndDate());
62
+
63
+        if (allData == null || allData.isEmpty()) {
64
+            return Collections.emptyList();
65
+        }
66
+
67
+        List<BigDecimal> seizureQuantities = allData.stream()
68
+                .map(SeizureLargeScreenHomePageItemDto::getQuantity)
69
+                .filter(Objects::nonNull)
70
+                .collect(Collectors.toList());
71
+
72
+        if (seizureQuantities.isEmpty()) {
73
+            return Collections.emptyList();
74
+        }
75
+
76
+        // 1. 确定数据的极差(R) = 最大值 - 最小值
77
+        BigDecimal maxQuantity = seizureQuantities.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
78
+        BigDecimal minQuantity = seizureQuantities.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
79
+        BigDecimal range = maxQuantity.subtract(minQuantity);
80
+
81
+        // 2. 确定组距(h) = R/7
82
+        BigDecimal groupWidth = range.divide(new BigDecimal(7), 2, java.math.RoundingMode.HALF_UP);
83
+
84
+        // 如果极差为0(所有数据相同),则使用默认组距
85
+        if (range.compareTo(BigDecimal.ZERO) <= 0) {
86
+            groupWidth = new BigDecimal("10");
87
+            minQuantity = minQuantity.subtract(new BigDecimal("5"));
88
+        }
89
+
90
+        // 3. 确定各组的界限值
91
+        BigDecimal firstLowerBound = minQuantity;
92
+
93
+        // 创建7个组的边界
94
+        List<SeizureHistogramGroup> groups = new ArrayList<>();
95
+        BigDecimal currentLowerBound = firstLowerBound;
96
+
97
+        for (int i = 0; i < 7; i++) {
98
+            BigDecimal currentUpperBound = currentLowerBound.add(groupWidth);
99
+            // 对于最后一组,确保上界至少包含最大值
100
+            if (i == 6) {
101
+                BigDecimal adjustedUpperBound = currentUpperBound;
102
+                // 确保最后一组上界至少等于最大值
103
+                if (adjustedUpperBound.compareTo(maxQuantity) < 0) {
104
+                    adjustedUpperBound = maxQuantity;
105
+                }
106
+                groups.add(new SeizureHistogramGroup(currentLowerBound, adjustedUpperBound));
107
+            } else {
108
+                groups.add(new SeizureHistogramGroup(currentLowerBound, currentUpperBound));
109
+            }
110
+            currentLowerBound = currentUpperBound;
111
+        }
112
+
113
+        // 4. 按界限值分组统计安检员人数
114
+        List<SeizureHistogramData> result = new ArrayList<>();
115
+        for (int i = 0; i < groups.size(); i++) {
116
+            SeizureHistogramGroup group = groups.get(i);
117
+            int count = 0;
118
+            for (BigDecimal quantity : seizureQuantities) {
119
+                // 最后一组:左闭右闭区间 [lower, upper]
120
+                if (i == groups.size() - 1) {
121
+                    if (quantity.compareTo(group.lowerBound) >= 0 && quantity.compareTo(group.upperBound) <= 0) {
122
+                        count++;
123
+                    }
124
+                } else {
125
+                    // 其他组:左闭右开区间 [lower, upper)
126
+                    if (quantity.compareTo(group.lowerBound) >= 0 && quantity.compareTo(group.upperBound) < 0) {
127
+                        count++;
128
+                    }
129
+                }
130
+            }
131
+            result.add(new SeizureHistogramData(
132
+                    group.lowerBound.setScale(2, java.math.RoundingMode.HALF_UP),
133
+                    group.upperBound.setScale(2, java.math.RoundingMode.HALF_UP),
134
+                    count
135
+            ));
136
+        }
137
+
138
+        return result;
139
+    }
140
+
141
+    /**
142
+     * 获取指定站点的查获数据(全站级别)
143
+     */
144
+    private List<SeizureLargeScreenHomePageItemDto> getSeizureDataForStation(Long deptId, Date startDate, Date endDate) {
145
+        // 创建查询参数
146
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
147
+        dto.setDeptId(deptId);
148
+        dto.setStartDate(startDate);
149
+        dto.setEndDate(endDate);
150
+
151
+        return seizureReportService.getSeizureLargeScreenHomePageItemDtoList(dto);
152
+    }
153
+
154
+    /**
155
+     * 获取指定科室的查获数据(科室级别)
156
+     *
157
+     * @param brigadeId 科室ID
158
+     * @param startDate 开始日期
159
+     * @param endDate   结束日期
160
+     * @return 科室级别查获数据列表
161
+     */
162
+    private List<SeizureLargeScreenHomePageItemDto> getSeizureDataForBrigade(Long brigadeId, Date startDate, Date endDate) {
163
+        if (brigadeId == null) {
164
+            return Collections.emptyList();
165
+        }
166
+        // 创建查询参数
167
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
168
+        dto.setInspectBrigadeId(brigadeId);
169
+        dto.setStartDate(startDate);
170
+        dto.setEndDate(DateUtils.addDays(endDate, 1));
171
+        return seizureReportService.getSeizureLargeScreenHomePinspectBrigadeIdList(dto);
172
+    }
173
+
174
+    /**
175
+     * 直方图数据类
176
+     */
177
+    public static class SeizureHistogramData {
178
+        /**
179
+         * 直方图组的下限值
180
+         */
181
+        private BigDecimal lowerBound;
182
+        /**
183
+         * 直方图组的上限值
184
+         */
185
+        private BigDecimal upperBound;
186
+        /**
187
+         * 该组内的数据数量
188
+         */
189
+        private int count;
190
+
191
+        public SeizureHistogramData(BigDecimal lowerBound, BigDecimal upperBound, int count) {
192
+            this.lowerBound = lowerBound;
193
+            this.upperBound = upperBound;
194
+            this.count = count;
195
+        }
196
+
197
+        public BigDecimal getLowerBound() {
198
+            return lowerBound;
199
+        }
200
+
201
+        public void setLowerBound(BigDecimal lowerBound) {
202
+            this.lowerBound = lowerBound;
203
+        }
204
+
205
+        public BigDecimal getUpperBound() {
206
+            return upperBound;
207
+        }
208
+
209
+        public void setUpperBound(BigDecimal upperBound) {
210
+            this.upperBound = upperBound;
211
+        }
212
+
213
+        public int getCount() {
214
+            return count;
215
+        }
216
+
217
+        public void setCount(int count) {
218
+            this.count = count;
219
+        }
220
+    }
221
+
222
+    /**
223
+     * 组边界类
224
+     */
225
+    private static class SeizureHistogramGroup {
226
+        BigDecimal lowerBound;
227
+        BigDecimal upperBound;
228
+
229
+        SeizureHistogramGroup(BigDecimal lowerBound, BigDecimal upperBound) {
230
+            this.lowerBound = lowerBound;
231
+            this.upperBound = upperBound;
232
+        }
233
+    }
234
+
235
+    /**
236
+     * 计算箱线图数据
237
+     * 箱线图共由五个数值点构成,分别是最小观察值,25%分位数(Q1),中位数,75%分位数(Q3),最大观察值。
238
+     * 最小观察值=Q1-1.5(IQR),IQR=Q3-Q1
239
+     * 最大观察值=Q3+1.5(IQR),IQR=Q3-Q1
240
+     * 箱线图中,下限为最小观察值与真实最小值之间的较大值;上限为最大观察值与最大值之间的较小值。
241
+     * 如果数据存在离群点即异常值,他们大于超出最大或者最小观察值,此时此将离群点以"圆点"形式进行展示。
242
+     *
243
+     * @param dto 查询参数
244
+     * @return 箱线图数据列表(按科室分组)
245
+     */
246
+    public List<GroupedBoxPlotDataDto> calculateBoxPlotData(BaseLargeScreenQueryParamDto dto) {
247
+        if (dto.getInspectStationId() == null) {
248
+            return Collections.emptyList();
249
+        }
250
+
251
+        // 获取站点信息(站级)
252
+        SysDept stationDept = sysDeptService.selectDeptById(dto.getInspectStationId());
253
+        if (stationDept == null) {
254
+            return Collections.emptyList();
255
+        }
256
+
257
+        // 获取该站点下的所有大队
258
+        List<SysDept> brigades = getBrigadesByStation(dto.getInspectStationId());
259
+
260
+        // 构建所有分组:先添加站级,再添加大队级
261
+        List<SysDept> allGroups = new ArrayList<>();
262
+        allGroups.add(stationDept); // 添加站级
263
+        allGroups.addAll(brigades); // 添加大队级
264
+
265
+        // 获取查获数量数据
266
+        List<SeizureLargeScreenHomePageItemDto> seizureReportList = getSeizureDataForStation(dto.getDeptId(), dto.getStartDate(), dto.getEndDate());
267
+        if (seizureReportList == null || seizureReportList.isEmpty()) {
268
+            return Collections.emptyList();
269
+        }
270
+
271
+        // 按部门分组数据
272
+        Map<String, List<BigDecimal>> groupedQuantities = new HashMap<>();
273
+
274
+        // 首先处理站级数据(全站范围)
275
+        List<BigDecimal> stationQuantities = new ArrayList<>();
276
+        for (SeizureLargeScreenHomePageItemDto report : seizureReportList) {
277
+            BigDecimal quantity = report.getQuantity();
278
+            if (quantity != null) {
279
+                stationQuantities.add(quantity);
280
+            }
281
+        }
282
+        groupedQuantities.put(stationDept.getDeptName(), stationQuantities);
283
+
284
+        // 然后处理大队级数据
285
+        for (SysDept dept : brigades) {
286
+            List<SeizureLargeScreenHomePageItemDto> brigadeSeizureReportList = getSeizureDataForBrigade(dept.getDeptId(), dto.getStartDate(), dto.getEndDate());
287
+            List<BigDecimal> deptQuantities = brigadeSeizureReportList.stream().map(SeizureLargeScreenHomePageItemDto::getQuantity).filter(Objects::nonNull).collect(Collectors.toList());
288
+            groupedQuantities.put(dept.getDeptName(), deptQuantities);
289
+        }
290
+
291
+        // 为每个分组计算箱线图数据
292
+        List<GroupedBoxPlotDataDto> result = new ArrayList<>();
293
+
294
+        for (SysDept group : allGroups) {
295
+            String groupName = group.getDeptName();
296
+            List<BigDecimal> quantities = groupedQuantities.getOrDefault(groupName, Collections.emptyList());
297
+
298
+            List<BigDecimal> seizureQuantities = quantities.stream()
299
+                    .filter(Objects::nonNull)
300
+                    .sorted()
301
+                    .collect(Collectors.toList());
302
+
303
+            BoxPlotDataDto boxPlotData = new BoxPlotDataDto();
304
+            if (seizureQuantities.isEmpty()) {
305
+                boxPlotData.setTotalCount(0);
306
+            } else {
307
+                int size = seizureQuantities.size();
308
+
309
+                // 计算四分位数
310
+                BigDecimal q1 = calculateQuartile(seizureQuantities, 0.25);
311
+                BigDecimal median = calculateQuartile(seizureQuantities, 0.5);
312
+                BigDecimal q3 = calculateQuartile(seizureQuantities, 0.75);
313
+
314
+                // 计算IQR
315
+                BigDecimal iqr = q3.subtract(q1);
316
+
317
+                // 计算最小观察值和最大观察值
318
+                BigDecimal minObservation = q1.subtract(iqr);
319
+                BigDecimal maxObservation = q3.add(iqr);
320
+
321
+                // 获取真实最小值和最大值
322
+                BigDecimal realMin = seizureQuantities.get(0);
323
+                BigDecimal realMax = seizureQuantities.get(size - 1);
324
+
325
+                // 确定箱线图的下限和上限
326
+                BigDecimal lowerBound = minObservation.compareTo(realMin) > 0 ? minObservation : realMin;
327
+                BigDecimal upperBound = maxObservation.compareTo(realMax) < 0 ? maxObservation : realMax;
328
+
329
+                // 识别离群点
330
+                List<BigDecimal> outliers = new ArrayList<>();
331
+                List<BigDecimal> nonOutliers = new ArrayList<>();
332
+
333
+                for (BigDecimal quantity : seizureQuantities) {
334
+                    if (quantity.compareTo(lowerBound) < 0 || quantity.compareTo(upperBound) > 0) {
335
+                        outliers.add(quantity);
336
+                    } else {
337
+                        nonOutliers.add(quantity);
338
+                    }
339
+                }
340
+
341
+                // 设置箱线图数据
342
+                boxPlotData.setMinObservation(minObservation);
343
+                boxPlotData.setQ1(q1);
344
+                boxPlotData.setMedian(median);
345
+                boxPlotData.setQ3(q3);
346
+                boxPlotData.setMaxObservation(maxObservation);
347
+                boxPlotData.setLowerBound(lowerBound);
348
+                boxPlotData.setUpperBound(upperBound);
349
+                boxPlotData.setOutliers(outliers);
350
+                boxPlotData.setNonOutliers(nonOutliers);
351
+                boxPlotData.setTotalCount(size);
352
+            }
353
+
354
+            result.add(new GroupedBoxPlotDataDto(groupName, boxPlotData));
355
+        }
356
+
357
+        return result;
358
+    }
359
+
360
+
361
+    /**
362
+     * 获取指定站点下的所有大队
363
+     *
364
+     * @param stationDeptId 站点部门ID
365
+     * @return 大队列表
366
+     */
367
+    private List<SysDept> getBrigadesByStation(Long stationDeptId) {
368
+        if (stationDeptId == null) {
369
+            return Collections.emptyList();
370
+        }
371
+
372
+        // 查询站点下的所有子部门
373
+        List<SysDept> childrenDepts = sysDeptService.selectChildrenDeptById(stationDeptId);
374
+
375
+        // 过滤出大队级别的部门
376
+        return childrenDepts.stream()
377
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
378
+                .collect(Collectors.toList());
379
+    }
380
+
381
+    /**
382
+     * 计算指定分位数的值
383
+     * 使用线性插值法
384
+     *
385
+     * @param data     数据列表(已排序)
386
+     * @param quartile 分位数(0.25, 0.5, 0.75)
387
+     * @return 分位数值
388
+     */
389
+    private BigDecimal calculateQuartile(List<BigDecimal> data, double quartile) {
390
+        int n = data.size();
391
+        if (n == 0) {
392
+            return BigDecimal.ZERO;
393
+        }
394
+
395
+        // 计算位置
396
+        double position = quartile * (n + 1);
397
+        int floorPosition = (int) Math.floor(position);
398
+        int ceilPosition = (int) Math.ceil(position);
399
+
400
+        // 如果位置是整数,直接取该位置的值
401
+        if (floorPosition == ceilPosition) {
402
+            if (floorPosition <= 0) {
403
+                return data.get(0);
404
+            } else if (floorPosition > n) {
405
+                return data.get(n - 1);
406
+            } else {
407
+                return data.get(floorPosition - 1);
408
+            }
409
+        }
410
+
411
+        // 否则使用线性插值
412
+        if (floorPosition <= 0) {
413
+            return data.get(0);
414
+        } else if (ceilPosition > n) {
415
+            return data.get(n - 1);
416
+        } else {
417
+            BigDecimal lowerValue = data.get(floorPosition - 1);
418
+            BigDecimal upperValue = data.get(ceilPosition - 1);
419
+            double fraction = position - floorPosition;
420
+            return lowerValue.add(upperValue.subtract(lowerValue).multiply(BigDecimal.valueOf(fraction)));
421
+        }
422
+    }
423
+}

+ 21 - 0
airport-item/src/main/java/com/sundot/airport/item/service/SeizureEfficiencyService.java

@@ -0,0 +1,21 @@
1
+package com.sundot.airport.item.service;
2
+
3
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
4
+import com.sundot.airport.item.domain.SeizureEfficiencyDto;
5
+
6
+/**
7
+ * 查获效率Service接口
8
+ *
9
+ * @author ruoyi
10
+ * @date 2025-07-25
11
+ */
12
+public interface SeizureEfficiencyService {
13
+
14
+    /**
15
+     * 查询查获效率统计
16
+     *
17
+     * @param dto 查询参数
18
+     * @return 查获效率统计结果
19
+     */
20
+    public SeizureEfficiencyDto getSeizureEfficiency(BaseLargeScreenQueryParamDto dto);
21
+}

+ 28 - 0
airport-item/src/main/java/com/sundot/airport/item/service/SeizureReportService.java

@@ -757,4 +757,32 @@ public class SeizureReportService {
757 757
         });
758 758
         return result;
759 759
     }
760
+
761
+    /**
762
+     * 查获上报大队级别全量数据
763
+     *
764
+     * @param dto 查询参数
765
+     * @return 查获上报全量数据
766
+     */
767
+    public List<SeizureLargeScreenHomePageItemDto> getSeizureLargeScreenHomePinspectBrigadeIdList(BaseLargeScreenQueryParamDto dto) {
768
+        List<SeizureLargeScreenHomePageItemDto> result = new ArrayList<>();
769
+        List<LargeScreenHomePageUserInfoSqlDto> userInfoListAll = sysUserService.homePageUserInfo();
770
+        if (CollUtil.isEmpty(userInfoListAll)) {
771
+            return Collections.emptyList();
772
+        }
773
+        userInfoListAll = userInfoListAll.stream().filter(userInfoSqlDto -> userInfoSqlDto.getBrigadeId().equals(dto.getInspectBrigadeId())).collect(Collectors.toList());
774
+        List<ItemLargeScreenHomePageSeizureReportSqlDto> seizureReportListAll = seizureReportMapper.homePageSeizureReportBrigadeId(dto);
775
+        if (CollUtil.isEmpty(seizureReportListAll)) {
776
+            return Collections.emptyList();
777
+        }
778
+        Map<Long, BigDecimal> groupingBy = seizureReportListAll.stream().collect(Collectors.groupingBy(ItemLargeScreenHomePageSeizureReportSqlDto::getUserId, Collectors.reducing(BigDecimal.ZERO, ItemLargeScreenHomePageSeizureReportSqlDto::getQuantity, BigDecimal::add)));
779
+        userInfoListAll.forEach(userInfoSqlDto -> {
780
+            SeizureLargeScreenHomePageItemDto itemDto = new SeizureLargeScreenHomePageItemDto();
781
+            itemDto.setId(userInfoSqlDto.getUserId());
782
+            itemDto.setName(userInfoSqlDto.getNickName());
783
+            itemDto.setQuantity(groupingBy.getOrDefault(userInfoSqlDto.getUserId(), BigDecimal.ZERO));
784
+            result.add(itemDto);
785
+        });
786
+        return result;
787
+    }
760 788
 }

+ 269 - 0
airport-item/src/main/java/com/sundot/airport/item/service/impl/SeizureEfficiencyServiceImpl.java

@@ -0,0 +1,269 @@
1
+package com.sundot.airport.item.service.impl;
2
+
3
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
4
+import com.sundot.airport.common.utils.DateUtils;
5
+import com.sundot.airport.item.domain.ItemLargeScreenWorkDurationDto;
6
+import com.sundot.airport.item.domain.SeizureEfficiencyDto;
7
+import com.sundot.airport.item.domain.SeizureEfficiencyRankDto;
8
+import com.sundot.airport.item.mapper.ItemLargeScreenMapper;
9
+import com.sundot.airport.item.service.SeizureEfficiencyService;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.stereotype.Service;
12
+
13
+import java.math.BigDecimal;
14
+import java.util.ArrayList;
15
+import java.util.List;
16
+import java.util.Map;
17
+import java.util.stream.Collectors;
18
+
19
+/**
20
+ * 查获效率Service业务层处理
21
+ *
22
+ * @author ruoyi
23
+ * @date 2025-07-25
24
+ */
25
+@Service
26
+public class SeizureEfficiencyServiceImpl implements SeizureEfficiencyService {
27
+
28
+    @Autowired
29
+    private ItemLargeScreenMapper itemLargeScreenMapper;
30
+
31
+    /**
32
+     * 查询查获效率统计
33
+     *
34
+     * @param dto 查询参数
35
+     * @return 查获效率统计结果
36
+     */
37
+    @Override
38
+    public SeizureEfficiencyDto getSeizureEfficiency(BaseLargeScreenQueryParamDto dto) {
39
+        SeizureEfficiencyDto result = new SeizureEfficiencyDto();
40
+        if (dto.getEndDate() != null) {
41
+            //结束时间减一天
42
+            dto.setEndDate(DateUtils.addDays(dto.getEndDate(), -1));
43
+        }
44
+
45
+        // 获取查获数量和在岗时长数据
46
+        BigDecimal totalSeizureCount = itemLargeScreenMapper.getTotalSum(dto);
47
+        BigDecimal totalWorkDuration = itemLargeScreenMapper.getWorkDuration(dto);
48
+
49
+        result.setTotalSeizureCount(totalSeizureCount);
50
+        result.setTotalWorkDuration(totalWorkDuration);
51
+
52
+        // 计算总效率
53
+        if (totalWorkDuration.compareTo(BigDecimal.ZERO) > 0) {
54
+            result.setEfficiency(totalSeizureCount.divide(totalWorkDuration, 4, BigDecimal.ROUND_HALF_UP));
55
+        } else {
56
+            result.setEfficiency(BigDecimal.ZERO);
57
+        }
58
+
59
+        // 获取大队效率排行
60
+        result.setBrigadeRankList(getBrigadeEfficiencyRank(dto));
61
+
62
+        // 获取科室效率排行
63
+        result.setDepartmentRankList(getDepartmentEfficiencyRank(dto));
64
+
65
+        // 获取班组效率排行
66
+        result.setTeamRankList(getTeamEfficiencyRank(dto));
67
+
68
+        // 获取个人效率排行
69
+        result.setIndividualRankList(getIndividualEfficiencyRank(dto));
70
+
71
+        return result;
72
+    }
73
+
74
+    /**
75
+     * 获取大队效率排行
76
+     */
77
+    private List<SeizureEfficiencyRankDto> getBrigadeEfficiencyRank(BaseLargeScreenQueryParamDto dto) {
78
+        // 获取大队查获数量
79
+        List<SeizureEfficiencyRankDto> brigadeSeizureList = itemLargeScreenMapper.rankByBrigade(dto)
80
+                .stream()
81
+                .map(item -> {
82
+                    SeizureEfficiencyRankDto rankDto = new SeizureEfficiencyRankDto();
83
+                    rankDto.setId(item.getId());
84
+                    rankDto.setName(item.getName());
85
+                    rankDto.setSeizureCount(item.getTotal());
86
+                    return rankDto;
87
+                })
88
+                .collect(Collectors.toList());
89
+
90
+        // 计算每个科室的效率
91
+        for (SeizureEfficiencyRankDto item : brigadeSeizureList) {
92
+            BaseLargeScreenQueryParamDto deptDto = new BaseLargeScreenQueryParamDto();
93
+            deptDto.setStartDate(dto.getStartDate());
94
+            deptDto.setEndDate(dto.getEndDate());
95
+            deptDto.setInspectBrigadeId(item.getId());
96
+
97
+            BigDecimal workDuration = itemLargeScreenMapper.getBrigadeWorkDuration(deptDto);
98
+            item.setWorkDuration(workDuration);
99
+
100
+            if (workDuration.compareTo(BigDecimal.ZERO) > 0) {
101
+                item.setEfficiency(item.getSeizureCount().divide(workDuration, 4, BigDecimal.ROUND_HALF_UP));
102
+            } else {
103
+                item.setEfficiency(BigDecimal.ZERO);
104
+            }
105
+        }
106
+
107
+        // 排序并设置排名
108
+        brigadeSeizureList.sort((a, b) -> b.getEfficiency().compareTo(a.getEfficiency()));
109
+        for (int i = 0; i < brigadeSeizureList.size(); i++) {
110
+            brigadeSeizureList.get(i).setRank(i + 1);
111
+        }
112
+
113
+        return brigadeSeizureList;
114
+    }
115
+
116
+    /**
117
+     * 获取科室效率排行
118
+     */
119
+    private List<SeizureEfficiencyRankDto> getDepartmentEfficiencyRank(BaseLargeScreenQueryParamDto dto) {
120
+        // 获取科室查获数量
121
+        List<SeizureEfficiencyRankDto> departmentSeizureList = itemLargeScreenMapper.rankByDepartment(dto)
122
+                .stream()
123
+                .map(item -> {
124
+                    SeizureEfficiencyRankDto rankDto = new SeizureEfficiencyRankDto();
125
+                    rankDto.setId(item.getId());
126
+                    rankDto.setName(item.getName());
127
+                    rankDto.setSeizureCount(item.getTotal());
128
+                    return rankDto;
129
+                })
130
+                .collect(Collectors.toList());
131
+
132
+        // 计算每个科室的效率
133
+        for (SeizureEfficiencyRankDto item : departmentSeizureList) {
134
+            BaseLargeScreenQueryParamDto deptDto = new BaseLargeScreenQueryParamDto();
135
+            deptDto.setStartDate(dto.getStartDate());
136
+            deptDto.setEndDate(dto.getEndDate());
137
+            deptDto.setInspectDepartmentId(item.getId());
138
+
139
+            BigDecimal workDuration = itemLargeScreenMapper.getDepartmentWorkDuration(deptDto);
140
+            item.setWorkDuration(workDuration);
141
+
142
+            if (workDuration.compareTo(BigDecimal.ZERO) > 0) {
143
+                item.setEfficiency(item.getSeizureCount().divide(workDuration, 4, BigDecimal.ROUND_HALF_UP));
144
+            } else {
145
+                item.setEfficiency(BigDecimal.ZERO);
146
+            }
147
+        }
148
+
149
+        // 排序并设置排名
150
+        departmentSeizureList.sort((a, b) -> b.getEfficiency().compareTo(a.getEfficiency()));
151
+        for (int i = 0; i < departmentSeizureList.size(); i++) {
152
+            departmentSeizureList.get(i).setRank(i + 1);
153
+        }
154
+
155
+        return departmentSeizureList;
156
+    }
157
+
158
+    /**
159
+     * 获取班组效率排行
160
+     */
161
+    private List<SeizureEfficiencyRankDto> getTeamEfficiencyRank(BaseLargeScreenQueryParamDto dto) {
162
+        // 获取班组查获数量
163
+        List<SeizureEfficiencyRankDto> teamSeizureList = itemLargeScreenMapper.rankByTeam(dto)
164
+                .stream()
165
+                .map(item -> {
166
+                    SeizureEfficiencyRankDto rankDto = new SeizureEfficiencyRankDto();
167
+                    rankDto.setId(item.getId());
168
+                    rankDto.setName(item.getName());
169
+                    rankDto.setSeizureCount(item.getTotal());
170
+                    return rankDto;
171
+                })
172
+                .collect(Collectors.toList());
173
+
174
+        // 如果没有数据,直接返回
175
+        if (teamSeizureList.isEmpty()) {
176
+            return teamSeizureList;
177
+        }
178
+
179
+        // 批量获取所有班组的工作时长
180
+        List<Long> teamIds = teamSeizureList.stream()
181
+                .map(SeizureEfficiencyRankDto::getId)
182
+                .collect(Collectors.toList());
183
+
184
+        List<ItemLargeScreenWorkDurationDto> workDurationList = itemLargeScreenMapper.getBatchTeamWorkDuration(
185
+                DateUtils.parseDateToStr("yyyy-MM-dd", dto.getStartDate()),
186
+                DateUtils.parseDateToStr("yyyy-MM-dd", dto.getEndDate()),
187
+                teamIds);
188
+
189
+        // 转换为Map方便查找
190
+        Map<Long, BigDecimal> workDurationMap = workDurationList.stream()
191
+                .collect(Collectors.toMap(ItemLargeScreenWorkDurationDto::getId, ItemLargeScreenWorkDurationDto::getWorkDuration));
192
+
193
+        // 计算每个班组的效率
194
+        for (SeizureEfficiencyRankDto item : teamSeizureList) {
195
+            BigDecimal workDuration = workDurationMap.getOrDefault(item.getId(), BigDecimal.ZERO);
196
+            item.setWorkDuration(workDuration);
197
+
198
+            if (workDuration.compareTo(BigDecimal.ZERO) > 0) {
199
+                item.setEfficiency(item.getSeizureCount().divide(workDuration, 4, BigDecimal.ROUND_HALF_UP));
200
+            } else {
201
+                item.setEfficiency(BigDecimal.ZERO);
202
+            }
203
+        }
204
+
205
+        // 排序并设置排名
206
+        teamSeizureList.sort((a, b) -> b.getEfficiency().compareTo(a.getEfficiency()));
207
+        for (int i = 0; i < teamSeizureList.size(); i++) {
208
+            teamSeizureList.get(i).setRank(i + 1);
209
+        }
210
+
211
+        return teamSeizureList;
212
+    }
213
+
214
+    /**
215
+     * 获取个人效率排行
216
+     */
217
+    private List<SeizureEfficiencyRankDto> getIndividualEfficiencyRank(BaseLargeScreenQueryParamDto dto) {
218
+        // 获取个人查获数量
219
+        List<SeizureEfficiencyRankDto> individualSeizureList = itemLargeScreenMapper.rankByIndividual(dto)
220
+                .stream()
221
+                .map(item -> {
222
+                    SeizureEfficiencyRankDto rankDto = new SeizureEfficiencyRankDto();
223
+                    rankDto.setId(item.getId());
224
+                    rankDto.setName(item.getName());
225
+                    rankDto.setSeizureCount(item.getTotal());
226
+                    return rankDto;
227
+                })
228
+                .collect(Collectors.toList());
229
+
230
+        // 如果没有数据,直接返回
231
+        if (individualSeizureList.isEmpty()) {
232
+            return individualSeizureList;
233
+        }
234
+
235
+        // 批量获取所有人员的工作时长
236
+        List<Long> userIds = individualSeizureList.stream()
237
+                .map(SeizureEfficiencyRankDto::getId)
238
+                .collect(Collectors.toList());
239
+
240
+        List<ItemLargeScreenWorkDurationDto> workDurationList = itemLargeScreenMapper.getBatchIndividualWorkDuration(
241
+                DateUtils.parseDateToStr("yyyy-MM-dd", dto.getStartDate()),
242
+                DateUtils.parseDateToStr("yyyy-MM-dd", dto.getEndDate()),
243
+                userIds);
244
+
245
+        // 转换为Map方便查找
246
+        Map<Long, BigDecimal> workDurationMap = workDurationList.stream()
247
+                .collect(Collectors.toMap(ItemLargeScreenWorkDurationDto::getId, ItemLargeScreenWorkDurationDto::getWorkDuration));
248
+
249
+        // 计算每个人的效率
250
+        for (SeizureEfficiencyRankDto item : individualSeizureList) {
251
+            BigDecimal workDuration = workDurationMap.getOrDefault(item.getId(), BigDecimal.ZERO);
252
+            item.setWorkDuration(workDuration);
253
+
254
+            if (workDuration.compareTo(BigDecimal.ZERO) > 0) {
255
+                item.setEfficiency(item.getSeizureCount().divide(workDuration, 4, BigDecimal.ROUND_HALF_UP));
256
+            } else {
257
+                item.setEfficiency(BigDecimal.ZERO);
258
+            }
259
+        }
260
+
261
+        // 排序并设置排名
262
+        individualSeizureList.sort((a, b) -> b.getEfficiency().compareTo(a.getEfficiency()));
263
+        for (int i = 0; i < individualSeizureList.size(); i++) {
264
+            individualSeizureList.get(i).setRank(i + 1);
265
+        }
266
+
267
+        return individualSeizureList;
268
+    }
269
+}

+ 130 - 0
airport-item/src/main/resources/mapper/item/ItemLargeScreenMapper.xml

@@ -515,4 +515,134 @@
515 515
         order by total desc
516 516
     </select>
517 517
 
518
+    <!--批量获取个人在岗时长-->
519
+    <select id="getBatchIndividualWorkDuration"
520
+            resultType="com.sundot.airport.item.domain.ItemLargeScreenWorkDurationDto">
521
+        select apr.user_id as id,
522
+        ifnull(sum(apr.work_duration), 0) as workDuration
523
+        from attendance_post_record apr
524
+        where apr.user_id in
525
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
526
+            #{userId}
527
+        </foreach>
528
+        and apr.work_duration > 0
529
+        <if test="startDate != null and endDate != null">
530
+            and apr.check_in_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
531
+            and apr.check_out_time >= #{startDate}
532
+        </if>
533
+        group by apr.user_id
534
+    </select>
535
+
536
+    <!--批量获取班组在岗时长-->
537
+    <select id="getBatchTeamWorkDuration" resultType="com.sundot.airport.item.domain.ItemLargeScreenWorkDurationDto">
538
+        select apr.attendance_team_id as id,
539
+        ifnull(sum(apr.work_duration), 0) as workDuration
540
+        from attendance_post_record apr
541
+        where apr.attendance_team_id in
542
+        <foreach collection="teamIds" item="teamId" open="(" separator="," close=")">
543
+            #{teamId}
544
+        </foreach>
545
+        and apr.work_duration > 0
546
+        <if test="startDate != null and endDate != null">
547
+            and apr.check_in_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
548
+            and apr.check_out_time >= #{startDate}
549
+        </if>
550
+        group by apr.attendance_team_id
551
+    </select>
552
+
553
+    <!--获取科室在岗时长-->
554
+    <select id="getDepartmentWorkDuration" resultType="java.math.BigDecimal">
555
+        select ifnull(sum(apr.work_duration), 0) as workDuration
556
+        from attendance_post_record apr
557
+        where 1 = 1
558
+        and apr.work_duration > 0
559
+        <if test="startDate != null and endDate != null">
560
+            and apr.check_in_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
561
+            and apr.check_out_time >= #{startDate}
562
+        </if>
563
+        <if test="inspectDepartmentId != null">
564
+            and apr.attendance_department_id = #{inspectDepartmentId}
565
+        </if>
566
+    </select>
567
+
568
+    <!--获取大队在岗时长-->
569
+    <select id="getBrigadeWorkDuration" resultType="java.math.BigDecimal">
570
+        select ifnull(sum(apr.work_duration), 0) as workDuration
571
+        from attendance_post_record apr
572
+        where 1 = 1
573
+        and apr.work_duration > 0
574
+        <if test="startDate != null and endDate != null">
575
+            and apr.check_in_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
576
+            and apr.check_out_time >= #{startDate}
577
+        </if>
578
+        <if test="inspectBrigadeId != null">
579
+            and apr.attendance_brigade_id = #{inspectBrigadeId}
580
+        </if>
581
+    </select>
582
+
583
+    <!--获取在岗时长-->
584
+    <select id="getWorkDuration" resultType="java.math.BigDecimal">
585
+        select ifnull(sum(apr.work_duration), 0) as workDuration
586
+        from attendance_post_record apr
587
+        where 1 = 1
588
+        and apr.work_duration > 0
589
+        <if test="startDate != null and endDate != null">
590
+            and apr.check_in_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
591
+            and apr.check_out_time >= #{startDate}
592
+        </if>
593
+        <if test="inspectBrigadeId != null">
594
+            and apr.attendance_brigade_id = #{inspectBrigadeId}
595
+        </if>
596
+        <if test="inspectDepartmentId != null">
597
+            and apr.attendance_department_id = #{inspectDepartmentId}
598
+        </if>
599
+        <if test="inspectTeamId != null">
600
+            and apr.attendance_team_id = #{inspectTeamId}
601
+        </if>
602
+    </select>
603
+
604
+    <!-- 查询全站查获违禁品 -->
605
+    <select id="selectProhibitedItemsTop3" resultType="com.sundot.airport.item.domain.dto.ProhibitedItemsTop3DTO">
606
+        SELECT
607
+        isi.category_code_two AS categoryCode,
608
+        isi.category_name_two AS categoryName,
609
+        SUM(isi.quantity) AS quantity,
610
+        0 AS percentage,
611
+        0 AS rank
612
+        FROM item_seizure_record isr
613
+        LEFT JOIN item_seizure_items isi ON isr.id = isi.record_id
614
+        WHERE isr.process_status = 3 and isr.inspect_station_id = #{inspectStationId}
615
+        <if test="startDate != null and endDate != null">
616
+            AND isr.seizure_time >= #{startDate}
617
+            AND isr.seizure_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
618
+        </if>
619
+        GROUP BY isi.category_code_two, isi.category_name_two
620
+        ORDER BY quantity DESC
621
+        LIMIT 3
622
+    </select>
623
+
624
+    <!-- 查询隐匿重点部位 Top1 -->
625
+    <select id="selectConcealmentPositionTop1"
626
+            resultType="com.sundot.airport.item.domain.dto.ConcealmentPositionTop1DTO">
627
+        SELECT
628
+        isi.check_position_code_one AS checkPositionCodeOne,
629
+        isi.check_position_name_one AS checkPositionNameOne,
630
+        isi.check_position_code_two AS checkPositionCodeTwo,
631
+        isi.check_position_name_two AS checkPositionNameTwo,
632
+        SUM(isi.quantity) AS quantity
633
+        FROM item_seizure_record isr
634
+        LEFT JOIN item_seizure_items isi ON isr.id = isi.record_id
635
+        WHERE isr.process_status = 3
636
+        AND isr.inspect_station_id = #{inspectStationId}
637
+        AND isi.is_active_concealment = 1
638
+        <if test="startDate != null and endDate != null">
639
+            AND isr.seizure_time >= #{startDate}
640
+            AND isr.seizure_time <![CDATA[ < ]]> date_add(#{endDate}, interval 1 day)
641
+        </if>
642
+        GROUP BY isi.check_position_code_one, isi.check_position_name_one,
643
+        isi.check_position_code_two, isi.check_position_name_two
644
+        ORDER BY quantity DESC
645
+        LIMIT 1
646
+    </select>
647
+
518 648
 </mapper>

+ 17 - 0
airport-item/src/main/resources/mapper/item/SeizureReportMapper.xml

@@ -714,4 +714,21 @@
714 714
             AND isr.create_time BETWEEN #{startDate} AND #{endDate}
715 715
         </if>
716 716
     </select>
717
+
718
+    <select id="homePageSeizureReportBrigadeId"
719
+            resultType="com.sundot.airport.item.domain.ItemLargeScreenHomePageSeizureReportSqlDto">
720
+        select isr.inspect_user_id userId,
721
+        isr.inspect_user_name userName,
722
+        isi.quantity quantity
723
+        from item_seizure_record isr
724
+        left join item_seizure_items isi on isr.id = isi.record_id
725
+        where 1 = 1
726
+        and isr.process_status = 3
727
+        <if test="startDate != null and endDate != null">
728
+            AND isr.create_time BETWEEN #{startDate} AND #{endDate}
729
+        </if>
730
+        <if test="inspectBrigadeId != null">
731
+            and isr.inspect_brigade_id = #{inspectBrigadeId}
732
+        </if>
733
+    </select>
717 734
 </mapper>