Преглед на файлове

重构completion-comparison接口:xAxis改为周期标签,data改为各周期聚合完成数(美兰)

年:[去年总数, 当年总数],仅同比
季度/月:[去年同期, 上期, 当期],同比+环比

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simonlll преди 2 седмици
родител
ревизия
117cc53d13
променени са 1 файла, в които са добавени 203 реда и са изтрити 222 реда
  1. 203 222
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamComparisonController.java

+ 203 - 222
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamComparisonController.java

@@ -21,9 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
21 21
 import java.math.BigDecimal;
22 22
 import java.math.RoundingMode;
23 23
 import java.sql.Date;
24
-import java.text.SimpleDateFormat;
25 24
 import java.time.LocalDate;
26
-import java.time.format.DateTimeFormatter;
27 25
 import java.util.*;
28 26
 import java.util.stream.Collectors;
29 27
 
@@ -31,11 +29,13 @@ import java.util.stream.Collectors;
31 29
  * 抽问抽答完成趋势对比接口(PC管理端,美兰4级架构)
32 30
  * STATION(安检站)> BRIGADE(大队)> MANAGER(主管)> 班组
33 31
  *
34
- * 返回当前周期的时间序列趋势数据(同 /completion-trend),
35
- * 同时在每条 series 上附加 yoyRate(同比)和 chainRatio(环比)。
32
+ * xAxis 为周期标签,每条 series 的 data 为各周期聚合完成数:
33
+ *   YEAR    → [去年总数, 当年总数]               xAxis: ["2025年","2026年"]
34
+ *   QUARTER → [去年同季度, 上季度, 当季度]        xAxis: ["2025年第1季度","2025年第4季度","2026年第1季度"]
35
+ *   MONTH   → [去年同月, 上月, 当月]              xAxis: ["2025年3月","2026年2月","2026年3月"]
36 36
  *
37
- * yoyRate  = (本期总数 - 去年同期总数) / 去年同期总数 × 100
38
- * chainRatio = (本期总数 - 上期总数) / 上期总数 × 100(仅 QUARTER/MONTH)
37
+ * 同比 yoyRate   = (当期 - 去年同期) / 去年同期 × 100
38
+ * 环比 chainRatio = (当期 - 上期)   / 上期     × 100(仅 QUARTER/MONTH)
39 39
  */
40 40
 @RestController
41 41
 @RequestMapping("/v1/cs/app/daily-exam")
@@ -43,6 +43,8 @@ public class DailyExamComparisonController {
43 43
 
44 44
     private static final Logger log = LoggerFactory.getLogger(DailyExamComparisonController.class);
45 45
 
46
+    private static final String[] QUARTER_NAMES = {"第一", "第二", "第三", "第四"};
47
+
46 48
     @Autowired
47 49
     private DailyTaskMapper dailyTaskMapper;
48 50
 
@@ -89,23 +91,18 @@ public class DailyExamComparisonController {
89 91
                 return HttpResult.success(noShow);
90 92
             }
91 93
 
92
-            int curYear = (year != null) ? year : LocalDate.now().getYear();
93
-            int curQuarter = (quarter != null) ? quarter : ((LocalDate.now().getMonthValue() - 1) / 3 + 1);
94
-            int curMonth = (month != null) ? month : LocalDate.now().getMonthValue();
95
-
96
-            // 当前周期
97
-            LocalDate[] currentRange = resolveDateRange(dateRangeQueryType, curYear, curQuarter, curMonth);
98
-            // 去年同期(用于计算同比)
99
-            LocalDate[] yoyRange = resolveDateRange(dateRangeQueryType, curYear - 1, curQuarter, curMonth);
100
-            // 上一期(用于计算环比,仅 QUARTER/MONTH)
101
-            LocalDate[] prevRange = buildPrevRange(dateRangeQueryType, curYear, curQuarter, curMonth);
94
+            LocalDate today = LocalDate.now();
95
+            int curYear = (year != null) ? year : today.getYear();
96
+            int curQuarter = (quarter != null) ? quarter : ((today.getMonthValue() - 1) / 3 + 1);
97
+            int curMonth = (month != null) ? month : today.getMonthValue();
102 98
 
103
-            boolean byMonth = "YEAR".equals(dateRangeQueryType);
104
-            List<String> xAxis = buildXAxis(currentRange[0], currentRange[1], byMonth);
99
+            List<String> xAxis = buildXAxisLabels(dateRangeQueryType, curYear, curQuarter, curMonth);
100
+            List<LocalDate[]> periods = buildPeriods(dateRangeQueryType, curYear, curQuarter, curMonth);
101
+            boolean hasChain = !"YEAR".equals(dateRangeQueryType);
105 102
 
106
-            List<Map<String, Object>> series = buildComparisonSeries(
107
-                    scopeType, scopeId, xAxis, currentRange, yoyRange, prevRange,
108
-                    byMonth, isStation, isBrigade, isManager, userDeptId, loginUser);
103
+            List<Map<String, Object>> series = buildSeries(
104
+                    scopeType, scopeId, xAxis, periods, hasChain,
105
+                    isStation, isBrigade, isManager, userDeptId, loginUser);
109 106
 
110 107
             Map<String, Object> result = new LinkedHashMap<>();
111 108
             result.put("show", true);
@@ -119,291 +116,275 @@ public class DailyExamComparisonController {
119 116
         }
120 117
     }
121 118
 
119
+    // -------------------------------------------------------------------------
120
+    // xAxis 标签
121
+    // -------------------------------------------------------------------------
122
+
122 123
     /**
123
-     * 计算上一期日期范围(QUARTER/MONTH);YEAR 返回 null(无环比)
124
+     * YEAR    → ["2025年", "2026年"]
125
+     * QUARTER → ["2025年第1季度", "2025年第4季度", "2026年第1季度"]
126
+     * MONTH   → ["2025年3月", "2026年2月", "2026年3月"]
124 127
      */
125
-    private LocalDate[] buildPrevRange(String type, int year, int quarter, int month) {
126
-        if ("YEAR".equals(type)) return null;
127
-        if ("QUARTER".equals(type)) {
128
-            int prevQ = quarter - 1;
129
-            int prevYear = year;
130
-            if (prevQ < 1) { prevQ = 4; prevYear = year - 1; }
131
-            return resolveDateRange("QUARTER", prevYear, prevQ, month);
128
+    private List<String> buildXAxisLabels(String type, int year, int quarter, int month) {
129
+        List<String> labels = new ArrayList<>();
130
+        switch (type == null ? "MONTH" : type) {
131
+            case "YEAR":
132
+                labels.add((year - 1) + "年");
133
+                labels.add(year + "年");
134
+                break;
135
+            case "QUARTER": {
136
+                labels.add((year - 1) + "年" + QUARTER_NAMES[quarter - 1] + "季度");
137
+                int prevQ = quarter - 1, prevYear = year;
138
+                if (prevQ < 1) { prevQ = 4; prevYear = year - 1; }
139
+                labels.add(prevYear + "年" + QUARTER_NAMES[prevQ - 1] + "季度");
140
+                labels.add(year + "年" + QUARTER_NAMES[quarter - 1] + "季度");
141
+                break;
142
+            }
143
+            default: {
144
+                labels.add((year - 1) + "年" + month + "月");
145
+                int prevM = month - 1, prevYear = year;
146
+                if (prevM < 1) { prevM = 12; prevYear = year - 1; }
147
+                labels.add(prevYear + "年" + prevM + "月");
148
+                labels.add(year + "年" + month + "月");
149
+                break;
150
+            }
132 151
         }
133
-        // MONTH
134
-        int prevM = month - 1;
135
-        int prevYear = year;
136
-        if (prevM < 1) { prevM = 12; prevYear = year - 1; }
137
-        return resolveDateRange("MONTH", prevYear, quarter, prevM);
152
+        return labels;
138 153
     }
139 154
 
155
+    // -------------------------------------------------------------------------
156
+    // 周期日期范围
157
+    // -------------------------------------------------------------------------
158
+
140 159
     /**
141
-     * 构建 series 列表:每条 series 含当前周期趋势数据 + yoyRate + chainRatio
160
+     * 返回各周期日期范围(顺序与 xAxis 对应)
161
+     * YEAR    → [去年全年, 今年全年]           (size=2)
162
+     * QUARTER → [去年同季度, 上季度, 当季度]    (size=3)
163
+     * MONTH   → [去年同月, 上月, 当月]          (size=3)
142 164
      */
143
-    private List<Map<String, Object>> buildComparisonSeries(
165
+    private List<LocalDate[]> buildPeriods(String type, int year, int quarter, int month) {
166
+        List<LocalDate[]> periods = new ArrayList<>();
167
+        switch (type == null ? "MONTH" : type) {
168
+            case "YEAR":
169
+                periods.add(yearRange(year - 1));
170
+                periods.add(yearRange(year));
171
+                break;
172
+            case "QUARTER": {
173
+                periods.add(quarterRange(year - 1, quarter));
174
+                int prevQ = quarter - 1, prevYear = year;
175
+                if (prevQ < 1) { prevQ = 4; prevYear = year - 1; }
176
+                periods.add(quarterRange(prevYear, prevQ));
177
+                periods.add(quarterRange(year, quarter));
178
+                break;
179
+            }
180
+            default: {
181
+                periods.add(monthRange(year - 1, month));
182
+                int prevM = month - 1, prevYear = year;
183
+                if (prevM < 1) { prevM = 12; prevYear = year - 1; }
184
+                periods.add(monthRange(prevYear, prevM));
185
+                periods.add(monthRange(year, month));
186
+                break;
187
+            }
188
+        }
189
+        return periods;
190
+    }
191
+
192
+    private LocalDate[] yearRange(int y) {
193
+        return new LocalDate[]{LocalDate.of(y, 1, 1), LocalDate.of(y, 12, 31)};
194
+    }
195
+
196
+    private LocalDate[] quarterRange(int y, int q) {
197
+        LocalDate start = LocalDate.of(y, (q - 1) * 3 + 1, 1);
198
+        return new LocalDate[]{start, start.plusMonths(3).minusDays(1)};
199
+    }
200
+
201
+    private LocalDate[] monthRange(int y, int m) {
202
+        LocalDate start = LocalDate.of(y, m, 1);
203
+        return new LocalDate[]{start, start.withDayOfMonth(start.lengthOfMonth())};
204
+    }
205
+
206
+    // -------------------------------------------------------------------------
207
+    // series 构建
208
+    // -------------------------------------------------------------------------
209
+
210
+    private List<Map<String, Object>> buildSeries(
144 211
             String scopeType, Long scopeId,
145
-            List<String> xAxis, LocalDate[] currentRange, LocalDate[] yoyRange, LocalDate[] prevRange,
146
-            boolean byMonth, boolean isStation, boolean isBrigade, boolean isManager,
212
+            List<String> xAxis, List<LocalDate[]> periods, boolean hasChain,
213
+            boolean isStation, boolean isBrigade, boolean isManager,
147 214
             Long userDeptId, LoginUser loginUser) {
148 215
 
216
+        int currentIdx = periods.size() - 1;
217
+
149 218
         if ("MANAGER".equals(scopeType) && scopeId != null) {
150
-            List<DailyTask> current = queryTasksByDepartmentId(scopeId, currentRange[0], currentRange[1]);
151
-            String name = current.stream().map(DailyTask::getDtDepartmentName)
152
-                    .filter(Objects::nonNull).findFirst().orElse("主管" + scopeId);
153
-            int yoyCount = countCompleted(queryTasksByDepartmentId(scopeId, yoyRange[0], yoyRange[1]));
154
-            Integer prevCount = prevRange != null
155
-                    ? countCompleted(queryTasksByDepartmentId(scopeId, prevRange[0], prevRange[1])) : null;
219
+            List<Integer> counts = countByPeriods(periods, r -> countCompletedByDepartmentId(scopeId, r[0], r[1]));
156 220
             return Collections.singletonList(
157
-                    buildSeriesItem(name, scopeId, null, xAxis, current, byMonth, yoyCount, prevCount));
221
+                    buildSeriesItem(getDeptName(scopeId), scopeId, null, counts, currentIdx, hasChain));
158 222
 
159 223
         } else if ("USER".equals(scopeType) && scopeId != null) {
160
-            List<DailyTask> current = queryTasksByUserId(scopeId, currentRange[0], currentRange[1]);
161
-            String name = current.stream().map(DailyTask::getDtUserName)
162
-                    .filter(Objects::nonNull).findFirst().orElse("用户" + scopeId);
163
-            int yoyCount = countCompleted(queryTasksByUserId(scopeId, yoyRange[0], yoyRange[1]));
164
-            Integer prevCount = prevRange != null
165
-                    ? countCompleted(queryTasksByUserId(scopeId, prevRange[0], prevRange[1])) : null;
224
+            List<Integer> counts = countByPeriods(periods, r -> countCompletedByUserId(scopeId, r[0], r[1]));
166 225
             return Collections.singletonList(
167
-                    buildSeriesItem(name, null, scopeId, xAxis, current, byMonth, yoyCount, prevCount));
226
+                    buildSeriesItem("用户" + scopeId, null, scopeId, counts, currentIdx, hasChain));
168 227
 
169 228
         } else if ("BRIGADE".equals(scopeType) && scopeId != null) {
170
-            return buildBrigadeSeries(scopeId, xAxis, currentRange, yoyRange, prevRange, byMonth);
229
+            return buildBrigadeSeriesList(scopeId, periods, hasChain, currentIdx);
171 230
 
172 231
         } else {
173 232
             Long stationDeptId = ("STATION".equals(scopeType) && scopeId != null) ? scopeId : userDeptId;
174 233
             if (isStation || "STATION".equals(scopeType)) {
175
-                return buildStationSeries(stationDeptId, xAxis, currentRange, yoyRange, prevRange, byMonth);
234
+                return buildStationSeriesList(stationDeptId, periods, hasChain, currentIdx);
176 235
             } else if (isBrigade) {
177
-                return buildBrigadeSeries(userDeptId, xAxis, currentRange, yoyRange, prevRange, byMonth);
236
+                return buildBrigadeSeriesList(userDeptId, periods, hasChain, currentIdx);
178 237
             } else {
179
-                // 主管:单条 series
238
+                // 主管:本主管室单条 series
180 239
                 String deptName = loginUser.getUser().getDept() != null
181 240
                         ? loginUser.getUser().getDept().getDeptName() : "本主管";
182
-                List<DailyTask> current = queryTasksByDepartmentId(userDeptId, currentRange[0], currentRange[1]);
183
-                int yoyCount = countCompleted(queryTasksByDepartmentId(userDeptId, yoyRange[0], yoyRange[1]));
184
-                Integer prevCount = prevRange != null
185
-                        ? countCompleted(queryTasksByDepartmentId(userDeptId, prevRange[0], prevRange[1])) : null;
241
+                List<Integer> counts = countByPeriods(periods,
242
+                        r -> countCompletedByDepartmentId(userDeptId, r[0], r[1]));
186 243
                 return Collections.singletonList(
187
-                        buildSeriesItem(deptName, userDeptId, null, xAxis, current, byMonth, yoyCount, prevCount));
244
+                        buildSeriesItem(deptName, userDeptId, null, counts, currentIdx, hasChain));
188 245
             }
189 246
         }
190 247
     }
191 248
 
192 249
     /**
193
-     * 站长视角:按大队分组,各大队各一条 series + 全站汇总
250
+     * 站长视角:各大队独立 series + 全站汇总
194 251
      */
195
-    private List<Map<String, Object>> buildStationSeries(Long stationDeptId,
196
-                                                          List<String> xAxis,
197
-                                                          LocalDate[] currentRange,
198
-                                                          LocalDate[] yoyRange,
199
-                                                          LocalDate[] prevRange,
200
-                                                          boolean byMonth) {
252
+    private List<Map<String, Object>> buildStationSeriesList(
253
+            Long stationDeptId, List<LocalDate[]> periods, boolean hasChain, int currentIdx) {
254
+
201 255
         List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(stationDeptId);
202 256
         if (subDepts == null) subDepts = Collections.emptyList();
203
-
204
-        // 大队(站的直接下级)
205
-        List<SysDept> brigadeDepts = subDepts.stream()
257
+        List<SysDept> brigades = subDepts.stream()
206 258
                 .filter(d -> stationDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
207 259
                 .collect(Collectors.toList());
208 260
 
209 261
         List<Map<String, Object>> series = new ArrayList<>();
210
-        List<DailyTask> stationCurrentAll = new ArrayList<>();
211
-        int stationYoyTotal = 0;
212
-        int stationPrevTotal = 0;
213
-
214
-        for (SysDept brigade : brigadeDepts) {
215
-            List<DailyTask> current = queryTasksByBrigadeId(brigade.getDeptId(), currentRange[0], currentRange[1]);
216
-            int yoyCount = countCompletedByBrigadeId(brigade.getDeptId(), yoyRange[0], yoyRange[1]);
217
-            int prevCount = prevRange != null
218
-                    ? countCompletedByBrigadeId(brigade.getDeptId(), prevRange[0], prevRange[1]) : 0;
219
-
220
-            stationCurrentAll.addAll(current);
221
-            stationYoyTotal += yoyCount;
222
-            stationPrevTotal += prevCount;
223
-
224
-            series.add(buildSeriesItem(brigade.getDeptName(), brigade.getDeptId(), null,
225
-                    xAxis, current, byMonth, yoyCount, prevRange != null ? prevCount : null));
262
+        int[] stationTotals = new int[periods.size()];
263
+
264
+        for (SysDept brigade : brigades) {
265
+            List<Integer> counts = countByPeriods(periods,
266
+                    r -> countCompletedByBrigadeId(brigade.getDeptId(), r[0], r[1]));
267
+            for (int i = 0; i < counts.size(); i++) stationTotals[i] += counts.get(i);
268
+            series.add(buildSeriesItem(brigade.getDeptName(), brigade.getDeptId(), null, counts, currentIdx, hasChain));
226 269
         }
227 270
 
228
-        // 全站汇总 series
229
-        int finalYoyTotal = stationYoyTotal;
230
-        Integer finalPrevTotal = prevRange != null ? stationPrevTotal : null;
231
-        series.add(buildSeriesItem("全站", null, null, xAxis, stationCurrentAll, byMonth,
232
-                finalYoyTotal, finalPrevTotal));
271
+        List<Integer> totalCounts = new ArrayList<>();
272
+        for (int c : stationTotals) totalCounts.add(c);
273
+        series.add(buildSeriesItem("全站", null, null, totalCounts, currentIdx, hasChain));
233 274
         return series;
234 275
     }
235 276
 
236 277
     /**
237
-     * 大队视角:按主管分组,各主管各一条 series
278
+     * 大队视角:各主管独立 series
238 279
      */
239
-    private List<Map<String, Object>> buildBrigadeSeries(Long brigadeId,
240
-                                                          List<String> xAxis,
241
-                                                          LocalDate[] currentRange,
242
-                                                          LocalDate[] yoyRange,
243
-                                                          LocalDate[] prevRange,
244
-                                                          boolean byMonth) {
280
+    private List<Map<String, Object>> buildBrigadeSeriesList(
281
+            Long brigadeId, List<LocalDate[]> periods, boolean hasChain, int currentIdx) {
282
+
245 283
         List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(brigadeId);
246 284
         if (subDepts == null) subDepts = Collections.emptyList();
247
-
248
-        List<SysDept> managerDepts = subDepts.stream()
285
+        List<SysDept> managers = subDepts.stream()
249 286
                 .filter(d -> brigadeId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
250 287
                 .collect(Collectors.toList());
251 288
 
252 289
         List<Map<String, Object>> series = new ArrayList<>();
253
-        for (SysDept manager : managerDepts) {
254
-            List<DailyTask> current = queryTasksByDepartmentId(manager.getDeptId(), currentRange[0], currentRange[1]);
255
-            int yoyCount = countCompleted(queryTasksByDepartmentId(manager.getDeptId(), yoyRange[0], yoyRange[1]));
256
-            Integer prevCount = prevRange != null
257
-                    ? countCompleted(queryTasksByDepartmentId(manager.getDeptId(), prevRange[0], prevRange[1])) : null;
258
-            series.add(buildSeriesItem(manager.getDeptName(), manager.getDeptId(), null,
259
-                    xAxis, current, byMonth, yoyCount, prevCount));
290
+        for (SysDept manager : managers) {
291
+            List<Integer> counts = countByPeriods(periods,
292
+                    r -> countCompletedByDepartmentId(manager.getDeptId(), r[0], r[1]));
293
+            series.add(buildSeriesItem(manager.getDeptName(), manager.getDeptId(), null, counts, currentIdx, hasChain));
260 294
         }
261 295
         return series;
262 296
     }
263 297
 
298
+    // -------------------------------------------------------------------------
299
+    // series item 构建
300
+    // -------------------------------------------------------------------------
301
+
264 302
     /**
265
-     * 构建单条 series:趋势数据来自当前周期,yoyRate/chainRatio 由聚合总数计算
303
+     * 构建单条 series
304
+     *
305
+     * @param counts     各周期完成数,顺序与 xAxis 对应
306
+     * @param currentIdx 当期在 counts 中的下标(最后一个)
307
+     * @param hasChain   是否计算环比
266 308
      */
267 309
     private Map<String, Object> buildSeriesItem(String name, Long deptId, Long userId,
268
-                                                 List<String> xAxis, List<DailyTask> currentTasks,
269
-                                                 boolean byMonth, int yoyBaseCount, Integer prevCount) {
270
-        List<Integer> data = buildSeriesData(xAxis, currentTasks, byMonth);
271
-        int currentTotal = data.stream().mapToInt(Integer::intValue).sum();
310
+                                                 List<Integer> counts, int currentIdx, boolean hasChain) {
311
+        int currentVal = counts.get(currentIdx);
312
+        int yoyVal = counts.get(0);
313
+        Integer prevVal = hasChain ? counts.get(1) : null;
272 314
 
273 315
         Map<String, Object> s = new LinkedHashMap<>();
274 316
         s.put("name", name);
275 317
         if (deptId != null) s.put("deptId", deptId);
276 318
         if (userId != null) s.put("userId", userId);
277
-        s.put("data", data);
278
-        s.put("yoyRate", calcRate(currentTotal, yoyBaseCount));
279
-        s.put("chainRatio", prevCount != null ? calcRate(currentTotal, prevCount) : null);
319
+        s.put("data", counts);
320
+        s.put("yoyRate", calcRate(currentVal, yoyVal));
321
+        s.put("chainRatio", prevVal != null ? calcRate(currentVal, prevVal) : null);
280 322
         return s;
281 323
     }
282 324
 
283
-    /**
284
-     * 将任务列表按时间维度汇聚,对齐到 xAxis
285
-     */
286
-    private List<Integer> buildSeriesData(List<String> xAxis, List<DailyTask> tasks, boolean byMonth) {
287
-        SimpleDateFormat sdf = new SimpleDateFormat(byMonth ? "yyyy-MM" : "yyyy-MM-dd");
288
-        Map<String, Long> countMap = tasks.stream()
289
-                .filter(t -> "COMPLETED".equals(t.getDtStatus()) && t.getDtBusinessDate() != null)
290
-                .collect(Collectors.groupingBy(
291
-                        t -> sdf.format(t.getDtBusinessDate()),
292
-                        Collectors.counting()));
293
-        List<Integer> result = new ArrayList<>();
294
-        for (String label : xAxis) {
295
-            result.add(countMap.getOrDefault(label, 0L).intValue());
296
-        }
297
-        return result;
325
+    // -------------------------------------------------------------------------
326
+    // 统计辅助
327
+    // -------------------------------------------------------------------------
328
+
329
+    @FunctionalInterface
330
+    interface PeriodCounter {
331
+        int count(LocalDate[] range);
298 332
     }
299 333
 
300
-    /**
301
-     * 生成 X 轴标签:YEAR→每月(yyyy-MM);QUARTER/MONTH→每天(yyyy-MM-dd)
302
-     */
303
-    private List<String> buildXAxis(LocalDate start, LocalDate end, boolean byMonth) {
304
-        List<String> xAxis = new ArrayList<>();
305
-        if (byMonth) {
306
-            LocalDate cursor = start.withDayOfMonth(1);
307
-            LocalDate endMonth = end.withDayOfMonth(1);
308
-            while (!cursor.isAfter(endMonth)) {
309
-                xAxis.add(cursor.format(DateTimeFormatter.ofPattern("yyyy-MM")));
310
-                cursor = cursor.plusMonths(1);
311
-            }
312
-        } else {
313
-            LocalDate cursor = start;
314
-            while (!cursor.isAfter(end)) {
315
-                xAxis.add(cursor.toString());
316
-                cursor = cursor.plusDays(1);
317
-            }
334
+    private List<Integer> countByPeriods(List<LocalDate[]> periods, PeriodCounter counter) {
335
+        List<Integer> result = new ArrayList<>();
336
+        for (LocalDate[] range : periods) {
337
+            result.add(counter.count(range));
318 338
         }
319
-        return xAxis;
339
+        return result;
320 340
     }
321 341
 
322
-    /**
323
-     * 计算变化率百分比;base=0 时返回 null
324
-     */
325
-    private BigDecimal calcRate(int current, int base) {
326
-        if (base == 0) return null;
327
-        return BigDecimal.valueOf(current - base)
328
-                .multiply(BigDecimal.valueOf(100))
329
-                .divide(BigDecimal.valueOf(base), 2, RoundingMode.HALF_UP);
342
+    private int countCompletedByDepartmentId(Long deptId, LocalDate start, LocalDate end) {
343
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
344
+        w.eq(DailyTask::getDtDepartmentId, deptId);
345
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
346
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
347
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
348
+        w.eq(DailyTask::getDelStatus, 0);
349
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
330 350
     }
331 351
 
332
-    private int countCompleted(List<DailyTask> tasks) {
333
-        return (int) tasks.stream().filter(t -> "COMPLETED".equals(t.getDtStatus())).count();
352
+    private int countCompletedByUserId(Long userId, LocalDate start, LocalDate end) {
353
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
354
+        w.eq(DailyTask::getDtUserId, userId);
355
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
356
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
357
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
358
+        w.eq(DailyTask::getDelStatus, 0);
359
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
334 360
     }
335 361
 
336
-    /**
337
-     * 查询大队下所有主管的任务(通过大队子部门的 dtDepartmentId)
338
-     */
339
-    private List<DailyTask> queryTasksByBrigadeId(Long brigadeId, LocalDate start, LocalDate end) {
362
+    private int countCompletedByBrigadeId(Long brigadeId, LocalDate start, LocalDate end) {
340 363
         List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(brigadeId);
341 364
         if (subDepts == null) subDepts = Collections.emptyList();
342 365
         Set<Long> managerIds = subDepts.stream()
343 366
                 .filter(d -> brigadeId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
344 367
                 .map(SysDept::getDeptId)
345 368
                 .collect(Collectors.toSet());
346
-        if (managerIds.isEmpty()) return Collections.emptyList();
347
-        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
348
-        wrapper.in(DailyTask::getDtDepartmentId, managerIds);
349
-        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
350
-        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
351
-        wrapper.eq(DailyTask::getDelStatus, 0);
352
-        return dailyTaskMapper.selectList(wrapper);
369
+        if (managerIds.isEmpty()) return 0;
370
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
371
+        w.in(DailyTask::getDtDepartmentId, managerIds);
372
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
373
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
374
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
375
+        w.eq(DailyTask::getDelStatus, 0);
376
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
353 377
     }
354 378
 
355
-    private int countCompletedByBrigadeId(Long brigadeId, LocalDate start, LocalDate end) {
356
-        return countCompleted(queryTasksByBrigadeId(brigadeId, start, end));
357
-    }
358
-
359
-    private LocalDate[] resolveDateRange(String type, Integer year, Integer quarter, Integer month) {
360
-        LocalDate today = LocalDate.now();
361
-        int y = (year != null) ? year : today.getYear();
362
-        LocalDate start, end;
363
-        switch (type == null ? "MONTH" : type) {
364
-            case "YEAR":
365
-                start = LocalDate.of(y, 1, 1);
366
-                end = LocalDate.of(y, 12, 31);
367
-                break;
368
-            case "QUARTER":
369
-                int q = (quarter != null) ? quarter : 1;
370
-                start = LocalDate.of(y, (q - 1) * 3 + 1, 1);
371
-                end = start.plusMonths(3).minusDays(1);
372
-                break;
373
-            case "MONTH":
374
-            default:
375
-                int m = (month != null) ? month : today.getMonthValue();
376
-                start = LocalDate.of(y, m, 1);
377
-                end = start.withDayOfMonth(start.lengthOfMonth());
378
-                break;
379
-        }
380
-        return new LocalDate[]{start, end};
379
+    private String getDeptName(Long deptId) {
380
+        SysDept dept = sysDeptMapper.selectById(deptId);
381
+        return dept != null ? dept.getDeptName() : "部门" + deptId;
381 382
     }
382 383
 
383
-    private List<DailyTask> queryTasksByDeptIds(Set<Long> deptIds, LocalDate start, LocalDate end) {
384
-        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
385
-        wrapper.in(DailyTask::getDtDeptId, deptIds);
386
-        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
387
-        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
388
-        wrapper.eq(DailyTask::getDelStatus, 0);
389
-        return dailyTaskMapper.selectList(wrapper);
390
-    }
391
-
392
-    private List<DailyTask> queryTasksByDepartmentId(Long departmentId, LocalDate start, LocalDate end) {
393
-        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
394
-        wrapper.eq(DailyTask::getDtDepartmentId, departmentId);
395
-        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
396
-        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
397
-        wrapper.eq(DailyTask::getDelStatus, 0);
398
-        return dailyTaskMapper.selectList(wrapper);
399
-    }
400
-
401
-    private List<DailyTask> queryTasksByUserId(Long userId, LocalDate start, LocalDate end) {
402
-        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
403
-        wrapper.eq(DailyTask::getDtUserId, userId);
404
-        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
405
-        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
406
-        wrapper.eq(DailyTask::getDelStatus, 0);
407
-        return dailyTaskMapper.selectList(wrapper);
384
+    private BigDecimal calcRate(int current, int base) {
385
+        if (base == 0) return null;
386
+        return BigDecimal.valueOf(current - base)
387
+                .multiply(BigDecimal.valueOf(100))
388
+                .divide(BigDecimal.valueOf(base), 2, RoundingMode.HALF_UP);
408 389
     }
409 390
 }