|
|
@@ -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
|
}
|