Browse Source

质控分析报告-勤务组织

chenshudong 1 month ago
parent
commit
e619cfafda
14 changed files with 1730 additions and 0 deletions
  1. 319 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/quality/QualificationLevelStatsController.java
  2. 467 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/quality/SimpleAttendancePersonController.java
  3. 63 0
      airport-attendance/src/main/java/com/sundot/airport/attendance/calculator/AttendancePersonCountCalculator.java
  4. 66 0
      airport-attendance/src/main/java/com/sundot/airport/attendance/calculator/BrigadeAttendancePersonCountCalculator.java
  5. 50 0
      airport-attendance/src/main/java/com/sundot/airport/attendance/dto/AttendancePersonStatsDTO.java
  6. 32 0
      airport-attendance/src/main/java/com/sundot/airport/attendance/dto/AttendanceTrendDataDTO.java
  7. 41 0
      airport-common/src/main/java/com/sundot/airport/common/enums/IndicatorTypeEnum.java
  8. 59 0
      airport-common/src/main/java/com/sundot/airport/common/statistics/AbstractIndicatorCalculator.java
  9. 48 0
      airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculationParams.java
  10. 85 0
      airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculationResult.java
  11. 37 0
      airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculator.java
  12. 188 0
      airport-common/src/main/java/com/sundot/airport/common/statistics/MultiModelIndicatorCalculator.java
  13. 210 0
      airport-item/src/main/java/com/sundot/airport/item/domain/dto/QualificationLevelStatsDTO.java
  14. 65 0
      airport-system/src/main/java/com/sundot/airport/system/domain/QualificationLevelConverter.java

+ 319 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/quality/QualificationLevelStatsController.java

@@ -0,0 +1,319 @@
1
+package com.sundot.airport.web.controller.quality;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.entity.SysDept;
6
+import com.sundot.airport.common.core.domain.entity.SysUser;
7
+import com.sundot.airport.common.enums.DeptTypeEnum;
8
+import com.sundot.airport.common.utils.DeptUtils;
9
+import com.sundot.airport.item.domain.dto.QualificationLevelStatsDTO;
10
+import com.sundot.airport.system.domain.QualificationLevelConverter;
11
+import com.sundot.airport.system.domain.portrait.QualificationStats;
12
+import com.sundot.airport.system.mapper.portrait.QualificationLevelIndicatorMapper;
13
+import com.sundot.airport.system.service.ISysDeptService;
14
+import com.sundot.airport.system.service.ISysUserService;
15
+import io.swagger.annotations.Api;
16
+import io.swagger.annotations.ApiOperation;
17
+import org.springframework.beans.factory.annotation.Autowired;
18
+import org.springframework.web.bind.annotation.GetMapping;
19
+import org.springframework.web.bind.annotation.RequestMapping;
20
+import org.springframework.web.bind.annotation.RestController;
21
+
22
+import java.math.BigDecimal;
23
+import java.math.RoundingMode;
24
+import java.util.ArrayList;
25
+import java.util.Arrays;
26
+import java.util.HashMap;
27
+import java.util.List;
28
+import java.util.Map;
29
+import java.util.Objects;
30
+import java.util.stream.Collectors;
31
+
32
+/**
33
+ * 资质等级统计控制器
34
+ * 提供资质等级统计的API接口
35
+ *
36
+ * @author wangxx
37
+ */
38
+@Api(tags = "资质等级统计")
39
+@RestController
40
+@RequestMapping("/statistics/qualification")
41
+public class QualificationLevelStatsController extends BaseController {
42
+
43
+    @Autowired
44
+    private QualificationLevelIndicatorMapper qualificationLevelMapper;
45
+
46
+    @Autowired
47
+    private ISysDeptService sysDeptService;
48
+
49
+    @Autowired
50
+    private ISysUserService sysUserService;
51
+
52
+
53
+    /**
54
+     * 固定的资质等级列表(一级到五级)
55
+     */
56
+    private static final List<String> QUALIFICATION_LEVELS = Arrays.asList(
57
+            "一级", "二级", "三级", "四级", "五级"
58
+    );
59
+
60
+    /**
61
+     * 获取饼状图数据(全站资质等级分布)
62
+     */
63
+    @ApiOperation("获取饼状图数据")
64
+    @GetMapping("/pie-chart")
65
+    public AjaxResult getPieChartData() {
66
+
67
+        try {
68
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
69
+            if (topSiteId == null) {
70
+                return AjaxResult.error("无法找到有效的站点信息");
71
+            }
72
+
73
+            List<Long> siteUserIds = getAllUserIdsUnderSite(topSiteId);
74
+            if (siteUserIds.isEmpty()) {
75
+                return AjaxResult.success("获取成功", new QualificationLevelStatsDTO.PieChartData());
76
+            }
77
+
78
+
79
+            QualificationLevelStatsDTO.PieChartData pieChart = calculatePieChartData(siteUserIds);
80
+            return AjaxResult.success("获取成功", pieChart);
81
+
82
+        } catch (Exception e) {
83
+            logger.error("获取饼状图数据失败", e);
84
+            return AjaxResult.error("获取饼状图数据失败: " + e.getMessage());
85
+        }
86
+    }
87
+
88
+    /**
89
+     * 获取柱状图数据(各大队资质等级分布)
90
+     */
91
+    @ApiOperation("获取柱状图数据")
92
+    @GetMapping("/bar-chart")
93
+    public AjaxResult getBarChartData() {
94
+
95
+        try {
96
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
97
+            if (topSiteId == null) {
98
+                return AjaxResult.error("无法找到有效的站点信息");
99
+            }
100
+
101
+            QualificationLevelStatsDTO.BarChartData barChart = calculateBarChartDataOptimized(topSiteId);
102
+            return AjaxResult.success("获取成功", barChart);
103
+
104
+        } catch (Exception e) {
105
+            logger.error("获取柱状图数据失败", e);
106
+            return AjaxResult.error("获取柱状图数据失败: " + e.getMessage());
107
+        }
108
+    }
109
+
110
+    /**
111
+     * 计算饼状图数据(全站范围资质等级分布)
112
+     */
113
+    private QualificationLevelStatsDTO.PieChartData calculatePieChartData(List<Long> userIds) {
114
+        QualificationLevelStatsDTO.PieChartData pieChart = new QualificationLevelStatsDTO.PieChartData();
115
+
116
+        // 查询资质等级统计(暂不考虑时间参数,后续可根据需要扩展)
117
+        List<QualificationStats> statsList = qualificationLevelMapper.queryOrgQualificationLevelStats(userIds, null);
118
+
119
+        // 转换为分布数据
120
+        List<QualificationLevelStatsDTO.LevelDistribution> distributions = new ArrayList<>();
121
+        int totalPersons = 0;
122
+
123
+        // 统计实际存在的等级数据
124
+        Map<String, QualificationStats> statsMap = statsList.stream()
125
+                .collect(Collectors.toMap(
126
+                        stat -> QualificationLevelConverter.toDisplay(stat.getLevelName()),
127
+                        s -> s
128
+                ));
129
+
130
+        // 按固定顺序处理所有等级
131
+        for (String levelName : QUALIFICATION_LEVELS) {
132
+            QualificationLevelStatsDTO.LevelDistribution distribution = new QualificationLevelStatsDTO.LevelDistribution();
133
+            distribution.setLevelName(levelName);
134
+
135
+            QualificationStats stats = statsMap.get(levelName);
136
+            if (stats != null) {
137
+                distribution.setCount(stats.getCount());
138
+                distribution.setPercentage(new BigDecimal(stats.getPercentage()).setScale(2, RoundingMode.HALF_UP));
139
+                totalPersons += stats.getCount();
140
+            } else {
141
+                // 该等级无人
142
+                distribution.setCount(0);
143
+                distribution.setPercentage(BigDecimal.ZERO);
144
+            }
145
+
146
+            distributions.add(distribution);
147
+        }
148
+
149
+        pieChart.setData(distributions);
150
+        pieChart.setTotalPersons(totalPersons);
151
+
152
+        return pieChart;
153
+    }
154
+
155
+    /**
156
+     * 计算柱状图数据(各大队资质等级分布)
157
+     */
158
+    private QualificationLevelStatsDTO.BarChartData calculateBarChartDataOptimized(Long siteId) {
159
+        QualificationLevelStatsDTO.BarChartData barChart = new QualificationLevelStatsDTO.BarChartData();
160
+
161
+        // 获取站点下的所有大队
162
+        List<SysDept> brigades = getBrigadesUnderSite(siteId);
163
+        if (brigades.isEmpty()) {
164
+            barChart.setBrigades(new ArrayList<>());
165
+            return barChart;
166
+        }
167
+
168
+        // 批量获取所有大队的用户ID
169
+        Map<Long, List<Long>> deptUserIdsMap = batchGetUserIdsByBrigades(brigades);
170
+
171
+        // 批量查询所有大队的资质统计数据
172
+        Map<Long, List<QualificationStats>> deptStatsMap = batchQueryQualificationStats(deptUserIdsMap);
173
+
174
+        // 构建大队数据
175
+        List<QualificationLevelStatsDTO.BrigadeData> deptDataList = brigades.stream()
176
+                .map(dept -> {
177
+                    QualificationLevelStatsDTO.BrigadeData deptData = new QualificationLevelStatsDTO.BrigadeData();
178
+                    deptData.setDeptId(dept.getDeptId());
179
+                    deptData.setDeptName(dept.getDeptName());
180
+
181
+                    List<QualificationStats> deptStats = deptStatsMap.get(dept.getDeptId());
182
+                    if (deptStats != null && !deptStats.isEmpty()) {
183
+                        List<QualificationLevelStatsDTO.LevelCount> levelCounts = convertToLevelCounts(deptStats);
184
+                        deptData.setLevelCounts(levelCounts);
185
+                    } else {
186
+                        // 大队无用户或无统计数据,初始化为空数据
187
+                        List<QualificationLevelStatsDTO.LevelCount> emptyCounts = QUALIFICATION_LEVELS.stream()
188
+                                .map(level -> {
189
+                                    QualificationLevelStatsDTO.LevelCount count = new QualificationLevelStatsDTO.LevelCount();
190
+                                    count.setLevelName(level);
191
+                                    count.setCount(0);
192
+                                    return count;
193
+                                })
194
+                                .collect(Collectors.toList());
195
+                        deptData.setLevelCounts(emptyCounts);
196
+                    }
197
+
198
+                    return deptData;
199
+                })
200
+                .collect(Collectors.toList());
201
+
202
+        barChart.setBrigades(deptDataList);
203
+        return barChart;
204
+    }
205
+
206
+    /**
207
+     * 批量获取大队用户ID映射
208
+     */
209
+    private Map<Long, List<Long>> batchGetUserIdsByBrigades(List<SysDept> brigades) {
210
+        Map<Long, List<Long>> deptUserIdsMap = new HashMap<>();
211
+
212
+        for (SysDept dept : brigades) {
213
+            List<Long> deptUserIds = getAllUserIdsUnderDept(dept.getDeptId());
214
+            deptUserIdsMap.put(dept.getDeptId(), deptUserIds);
215
+        }
216
+
217
+        return deptUserIdsMap;
218
+    }
219
+
220
+    /**
221
+     * 批量查询资质统计数据
222
+     */
223
+    private Map<Long, List<QualificationStats>> batchQueryQualificationStats(Map<Long, List<Long>> deptUserIdsMap) {
224
+        Map<Long, List<QualificationStats>> deptStatsMap = new HashMap<>();
225
+
226
+        // 过滤掉没有用户的大队
227
+        Map<Long, List<Long>> validDeptUserIdsMap = deptUserIdsMap.entrySet().stream()
228
+                .filter(entry -> !entry.getValue().isEmpty())
229
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
230
+
231
+        if (validDeptUserIdsMap.isEmpty()) {
232
+            return deptStatsMap;
233
+        }
234
+
235
+        for (Map.Entry<Long, List<Long>> entry : validDeptUserIdsMap.entrySet()) {
236
+            Long deptId = entry.getKey();
237
+            List<Long> userIds = entry.getValue();
238
+            List<QualificationStats> stats = qualificationLevelMapper.queryOrgQualificationLevelStats(userIds, null);
239
+            deptStatsMap.put(deptId, stats);
240
+        }
241
+
242
+        return deptStatsMap;
243
+    }
244
+
245
+    /**
246
+     * 将资质统计转换为等级人数数据
247
+     */
248
+    private List<QualificationLevelStatsDTO.LevelCount> convertToLevelCounts(List<QualificationStats> statsList) {
249
+        Map<String, Integer> countMap = statsList.stream()
250
+                .collect(Collectors.toMap(QualificationStats::getLevelName, QualificationStats::getCount));
251
+
252
+        return QUALIFICATION_LEVELS.stream()
253
+                .map(level -> {
254
+                    String levelName = QualificationLevelConverter.toDatabase(level);
255
+                    QualificationLevelStatsDTO.LevelCount count = new QualificationLevelStatsDTO.LevelCount();
256
+                    count.setLevelName(level);
257
+                    count.setCount(countMap.getOrDefault(levelName, 0));
258
+                    return count;
259
+                })
260
+                .collect(Collectors.toList());
261
+    }
262
+
263
+
264
+    /**
265
+     * 获取站点下的所有用户ID
266
+     */
267
+    private List<Long> getAllUserIdsUnderSite(Long siteId) {
268
+        List<Long> userIds = new ArrayList<>();
269
+
270
+        // 获取站点本身及其下属部门
271
+        List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(siteId);
272
+        allDepts.add(0, sysDeptService.selectDeptById(siteId)); // 添加站点本身
273
+
274
+        // 获取各部门下的用户
275
+        for (SysDept dept : allDepts) {
276
+            if (Objects.nonNull(dept)) {
277
+                List<Long> deptUserIds = sysUserService.selectUserByDeptId(dept.getDeptId()).stream()
278
+                        .map(SysUser::getUserId)
279
+                        .collect(Collectors.toList());
280
+                userIds.addAll(deptUserIds);
281
+            }
282
+        }
283
+
284
+        return userIds.stream().distinct().collect(Collectors.toList());
285
+    }
286
+
287
+    /**
288
+     * 获取站点下的所有大队
289
+     */
290
+    private List<SysDept> getBrigadesUnderSite(Long siteId) {
291
+        List<SysDept> children = sysDeptService.selectChildrenDeptById(siteId);
292
+        return children.stream()
293
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
294
+                .collect(Collectors.toList());
295
+    }
296
+
297
+    /**
298
+     * 获取部门下的所有用户ID
299
+     */
300
+    private List<Long> getAllUserIdsUnderDept(Long deptId) {
301
+        List<Long> userIds = new ArrayList<>();
302
+
303
+        // 获取部门本身及其下属部门
304
+        List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(deptId);
305
+        allDepts.add(0, sysDeptService.selectDeptById(deptId)); // 添加部门本身
306
+
307
+        // 获取各部门下的用户
308
+        for (SysDept dept : allDepts) {
309
+            List<Long> deptUserIds = sysUserService.selectUserByDeptId(dept.getDeptId()).stream()
310
+                    .map(SysUser::getUserId)
311
+                    .collect(Collectors.toList());
312
+            userIds.addAll(deptUserIds);
313
+        }
314
+
315
+        return userIds.stream().distinct().collect(Collectors.toList());
316
+    }
317
+
318
+
319
+}

+ 467 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/quality/SimpleAttendancePersonController.java

@@ -0,0 +1,467 @@
1
+package com.sundot.airport.web.controller.quality;
2
+
3
+import com.sundot.airport.attendance.calculator.AttendancePersonCountCalculator;
4
+import com.sundot.airport.attendance.calculator.BrigadeAttendancePersonCountCalculator;
5
+import com.sundot.airport.attendance.dto.AttendanceTrendDataDTO;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.attendance.dto.AttendancePersonStatsDTO;
8
+import com.sundot.airport.common.enums.DeptTypeEnum;
9
+import com.sundot.airport.common.statistics.*;
10
+import com.sundot.airport.common.utils.DeptUtils;
11
+import com.sundot.airport.common.utils.StringUtils;
12
+import com.sundot.airport.system.service.ISysDeptService;
13
+import io.swagger.annotations.Api;
14
+import io.swagger.annotations.ApiOperation;
15
+import org.springframework.beans.factory.annotation.Autowired;
16
+import org.springframework.web.bind.annotation.*;
17
+
18
+import java.math.BigDecimal;
19
+import java.util.*;
20
+import java.util.stream.Collectors;
21
+
22
+import static com.sundot.airport.common.utils.SecurityUtils.getDeptId;
23
+
24
+/**
25
+ * 出勤人次控制器
26
+ * 根据时间维度返回总体+大队的人次数据
27
+ */
28
+@Api(tags = "出勤人次")
29
+@RestController
30
+@RequestMapping("/quality/attendance")
31
+public class SimpleAttendancePersonController {
32
+
33
+    @Autowired
34
+    private AttendancePersonCountCalculator overallCalculator; // 全站计算器
35
+
36
+    @Autowired
37
+    private BrigadeAttendancePersonCountCalculator deptCalculator; // 大队计算器
38
+
39
+    @Autowired
40
+    private MultiModelIndicatorCalculator multiModelIndicatorCalculator;
41
+
42
+    @Autowired
43
+    private ISysDeptService deptService;
44
+
45
+
46
+    /**
47
+     * 出勤人次分析
48
+     */
49
+    @ApiOperation("出勤人次分析")
50
+    @PostMapping("/calculate")
51
+    public List<AttendancePersonStatsDTO> calculateStatistics(@RequestBody IndicatorCalculationParams params) {
52
+        List<AttendancePersonStatsDTO> results = new ArrayList<>();
53
+
54
+        // 获取当前用户所在站点ID
55
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
56
+        List<SysDept> brigadesUnderSite = getBrigadesUnderSite(topSiteId);
57
+        brigadesUnderSite.add(deptService.selectDeptById(topSiteId));
58
+        brigadesUnderSite.forEach(dept -> {
59
+            results.add(createAttendancePersonStats(dept, params));
60
+        });
61
+
62
+        return results;
63
+    }
64
+
65
+    /**
66
+     * 出勤人次趋势数据
67
+     * 根据时间维度参数动态生成柱状图数据
68
+     * 支持月度(同比+环比)、季度(同比)、年度(同比)等多种展示方式
69
+     *
70
+     * @param params 时间维度参数(年、季度、月等)
71
+     * @return 趋势数据列表
72
+     */
73
+    @ApiOperation("出勤人次趋势数据")
74
+    @PostMapping("/trend-data")
75
+    public List<AttendanceTrendDataDTO> getTrendData(@RequestBody IndicatorCalculationParams params) {
76
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
77
+
78
+        // 根据时间维度类型决定展示策略
79
+        if (params.getMonth() != null) {
80
+            // 月度:展示当前月、上月、去年同期(同比+环比)
81
+            results.addAll(generateMonthlyTrendData(params));
82
+        } else if (params.getQuarter() != null) {
83
+            // 季度:展示当前季度和去年同期
84
+            results.addAll(generateQuarterlyTrendData(params));
85
+        } else if (params.getYear() != null) {
86
+            // 年度:仅展示当前年和去年(仅同比)
87
+            results.addAll(generateYearlyTrendData(params));
88
+        } else {
89
+            // 默认按月度处理
90
+            results.addAll(generateMonthlyTrendData(params));
91
+        }
92
+
93
+        return results;
94
+    }
95
+
96
+    /**
97
+     * 生成月度趋势数据(同比+环比)
98
+     * 展示:当前月、上月、去年同期
99
+     */
100
+    private List<AttendanceTrendDataDTO> generateMonthlyTrendData(IndicatorCalculationParams params) {
101
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
102
+
103
+        // 获取当前用户所在站点ID
104
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
105
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
106
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
107
+
108
+
109
+        // 计算当前月数据
110
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
111
+
112
+        // 计算上月数据
113
+        IndicatorCalculationParams lastMonthParams = createLastMonthParams(params);
114
+        List<AttendancePersonStatsDTO> lastMonthList = calculateDeptStats(stationAndThreeDepts, lastMonthParams);
115
+
116
+        // 计算去年同期数据
117
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
118
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
119
+
120
+
121
+        // 构造柱状图数据
122
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(getMonthLabel(params), currentList);
123
+        AttendanceTrendDataDTO lastMonthData = createTrendDataPoint(getLastMonthLabel(params), lastMonthList);
124
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint(getLastYearMonthLabel(params), lastYearList);
125
+
126
+        results.add(lastYearData);
127
+        results.add(lastMonthData);
128
+        results.add(currentData);
129
+
130
+        return results;
131
+    }
132
+
133
+    /**
134
+     * 生成季度趋势数据(同比+环比)
135
+     * 展示:当前季度、上季度、去年同期季度
136
+     */
137
+    private List<AttendanceTrendDataDTO> generateQuarterlyTrendData(IndicatorCalculationParams params) {
138
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
139
+        // 获取当前用户所在站点ID
140
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
141
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
142
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
143
+        // 计算当前季度数据
144
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
145
+
146
+        // 计算上季度数据
147
+        IndicatorCalculationParams lastQuarterParams = createLastQuarterParams(params);
148
+        List<AttendancePersonStatsDTO> lastQuarterList = calculateDeptStats(stationAndThreeDepts, lastQuarterParams);
149
+
150
+        // 计算去年同期季度数据
151
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
152
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
153
+
154
+        // 构造柱状图数据
155
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(getQuarterLabel(params), currentList);
156
+        AttendanceTrendDataDTO lastQuarterData = createTrendDataPoint(getLastQuarterLabel(params), lastQuarterList);
157
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint(getLastYearQuarterLabel(params), lastYearList);
158
+
159
+        results.add(lastYearData);
160
+        results.add(lastQuarterData);
161
+        results.add(currentData);
162
+        return results;
163
+    }
164
+
165
+    /**
166
+     * 生成年度趋势数据(仅同比)
167
+     * 展示:当前年、去年
168
+     */
169
+    private List<AttendanceTrendDataDTO> generateYearlyTrendData(IndicatorCalculationParams params) {
170
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
171
+        // 获取当前用户所在站点ID
172
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
173
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
174
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
175
+        // 计算当前年数据
176
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
177
+
178
+        // 计算去年数据
179
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
180
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
181
+
182
+        // 构造柱状图数据
183
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(params.getYear() + "年", currentList);
184
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint((params.getYear() - 1) + "年", lastYearList);
185
+        results.add(lastYearData);
186
+        results.add(currentData);
187
+
188
+
189
+        return results;
190
+    }
191
+
192
+    /**
193
+     * 计算部门统计数据
194
+     */
195
+    private List<AttendancePersonStatsDTO> calculateDeptStats(List<SysDept> depts, IndicatorCalculationParams params) {
196
+        List<AttendancePersonStatsDTO> results = new ArrayList<>();
197
+        depts.forEach(dept -> {
198
+            results.add(createAttendancePersonStats(dept, params));
199
+        });
200
+        return results;
201
+    }
202
+
203
+    /**
204
+     * 创建趋势数据点
205
+     */
206
+    private AttendanceTrendDataDTO createTrendDataPoint(String timeLabel, List<AttendancePersonStatsDTO> dataList) {
207
+        AttendanceTrendDataDTO data = new AttendanceTrendDataDTO();
208
+        data.setTimeLabel(timeLabel);
209
+        data.setOverall(getStationValue(dataList));
210
+        data.setData1(getDeptValue(dataList, "安检一大队"));
211
+        data.setData2(getDeptValue(dataList, "安检二大队"));
212
+        data.setData3(getDeptValue(dataList, "安检三大队"));
213
+        data.setData4(getDeptValue(dataList, "安检综合大队"));
214
+        return data;
215
+    }
216
+
217
+    /**
218
+     * 获取季度标签
219
+     */
220
+    private String getQuarterLabel(IndicatorCalculationParams params) {
221
+        if (params.getYear() != null && params.getQuarter() != null) {
222
+            return params.getYear() + "年第" + params.getQuarter() + "季度";
223
+        }
224
+        return "当前季度";
225
+    }
226
+
227
+    /**
228
+     * 获取上季度标签
229
+     */
230
+    private String getLastQuarterLabel(IndicatorCalculationParams params) {
231
+        if (params.getYear() != null && params.getQuarter() != null) {
232
+            int lastQuarter = params.getQuarter() - 1;
233
+            int year = params.getYear();
234
+            if (lastQuarter <= 0) {
235
+                lastQuarter = 4;
236
+                year = year - 1;
237
+            }
238
+            return year + "年第" + lastQuarter + "季度";
239
+        }
240
+        return "上季度";
241
+    }
242
+
243
+    /**
244
+     * 获取去年季度标签
245
+     */
246
+    private String getLastYearQuarterLabel(IndicatorCalculationParams params) {
247
+        if (params.getYear() != null && params.getQuarter() != null) {
248
+            return (params.getYear() - 1) + "年第" + params.getQuarter() + "季度";
249
+        }
250
+        return "去年同期季度";
251
+    }
252
+
253
+    /**
254
+     * 月度标签
255
+     */
256
+    private String getMonthLabel(IndicatorCalculationParams params) {
257
+        if (params.getYear() != null && params.getMonth() != null) {
258
+            return params.getYear() + "年" + params.getMonth() + "月";
259
+        }
260
+        return "当前月度";
261
+    }
262
+
263
+    /**
264
+     * 获取上月标签
265
+     */
266
+    private String getLastMonthLabel(IndicatorCalculationParams params) {
267
+        if (params.getYear() != null && params.getMonth() != null) {
268
+            int lastMonth = params.getMonth() - 1;
269
+            int year = params.getYear();
270
+            if (lastMonth <= 0) {
271
+                lastMonth = 12;
272
+                year = year - 1;
273
+            }
274
+            return year + "年" + lastMonth + "月";
275
+        }
276
+        return "上月";
277
+    }
278
+
279
+    /**
280
+     * 获取去年本月度标签
281
+     */
282
+    private String getLastYearMonthLabel(IndicatorCalculationParams params) {
283
+        if (params.getYear() != null && params.getMonth() != null) {
284
+            return (params.getYear() - 1) + "年" + params.getMonth() + "月";
285
+        }
286
+        return "去年同期月度";
287
+    }
288
+
289
+
290
+    /**
291
+     * 创建出勤人次统计对象
292
+     */
293
+    private AttendancePersonStatsDTO createAttendancePersonStats(SysDept sysDept, IndicatorCalculationParams params) {
294
+        AttendancePersonStatsDTO result = new AttendancePersonStatsDTO();
295
+        result.setDeptId(sysDept.getDeptId());
296
+        result.setDeptName(sysDept.getDeptName());
297
+        result.setDeptType(sysDept.getDeptType());
298
+        try {
299
+            IndicatorCalculationParams calcParams = new IndicatorCalculationParams();
300
+            calcParams.setDeptId(sysDept.getDeptId());
301
+            calcParams.setYear(params.getYear());
302
+            calcParams.setQuarter(params.getQuarter());
303
+            calcParams.setMonth(params.getMonth());
304
+            calcParams.setStartTime(params.getStartTime());
305
+            calcParams.setEndTime(params.getEndTime());
306
+            calcParams.setChainRatio(params.getChainRatio());
307
+            calcParams.setYearOnYear(params.getYearOnYear());
308
+
309
+            IndicatorCalculationResult currentResult;
310
+            if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
311
+                currentResult = multiModelIndicatorCalculator.calculateWithRatio(overallCalculator, calcParams);
312
+            } else {
313
+                currentResult = multiModelIndicatorCalculator.calculateWithRatio(deptCalculator, calcParams);
314
+            }
315
+
316
+            if ("SUCCESS".equals(currentResult.getStatus())) {
317
+                result.setCurrentValue(currentResult.getValue());
318
+                result.setBaselineValue(currentResult.getBaselineValue());
319
+
320
+                boolean showChainRatio = params.getChainRatio() != null && params.getChainRatio();
321
+                boolean showYearOnYear = params.getYearOnYear() != null && params.getYearOnYear();
322
+
323
+                if (params.getYear() != null && params.getQuarter() == null && params.getMonth() == null) {
324
+                    showChainRatio = false;
325
+                }
326
+
327
+                if (showYearOnYear && currentResult.getYearOnYearValue() != null) {
328
+                    result.setYearOnYearValue(currentResult.getYearOnYearValue());
329
+                    result.setYearOnYearDirection(getDirectionText(currentResult.getYearOnYearValue()));
330
+                    result.setYearOnYearTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("yearOnYear"));
331
+                }
332
+
333
+                if (showChainRatio && currentResult.getChainRatioValue() != null) {
334
+                    result.setChainRatioValue(currentResult.getChainRatioValue());
335
+                    result.setChainRatioDirection(getDirectionText(currentResult.getChainRatioValue()));
336
+                    result.setChainTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("chain"));
337
+                }
338
+            }
339
+        } catch (Exception e) {
340
+            throw new RuntimeException(e);
341
+        }
342
+
343
+        return result;
344
+    }
345
+
346
+    /**
347
+     * 创建上月参数
348
+     */
349
+    private IndicatorCalculationParams createLastMonthParams(IndicatorCalculationParams originalParams) {
350
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
351
+        params.setYear(originalParams.getYear());
352
+        params.setQuarter(originalParams.getQuarter());
353
+        params.setMonth(originalParams.getMonth());
354
+        params.setStartTime(originalParams.getStartTime());
355
+        params.setEndTime(originalParams.getEndTime());
356
+        params.setChainRatio(false);
357
+        params.setYearOnYear(false);
358
+
359
+        // 计算上月
360
+        if (params.getMonth() != null) {
361
+            int lastMonth = params.getMonth() - 1;
362
+            int year = params.getYear();
363
+            if (lastMonth <= 0) {
364
+                lastMonth = 12;
365
+                year = year - 1;
366
+            }
367
+            params.setMonth(lastMonth);
368
+            params.setYear(year);
369
+        }
370
+
371
+        return params;
372
+    }
373
+
374
+    /**
375
+     * 创建上季度参数
376
+     */
377
+    private IndicatorCalculationParams createLastQuarterParams(IndicatorCalculationParams originalParams) {
378
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
379
+        params.setYear(originalParams.getYear());
380
+        params.setQuarter(originalParams.getQuarter());
381
+        params.setMonth(originalParams.getMonth());
382
+        params.setStartTime(originalParams.getStartTime());
383
+        params.setEndTime(originalParams.getEndTime());
384
+        params.setChainRatio(false);
385
+        params.setYearOnYear(false);
386
+
387
+        // 计算上季度
388
+        if (params.getQuarter() != null) {
389
+            int lastQuarter = params.getQuarter() - 1;
390
+            int year = params.getYear();
391
+            if (lastQuarter <= 0) {
392
+                lastQuarter = 4;
393
+                year = year - 1;
394
+            }
395
+            params.setQuarter(lastQuarter);
396
+            params.setYear(year);
397
+        }
398
+
399
+        return params;
400
+    }
401
+
402
+
403
+    /**
404
+     * 创建去年同期参数
405
+     */
406
+    private IndicatorCalculationParams createLastYearParams(IndicatorCalculationParams originalParams) {
407
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
408
+        params.setYear(originalParams.getYear());
409
+        params.setQuarter(originalParams.getQuarter());
410
+        params.setMonth(originalParams.getMonth());
411
+        params.setStartTime(originalParams.getStartTime());
412
+        params.setEndTime(originalParams.getEndTime());
413
+        params.setChainRatio(false);
414
+        params.setYearOnYear(false);
415
+
416
+        // 计算去年同期
417
+        if (params.getYear() != null) {
418
+            params.setYear(params.getYear() - 1);
419
+        }
420
+
421
+        return params;
422
+    }
423
+
424
+    /**
425
+     * 获取站点值
426
+     */
427
+    private BigDecimal getStationValue(List<AttendancePersonStatsDTO> list) {
428
+        return list.stream()
429
+                .filter(dto -> DeptTypeEnum.STATION.getCode().equals(dto.getDeptType()))
430
+                .findFirst()
431
+                .map(AttendancePersonStatsDTO::getCurrentValue)
432
+                .orElse(BigDecimal.ZERO);
433
+    }
434
+
435
+    /**
436
+     * 获取指定大队的值
437
+     */
438
+    private BigDecimal getDeptValue(List<AttendancePersonStatsDTO> list, String deptName) {
439
+        return list.stream()
440
+                .filter(dto -> DeptTypeEnum.BRIGADE.getCode().equals(dto.getDeptType())
441
+                        && deptName.equals(dto.getDeptName()))
442
+                .findFirst()
443
+                .map(AttendancePersonStatsDTO::getCurrentValue)
444
+                .orElse(BigDecimal.ZERO);
445
+    }
446
+
447
+    /**
448
+     * 获取方向文本:上升 或 下降
449
+     */
450
+    private String getDirectionText(String growthRate) {
451
+        if (StringUtils.isEmpty(growthRate)) return "";
452
+        if (growthRate.equals("--")) return "上升";
453
+        return new BigDecimal(growthRate).compareTo(BigDecimal.ZERO) >= 0 ? "上升" : "下降";
454
+    }
455
+
456
+
457
+    /**
458
+     * 获取站点下的所有大队
459
+     */
460
+    private List<SysDept> getBrigadesUnderSite(Long siteId) {
461
+        List<SysDept> children = deptService.selectChildrenDeptById(siteId);
462
+        return children.stream()
463
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
464
+                .collect(Collectors.toList());
465
+    }
466
+
467
+}

+ 63 - 0
airport-attendance/src/main/java/com/sundot/airport/attendance/calculator/AttendancePersonCountCalculator.java

@@ -0,0 +1,63 @@
1
+package com.sundot.airport.attendance.calculator;
2
+
3
+import com.sundot.airport.attendance.domain.AttendanceRecord;
4
+import com.sundot.airport.attendance.mapper.AttendanceRecordMapper;
5
+import com.sundot.airport.common.enums.IndicatorTypeEnum;
6
+import com.sundot.airport.common.statistics.AbstractIndicatorCalculator;
7
+import com.sundot.airport.common.statistics.IndicatorCalculationParams;
8
+import com.sundot.airport.common.statistics.TimeDimensionProcessor;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.stereotype.Component;
11
+
12
+import java.math.BigDecimal;
13
+import java.util.List;
14
+
15
+/**
16
+ * 出勤人次计算器
17
+ * 计算全站出勤人次统计
18
+ */
19
+@Component
20
+public class AttendancePersonCountCalculator extends AbstractIndicatorCalculator<BigDecimal> {
21
+
22
+    @Autowired
23
+    private AttendanceRecordMapper attendanceRecordMapper;
24
+
25
+    @Autowired
26
+    private TimeDimensionProcessor timeDimensionProcessor;
27
+
28
+    @Override
29
+    public BigDecimal calculate(IndicatorCalculationParams params) {
30
+        // 确保时间范围已计算
31
+        timeDimensionProcessor.calculateTimeRange(params);
32
+
33
+        // 查询全站出勤记录
34
+        AttendanceRecord record = new AttendanceRecord();
35
+        record.setAttendanceDateStart(params.getStartTime());
36
+        record.setAttendanceDateEnd(params.getEndTime());
37
+
38
+        // 查询考勤记录
39
+        List<AttendanceRecord> records = attendanceRecordMapper.selectAttendanceRecordList(record);
40
+
41
+        // 返回出勤人次(去重后的用户数量)
42
+        return new BigDecimal(records.stream()
43
+                .map(AttendanceRecord::getUserId)
44
+                .distinct()
45
+                .count());
46
+    }
47
+
48
+    @Override
49
+    public String getIndicatorType() {
50
+        return IndicatorTypeEnum.ATTENDANCE_PERSONS.getCode();
51
+    }
52
+
53
+    @Override
54
+    public String getIndicatorName() {
55
+        return IndicatorTypeEnum.ATTENDANCE_PERSONS.getName();
56
+    }
57
+
58
+    @Override
59
+    public String getIndicatorDescription() {
60
+        return "全站出勤人次统计";
61
+    }
62
+
63
+}

+ 66 - 0
airport-attendance/src/main/java/com/sundot/airport/attendance/calculator/BrigadeAttendancePersonCountCalculator.java

@@ -0,0 +1,66 @@
1
+package com.sundot.airport.attendance.calculator;
2
+
3
+import com.sundot.airport.attendance.domain.AttendanceRecord;
4
+import com.sundot.airport.attendance.mapper.AttendanceRecordMapper;
5
+import com.sundot.airport.common.enums.IndicatorTypeEnum;
6
+import com.sundot.airport.common.statistics.AbstractIndicatorCalculator;
7
+import com.sundot.airport.common.statistics.IndicatorCalculationParams;
8
+import com.sundot.airport.common.statistics.TimeDimensionProcessor;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.stereotype.Component;
11
+
12
+import java.math.BigDecimal;
13
+import java.util.List;
14
+
15
+/**
16
+ * 科室出勤人次计算器
17
+ * 计算指定科室的出勤人次统计
18
+ */
19
+@Component
20
+public class BrigadeAttendancePersonCountCalculator extends AbstractIndicatorCalculator<BigDecimal> {
21
+
22
+    @Autowired
23
+    private AttendanceRecordMapper attendanceRecordMapper;
24
+
25
+    @Autowired
26
+    private TimeDimensionProcessor timeDimensionProcessor;
27
+
28
+    @Override
29
+    public BigDecimal calculate(IndicatorCalculationParams params) {
30
+
31
+        timeDimensionProcessor.calculateTimeRange(params);
32
+
33
+        // 查询指定大队的出勤记录
34
+        AttendanceRecord record = new AttendanceRecord();
35
+        if (params.getDeptId() != null) {
36
+            // 根据大队ID查询
37
+            record.setBrigadeCode(params.getDeptId().toString());
38
+        }
39
+        record.setAttendanceDateStart(params.getStartTime());
40
+        record.setAttendanceDateEnd(params.getEndTime());
41
+
42
+        // 查询考勤记录
43
+        List<AttendanceRecord> records = attendanceRecordMapper.selectAttendanceRecordList(record);
44
+
45
+        // 返回科室出勤人次(去重后的用户数量)
46
+        return new BigDecimal(records.stream()
47
+                .map(AttendanceRecord::getUserId)
48
+                .distinct()
49
+                .count());
50
+    }
51
+
52
+    @Override
53
+    public String getIndicatorType() {
54
+        return IndicatorTypeEnum.DEPARTMENT_ATTENDANCE_PERSONS.getCode();
55
+    }
56
+
57
+    @Override
58
+    public String getIndicatorName() {
59
+        return IndicatorTypeEnum.DEPARTMENT_ATTENDANCE_PERSONS.getName();
60
+    }
61
+
62
+    @Override
63
+    public String getIndicatorDescription() {
64
+        return "科室出勤人次统计";
65
+    }
66
+}

+ 50 - 0
airport-attendance/src/main/java/com/sundot/airport/attendance/dto/AttendancePersonStatsDTO.java

@@ -0,0 +1,50 @@
1
+package com.sundot.airport.attendance.dto;
2
+
3
+import com.sundot.airport.common.statistics.TimeDimensionParams;
4
+import io.swagger.annotations.ApiModelProperty;
5
+import lombok.Data;
6
+
7
+import java.math.BigDecimal;
8
+
9
+/**
10
+ * 出勤人次统计DTO
11
+ * 用于返回出勤人次分析的统计数据
12
+ */
13
+@Data
14
+public class AttendancePersonStatsDTO {
15
+
16
+    @ApiModelProperty("部门ID")
17
+    private Long deptId;
18
+
19
+    @ApiModelProperty("部门名称")
20
+    private String deptName;
21
+
22
+    @ApiModelProperty("部门类型")
23
+    private String deptType;
24
+
25
+    @ApiModelProperty("当前值")
26
+    private BigDecimal currentValue;
27
+
28
+    @ApiModelProperty("环比值")
29
+    private String chainRatioValue;
30
+
31
+    @ApiModelProperty("同比值")
32
+    private String yearOnYearValue;
33
+
34
+    @ApiModelProperty("环比方向(上升/下降)")
35
+    private String chainRatioDirection;
36
+
37
+    @ApiModelProperty("同比方向(上升/下降)")
38
+    private String yearOnYearDirection;
39
+
40
+    @ApiModelProperty("基准值")
41
+    private BigDecimal baselineValue;
42
+
43
+    @ApiModelProperty("环比时间维度")
44
+    private TimeDimensionParams chainTimeDimension;
45
+
46
+    @ApiModelProperty("同比时间维度")
47
+    private TimeDimensionParams yearOnYearTimeDimension;
48
+
49
+
50
+}

+ 32 - 0
airport-attendance/src/main/java/com/sundot/airport/attendance/dto/AttendanceTrendDataDTO.java

@@ -0,0 +1,32 @@
1
+package com.sundot.airport.attendance.dto;
2
+
3
+import io.swagger.annotations.ApiModelProperty;
4
+import lombok.Data;
5
+
6
+import java.math.BigDecimal;
7
+
8
+/**
9
+ * 出勤趋势数据DTO
10
+ * 用于返回同比环比趋势数据
11
+ */
12
+@Data
13
+public class AttendanceTrendDataDTO {
14
+
15
+    @ApiModelProperty("时间标签")
16
+    private String timeLabel;
17
+
18
+    @ApiModelProperty("总体数据")
19
+    private BigDecimal overall;
20
+
21
+    @ApiModelProperty("安检一大队")
22
+    private BigDecimal data1;
23
+
24
+    @ApiModelProperty("安检二大队")
25
+    private BigDecimal data2;
26
+
27
+    @ApiModelProperty("安检三大队")
28
+    private BigDecimal data3;
29
+
30
+    @ApiModelProperty("安检综合大队")
31
+    private BigDecimal data4;
32
+}

+ 41 - 0
airport-common/src/main/java/com/sundot/airport/common/enums/IndicatorTypeEnum.java

@@ -0,0 +1,41 @@
1
+package com.sundot.airport.common.enums;
2
+
3
+import lombok.AllArgsConstructor;
4
+import lombok.Getter;
5
+
6
+/**
7
+ * 指标类型枚举
8
+ * 定义系统中所有的指标类型
9
+ */
10
+@Getter
11
+@AllArgsConstructor
12
+public enum IndicatorTypeEnum {
13
+    ATTENDANCE_PERSONS("attendance_persons", "出勤人次"),
14
+    DEPARTMENT_ATTENDANCE_PERSONS("department_attendance_persons", "科室出勤人次"),
15
+    ;
16
+
17
+    /**
18
+     * 指标类型编码
19
+     */
20
+    private final String code;
21
+
22
+    /**
23
+     * 指标类型名称
24
+     */
25
+    private final String name;
26
+
27
+    /**
28
+     * 根据编码获取枚举
29
+     *
30
+     * @param code 编码
31
+     * @return 枚举值
32
+     */
33
+    public static IndicatorTypeEnum getByCode(String code) {
34
+        for (IndicatorTypeEnum type : values()) {
35
+            if (type.code.equals(code)) {
36
+                return type;
37
+            }
38
+        }
39
+        return null;
40
+    }
41
+}

+ 59 - 0
airport-common/src/main/java/com/sundot/airport/common/statistics/AbstractIndicatorCalculator.java

@@ -0,0 +1,59 @@
1
+package com.sundot.airport.common.statistics;
2
+
3
+
4
+/**
5
+ * 抽象指标计算器
6
+ * 提供指标计算器的通用实现
7
+ */
8
+public abstract class AbstractIndicatorCalculator<T> implements IndicatorCalculator<T> {
9
+
10
+    /**
11
+     * 获取专用参数对象
12
+     *
13
+     * @param params 计算参数
14
+     * @return 专用参数对象
15
+     */
16
+    protected Object getSpecificParams(IndicatorCalculationParams params) {
17
+        return params.getSpecificParams();
18
+    }
19
+
20
+    /**
21
+     * 获取专用参数对象并转换为指定类型
22
+     *
23
+     * @param params 计算参数
24
+     * @param clazz  类型
25
+     * @return 专用参数对象
26
+     */
27
+    protected <R> R getSpecificParams(IndicatorCalculationParams params, Class<R> clazz) {
28
+        Object value = getSpecificParams(params);
29
+        if (value != null && clazz.isInstance(value)) {
30
+            return clazz.cast(value);
31
+        }
32
+        return null;
33
+    }
34
+
35
+    /**
36
+     * 获取上下文参数对象
37
+     *
38
+     * @param params 计算参数
39
+     * @return 上下文参数对象
40
+     */
41
+    protected Object getContextParams(IndicatorCalculationParams params) {
42
+        return params.getContextParams();
43
+    }
44
+
45
+    /**
46
+     * 获取上下文参数对象并转换为指定类型
47
+     *
48
+     * @param params 计算参数
49
+     * @param clazz  类型
50
+     * @return 上下文参数对象
51
+     */
52
+    protected <R> R getContextParams(IndicatorCalculationParams params, Class<R> clazz) {
53
+        Object value = getContextParams(params);
54
+        if (value != null && clazz.isInstance(value)) {
55
+            return clazz.cast(value);
56
+        }
57
+        return null;
58
+    }
59
+}

+ 48 - 0
airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculationParams.java

@@ -0,0 +1,48 @@
1
+package com.sundot.airport.common.statistics;
2
+
3
+import io.swagger.annotations.ApiModelProperty;
4
+import lombok.Data;
5
+
6
+/**
7
+ * 指标计算参数
8
+ * 继承时间维度参数,并添加通用的计算参数
9
+ */
10
+@Data
11
+public class IndicatorCalculationParams extends TimeDimensionParams {
12
+
13
+    /**
14
+     * 指标类型
15
+     */
16
+    @ApiModelProperty(value = "指标类型")
17
+    private String indicatorType;
18
+
19
+    /**
20
+     * 模块名称
21
+     */
22
+    @ApiModelProperty(value = "模块名称")
23
+    private String moduleName;
24
+
25
+    /**
26
+     * 部门ID
27
+     */
28
+    @ApiModelProperty(value = "部门ID")
29
+    private Long deptId;
30
+
31
+    /**
32
+     * 用户ID
33
+     */
34
+    @ApiModelProperty(value = "用户ID")
35
+    private Long userId;
36
+
37
+    /**
38
+     * 专用参数对象
39
+     */
40
+    @ApiModelProperty(value = "专用参数对象")
41
+    private Object specificParams;
42
+
43
+    /**
44
+     * 计算上下文参数对象
45
+     */
46
+    @ApiModelProperty(value = "计算上下文参数对象")
47
+    private Object contextParams;
48
+}

+ 85 - 0
airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculationResult.java

@@ -0,0 +1,85 @@
1
+package com.sundot.airport.common.statistics;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+import java.util.Map;
7
+
8
+/**
9
+ * 指标计算结果
10
+ */
11
+@Data
12
+public class IndicatorCalculationResult {
13
+
14
+    /**
15
+     * 指标类型
16
+     */
17
+    private String indicatorType;
18
+
19
+    /**
20
+     * 指标名称
21
+     */
22
+    private String indicatorName;
23
+
24
+    /**
25
+     * 指标值
26
+     */
27
+    private BigDecimal value;
28
+
29
+    /**
30
+     * 环比值
31
+     */
32
+    private String chainRatioValue;
33
+
34
+    /**
35
+     * 同比值
36
+     */
37
+    private String yearOnYearValue;
38
+
39
+    /**
40
+     * 基准值(用于环比、同比计算的参考值)
41
+     */
42
+    private BigDecimal baselineValue;
43
+
44
+    /**
45
+     * 时间维度信息
46
+     */
47
+    private String timeDimensionInfo;
48
+
49
+    /**
50
+     * 额外信息
51
+     */
52
+    private Map<String, Object> extraInfo;
53
+
54
+    /**
55
+     * 计算状态
56
+     */
57
+    private String status;
58
+
59
+
60
+    /**
61
+     * 错误信息
62
+     */
63
+    private String errorMessage;
64
+
65
+    public static IndicatorCalculationResult success(BigDecimal value) {
66
+        IndicatorCalculationResult result = new IndicatorCalculationResult();
67
+        result.setValue(value);
68
+        result.setStatus("SUCCESS");
69
+        return result;
70
+    }
71
+
72
+    public static IndicatorCalculationResult success(BigDecimal value, String chainRatioValue, String yearOnYearValue) {
73
+        IndicatorCalculationResult result = success(value);
74
+        result.setChainRatioValue(chainRatioValue);
75
+        result.setYearOnYearValue(yearOnYearValue);
76
+        return result;
77
+    }
78
+
79
+    public static IndicatorCalculationResult error(String errorMessage) {
80
+        IndicatorCalculationResult result = new IndicatorCalculationResult();
81
+        result.setErrorMessage(errorMessage);
82
+        result.setStatus("ERROR");
83
+        return result;
84
+    }
85
+}

+ 37 - 0
airport-common/src/main/java/com/sundot/airport/common/statistics/IndicatorCalculator.java

@@ -0,0 +1,37 @@
1
+package com.sundot.airport.common.statistics;
2
+
3
+/**
4
+ * 指标计算器接口
5
+ * 定义指标计算的基本操作
6
+ */
7
+public interface IndicatorCalculator<T> {
8
+
9
+    /**
10
+     * 计算指标值
11
+     *
12
+     * @param params 计算参数
13
+     * @return 指标计算结果
14
+     */
15
+    T calculate(IndicatorCalculationParams params);
16
+
17
+    /**
18
+     * 获取指标类型
19
+     *
20
+     * @return 指标类型标识
21
+     */
22
+    String getIndicatorType();
23
+
24
+    /**
25
+     * 获取指标名称
26
+     *
27
+     * @return 指标名称
28
+     */
29
+    String getIndicatorName();
30
+
31
+    /**
32
+     * 获取指标描述
33
+     *
34
+     * @return 指标描述
35
+     */
36
+    String getIndicatorDescription();
37
+}

+ 188 - 0
airport-common/src/main/java/com/sundot/airport/common/statistics/MultiModelIndicatorCalculator.java

@@ -0,0 +1,188 @@
1
+package com.sundot.airport.common.statistics;
2
+
3
+import org.springframework.beans.factory.annotation.Autowired;
4
+import org.springframework.stereotype.Component;
5
+
6
+import java.math.BigDecimal;
7
+import java.util.HashMap;
8
+import java.util.Map;
9
+
10
+/**
11
+ * 多模型指标计算器
12
+ * 支持多种指标类型的计算,包括环比和同比
13
+ */
14
+@Component
15
+public class MultiModelIndicatorCalculator {
16
+
17
+    @Autowired
18
+    private TimeDimensionProcessor timeDimensionProcessor;
19
+
20
+    /**
21
+     * 计算指标,支持环比和同比
22
+     *
23
+     * @param calculator 指标计算器
24
+     * @param params     计算参数
25
+     * @return 指标计算结果
26
+     */
27
+    public IndicatorCalculationResult calculateWithRatio(IndicatorCalculator<?> calculator,
28
+                                                         IndicatorCalculationParams params) {
29
+        // 首先计算当前指标值
30
+        Object currentValue = calculator.calculate(params);
31
+        BigDecimal currentDecimalValue = convertToBigDecimal(currentValue);
32
+
33
+        IndicatorCalculationResult result = new IndicatorCalculationResult();
34
+        result.setIndicatorType(calculator.getIndicatorType());
35
+        result.setIndicatorName(calculator.getIndicatorName());
36
+        result.setValue(currentDecimalValue);
37
+
38
+        // 设置时间维度信息
39
+        StringBuilder timeInfo = new StringBuilder();
40
+        if (params.getYear() != null) {
41
+            timeInfo.append(params.getYear()).append("年");
42
+            if (params.getQuarter() != null) {
43
+                timeInfo.append(params.getQuarter()).append("季度");
44
+            } else if (params.getMonth() != null) {
45
+                timeInfo.append(params.getMonth()).append("月");
46
+            }
47
+        } else if (params.getStartTime() != null && params.getEndTime() != null) {
48
+            timeInfo.append(params.getStartTime()).append("至").append(params.getEndTime());
49
+        }
50
+        result.setTimeDimensionInfo(timeInfo.toString());
51
+
52
+        // 根据时间维度决定计算哪些比率
53
+        boolean needChainRatio = params.getChainRatio() != null && params.getChainRatio();
54
+        boolean needYearOnYear = params.getYearOnYear() != null && params.getYearOnYear();
55
+
56
+        // 当选择年度时,只计算同比
57
+        if (params.getYear() != null && params.getQuarter() == null && params.getMonth() == null) {
58
+            needChainRatio = false;
59
+        }
60
+
61
+        // 计算环比
62
+        if (needChainRatio) {
63
+            try {
64
+                // 计算环比时间范围
65
+                TimeDimensionParams chainRatioParams = timeDimensionProcessor.calculateChainRatioTimeRange(
66
+                        params, params.getComparePeriods());
67
+
68
+                // 创建环比参数
69
+                IndicatorCalculationParams chainRatioCalcParams = new IndicatorCalculationParams();
70
+                chainRatioCalcParams.setStartTime(chainRatioParams.getStartTime());
71
+                chainRatioCalcParams.setEndTime(chainRatioParams.getEndTime());
72
+                chainRatioCalcParams.setYear(chainRatioParams.getYear());
73
+                chainRatioCalcParams.setQuarter(chainRatioParams.getQuarter());
74
+                chainRatioCalcParams.setMonth(chainRatioParams.getMonth());
75
+                chainRatioCalcParams.setIndicatorType(params.getIndicatorType());
76
+                chainRatioCalcParams.setModuleName(params.getModuleName());
77
+                chainRatioCalcParams.setDeptId(params.getDeptId());
78
+                chainRatioCalcParams.setUserId(params.getUserId());
79
+                chainRatioCalcParams.setSpecificParams(params.getSpecificParams());
80
+                chainRatioCalcParams.setContextParams(params.getContextParams());
81
+
82
+                // 计算环比基准值
83
+                Object chainRatioValue = calculator.calculate(chainRatioCalcParams);
84
+                BigDecimal chainRatioDecimalValue = convertToBigDecimal(chainRatioValue);
85
+
86
+                // 计算环比增长率
87
+                String chainRatioGrowth = calculateRatioGrowth(currentDecimalValue, chainRatioDecimalValue);
88
+                result.setChainRatioValue(chainRatioGrowth);
89
+                result.setBaselineValue(chainRatioDecimalValue);
90
+                Map<String, Object> extraInfo = new HashMap<>();
91
+                extraInfo.put("chainRatio", chainRatioParams);
92
+                result.setExtraInfo(extraInfo);
93
+            } catch (Exception e) {
94
+                result.setErrorMessage("环比计算失败: " + e.getMessage());
95
+            }
96
+        }
97
+
98
+        // 计算同比
99
+        if (needYearOnYear) {
100
+            try {
101
+                // 计算同比时间范围
102
+                TimeDimensionParams yearOnYearParams = timeDimensionProcessor.calculateYearOnYearTimeRange(params);
103
+
104
+                // 创建同比参数
105
+                IndicatorCalculationParams yearOnYearCalcParams = new IndicatorCalculationParams();
106
+                yearOnYearCalcParams.setStartTime(yearOnYearParams.getStartTime());
107
+                yearOnYearCalcParams.setEndTime(yearOnYearParams.getEndTime());
108
+                yearOnYearCalcParams.setYear(yearOnYearParams.getYear());
109
+                yearOnYearCalcParams.setQuarter(yearOnYearParams.getQuarter());
110
+                yearOnYearCalcParams.setMonth(yearOnYearParams.getMonth());
111
+                yearOnYearCalcParams.setIndicatorType(params.getIndicatorType());
112
+                yearOnYearCalcParams.setModuleName(params.getModuleName());
113
+                yearOnYearCalcParams.setDeptId(params.getDeptId());
114
+                yearOnYearCalcParams.setUserId(params.getUserId());
115
+                yearOnYearCalcParams.setSpecificParams(params.getSpecificParams());
116
+                yearOnYearCalcParams.setContextParams(params.getContextParams());
117
+
118
+                // 计算同比基准值
119
+                Object yearOnYearValue = calculator.calculate(yearOnYearCalcParams);
120
+                BigDecimal yearOnYearDecimalValue = convertToBigDecimal(yearOnYearValue);
121
+
122
+                // 计算同比增长率
123
+                String yearOnYearGrowth = calculateRatioGrowth(currentDecimalValue, yearOnYearDecimalValue);
124
+                result.setYearOnYearValue(yearOnYearGrowth);
125
+                Map<String, Object> extraInfo = new HashMap<>();
126
+                extraInfo.put("yearOnYear", yearOnYearParams);
127
+                result.setExtraInfo(extraInfo);
128
+            } catch (Exception e) {
129
+                result.setErrorMessage("同比计算失败: " + e.getMessage());
130
+            }
131
+        }
132
+
133
+        result.setStatus("SUCCESS");
134
+        return result;
135
+    }
136
+
137
+    /**
138
+     * 计算比率增长
139
+     *
140
+     * @param current  当前值
141
+     * @param baseline 基准值
142
+     * @return 增长率
143
+     */
144
+    private String calculateRatioGrowth(BigDecimal current, BigDecimal baseline) {
145
+        if (baseline == null || baseline.compareTo(BigDecimal.ZERO) == 0) {
146
+            if (current != null && current.compareTo(BigDecimal.ZERO) > 0) {
147
+                return "--";
148
+            }
149
+            return BigDecimal.ZERO.toString();
150
+        }
151
+
152
+        if (current == null) {
153
+            current = BigDecimal.ZERO;
154
+        }
155
+
156
+        // 计算增长率: (当前值 - 基准值) / 基准值 * 100%
157
+        return current.subtract(baseline)
158
+                .divide(baseline, 4, BigDecimal.ROUND_HALF_UP)
159
+                .multiply(new BigDecimal("100"))
160
+                .setScale(2, BigDecimal.ROUND_HALF_UP).toString();
161
+    }
162
+
163
+    /**
164
+     * 将对象转换为BigDecimal
165
+     *
166
+     * @param obj 待转换对象
167
+     * @return BigDecimal值
168
+     */
169
+    private BigDecimal convertToBigDecimal(Object obj) {
170
+        if (obj == null) {
171
+            return BigDecimal.ZERO;
172
+        }
173
+
174
+        if (obj instanceof BigDecimal) {
175
+            return (BigDecimal) obj;
176
+        } else if (obj instanceof Number) {
177
+            return new BigDecimal(obj.toString());
178
+        } else if (obj instanceof String) {
179
+            try {
180
+                return new BigDecimal((String) obj);
181
+            } catch (NumberFormatException e) {
182
+                return BigDecimal.ZERO;
183
+            }
184
+        } else {
185
+            return BigDecimal.ZERO;
186
+        }
187
+    }
188
+}

+ 210 - 0
airport-item/src/main/java/com/sundot/airport/item/domain/dto/QualificationLevelStatsDTO.java

@@ -0,0 +1,210 @@
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
+import java.util.List;
10
+
11
+/**
12
+ * 资质等级统计DTO
13
+ *
14
+ * @author wangxx
15
+ */
16
+@ApiModel("资质等级统计DTO")
17
+@Data
18
+public class QualificationLevelStatsDTO implements Serializable {
19
+
20
+    private static final long serialVersionUID = 1L;
21
+
22
+    /**
23
+     * 饼状图数据 - 全站范围内五个资质等级的人数占比分布
24
+     */
25
+    @ApiModelProperty("饼状图数据 - 全站资质等级分布")
26
+    private PieChartData pieChart;
27
+
28
+    /**
29
+     * 柱状图数据 - 各大队下五个资质等级的实际人数分布
30
+     */
31
+    @ApiModelProperty("柱状图数据 - 各大队资质等级分布")
32
+    private BarChartData barChart;
33
+
34
+    /**
35
+     * 趋势分析数据 - 支持年、季、月维度的时间趋势分析
36
+     */
37
+    @ApiModelProperty("趋势分析数据")
38
+    private TrendAnalysisData trendAnalysis;
39
+
40
+    /**
41
+     * 饼状图数据类
42
+     */
43
+    @ApiModel("饼状图数据")
44
+    @Data
45
+    public static class PieChartData implements Serializable {
46
+        private static final long serialVersionUID = 1L;
47
+
48
+        /**
49
+         * 数据项列表
50
+         */
51
+        @ApiModelProperty("资质等级分布数据")
52
+        private List<LevelDistribution> data;
53
+
54
+        /**
55
+         * 总人数
56
+         */
57
+        @ApiModelProperty("总人数")
58
+        private Integer totalPersons;
59
+    }
60
+
61
+    /**
62
+     * 柱状图数据类
63
+     */
64
+    @ApiModel("柱状图数据")
65
+    @Data
66
+    public static class BarChartData implements Serializable {
67
+        private static final long serialVersionUID = 1L;
68
+
69
+        /**
70
+         * 大队列表
71
+         */
72
+        @ApiModelProperty("大队数据列表")
73
+        private List<BrigadeData> brigades;
74
+    }
75
+
76
+    /**
77
+     * 资质等级分布数据项
78
+     */
79
+    @ApiModel("资质等级分布数据项")
80
+    @Data
81
+    public static class LevelDistribution implements Serializable {
82
+        private static final long serialVersionUID = 1L;
83
+
84
+        /**
85
+         * 资质等级名称
86
+         */
87
+        @ApiModelProperty("资质等级")
88
+        private String levelName;
89
+
90
+        /**
91
+         * 人数
92
+         */
93
+        @ApiModelProperty("人数")
94
+        private Integer count;
95
+
96
+        /**
97
+         * 占比
98
+         */
99
+        @ApiModelProperty("占比(%)")
100
+        private BigDecimal percentage;
101
+    }
102
+
103
+    /**
104
+     * 大队数据
105
+     */
106
+    @ApiModel("大队数据")
107
+    @Data
108
+    public static class BrigadeData implements Serializable {
109
+        private static final long serialVersionUID = 1L;
110
+
111
+        /**
112
+         * 大队ID
113
+         */
114
+        @ApiModelProperty("大队ID")
115
+        private Long deptId;
116
+
117
+        /**
118
+         * 大队名称
119
+         */
120
+        @ApiModelProperty("大队名称")
121
+        private String deptName;
122
+
123
+        /**
124
+         * 各等级人数数据
125
+         */
126
+        @ApiModelProperty("各等级人数数据")
127
+        private List<LevelCount> levelCounts;
128
+    }
129
+
130
+    /**
131
+     * 等级人数数据
132
+     */
133
+    @ApiModel("等级人数数据")
134
+    @Data
135
+    public static class LevelCount implements Serializable {
136
+        private static final long serialVersionUID = 1L;
137
+
138
+        /**
139
+         * 资质等级名称
140
+         */
141
+        @ApiModelProperty("资质等级")
142
+        private String levelName;
143
+
144
+        /**
145
+         * 人数
146
+         */
147
+        @ApiModelProperty("人数")
148
+        private Integer count;
149
+    }
150
+
151
+    /**
152
+     * 趋势分析数据类
153
+     */
154
+    @ApiModel("趋势分析数据")
155
+    @Data
156
+    public static class TrendAnalysisData implements Serializable {
157
+        private static final long serialVersionUID = 1L;
158
+
159
+        /**
160
+         * 时间维度标识(年/季/月)
161
+         */
162
+        @ApiModelProperty("时间维度标识")
163
+        private String timeDimension;
164
+
165
+        /**
166
+         * 时间点列表
167
+         */
168
+        @ApiModelProperty("时间点列表")
169
+        private List<String> timePoints;
170
+
171
+        /**
172
+         * 各等级趋势数据
173
+         */
174
+        @ApiModelProperty("各等级趋势数据")
175
+        private List<LevelTrendData> levelTrends;
176
+
177
+        /**
178
+         * 总人数趋势数据
179
+         */
180
+        @ApiModelProperty("总人数趋势数据")
181
+        private List<Integer> totalPersonsTrend;
182
+    }
183
+
184
+    /**
185
+     * 等级趋势数据
186
+     */
187
+    @ApiModel("等级趋势数据")
188
+    @Data
189
+    public static class LevelTrendData implements Serializable {
190
+        private static final long serialVersionUID = 1L;
191
+
192
+        /**
193
+         * 资质等级名称
194
+         */
195
+        @ApiModelProperty("资质等级")
196
+        private String levelName;
197
+
198
+        /**
199
+         * 人数趋势数据
200
+         */
201
+        @ApiModelProperty("人数趋势数据")
202
+        private List<Integer> countTrend;
203
+
204
+        /**
205
+         * 占比趋势数据
206
+         */
207
+        @ApiModelProperty("占比趋势数据(%)")
208
+        private List<BigDecimal> percentageTrend;
209
+    }
210
+}

+ 65 - 0
airport-system/src/main/java/com/sundot/airport/system/domain/QualificationLevelConverter.java

@@ -0,0 +1,65 @@
1
+package com.sundot.airport.system.domain;
2
+
3
+import java.util.HashMap;
4
+import java.util.Map;
5
+
6
+/**
7
+ * 资质等级枚举转换工具类
8
+ * 处理数据库存储的枚举值与前端显示的中文之间的转换
9
+ *
10
+ * @author wangxx
11
+ */
12
+public class QualificationLevelConverter {
13
+
14
+    /**
15
+     * 数据库枚举值到中文显示的映射
16
+     */
17
+    private static final Map<String, String> DB_TO_DISPLAY = new HashMap<>();
18
+
19
+    /**
20
+     * 中文显示到数据库枚举值的映射
21
+     */
22
+    private static final Map<String, String> DISPLAY_TO_DB = new HashMap<>();
23
+
24
+    static {
25
+        // 初始化映射关系
26
+        DB_TO_DISPLAY.put("LEVEL_ONE", "一级");
27
+        DB_TO_DISPLAY.put("LEVEL_TWO", "二级");
28
+        DB_TO_DISPLAY.put("LEVEL_THREE", "三级");
29
+        DB_TO_DISPLAY.put("LEVEL_FOUR", "四级");
30
+        DB_TO_DISPLAY.put("LEVEL_FIVE", "五级");
31
+
32
+        // 反向映射
33
+        for (Map.Entry<String, String> entry : DB_TO_DISPLAY.entrySet()) {
34
+            DISPLAY_TO_DB.put(entry.getValue(), entry.getKey());
35
+        }
36
+    }
37
+
38
+    /**
39
+     * 将数据库枚举值转换为中文显示
40
+     *
41
+     * @param dbValue 数据库存储的枚举值(如 LEVEL_ONE)
42
+     * @return 中文显示(如 一级)
43
+     */
44
+    public static String toDisplay(String dbValue) {
45
+        if (dbValue == null || dbValue.trim().isEmpty()) {
46
+            return "";
47
+        }
48
+        return DB_TO_DISPLAY.getOrDefault(dbValue.toUpperCase(), dbValue);
49
+    }
50
+
51
+    /**
52
+     * 将中文显示转换为数据库枚举值
53
+     *
54
+     * @param displayValue 中文显示(如 一级)
55
+     * @return 数据库枚举值(如 LEVEL_ONE)
56
+     */
57
+    public static String toDatabase(String displayValue) {
58
+        if (displayValue == null || displayValue.trim().isEmpty()) {
59
+            return "";
60
+        }
61
+        return DISPLAY_TO_DB.getOrDefault(displayValue, displayValue);
62
+    }
63
+
64
+
65
+}