Jiaqi Xia

feat: add SuspensionService and SuspensionServiceImpl, refactor suspension logic to use union window

...@@ -7,6 +7,7 @@ import com.infoloop.tianting.mealorderservice.*; ...@@ -7,6 +7,7 @@ import com.infoloop.tianting.mealorderservice.*;
7 import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient; 7 import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
8 import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient; 8 import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
9 import com.infoloop.tianting.service.client.MealOrderServiceRpcClient; 9 import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
10 +import com.infoloop.tianting.service.SuspensionService;
10 import com.infoloop.tianting.store.WeeklyTaskStore; 11 import com.infoloop.tianting.store.WeeklyTaskStore;
11 import com.infoloop.tianting.utils.DateUtil; 12 import com.infoloop.tianting.utils.DateUtil;
12 import lombok.RequiredArgsConstructor; 13 import lombok.RequiredArgsConstructor;
...@@ -46,6 +47,8 @@ public class AutoCreateMealOrderTask { ...@@ -46,6 +47,8 @@ public class AutoCreateMealOrderTask {
46 47
47 private final WeeklyTaskStore weeklyTaskStore; 48 private final WeeklyTaskStore weeklyTaskStore;
48 49
50 + private final SuspensionService suspensionService;
51 +
49 @Scheduled(cron = "0 0/5 * * * ?") 52 @Scheduled(cron = "0 0/5 * * * ?")
50 public void executeTask() { 53 public void executeTask() {
51 final int enterpriseId = 449; 54 final int enterpriseId = 449;
...@@ -188,6 +191,7 @@ public class AutoCreateMealOrderTask { ...@@ -188,6 +191,7 @@ public class AutoCreateMealOrderTask {
188 // 构建创建请求 191 // 构建创建请求
189 final var creations = notReservedDiners.stream() 192 final var creations = notReservedDiners.stream()
190 .map(diner -> toMealOrderCreation( 193 .map(diner -> toMealOrderCreation(
194 + enterpriseId,
191 diner, 195 diner,
192 clientMap, 196 clientMap,
193 gradeClassMap, 197 gradeClassMap,
...@@ -217,29 +221,32 @@ public class AutoCreateMealOrderTask { ...@@ -217,29 +221,32 @@ public class AutoCreateMealOrderTask {
217 } 221 }
218 } 222 }
219 223
220 - private MealOrderCreation toMealOrderCreation(SingleDinerRpcResponse diner, 224 + private MealOrderCreation toMealOrderCreation(int enterpriseId,
221 - Map<Integer, SingleClientRpcResponse> clientMap, 225 + SingleDinerRpcResponse diner,
222 - Map<Integer, SingleGradeClassRpcResponse> classMap, 226 + Map<Integer, SingleClientRpcResponse> clientIdToClient,
223 - Map<Integer, SingleMealMenuRecordRpcResponse> menuMap, 227 + Map<Integer, SingleGradeClassRpcResponse> classIdToClass,
224 - Map<Integer, SingleMpAccountDinerRefRpcResponse> mpAccountDinerRefMap, 228 + Map<Integer, SingleMealMenuRecordRpcResponse> clientIdToLatestMenu,
229 + Map<Integer, SingleMpAccountDinerRefRpcResponse> dinerIdToAccountRef,
225 ZonedDateTime startOfNextWeek, 230 ZonedDateTime startOfNextWeek,
226 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> schoolSuspensionMap, 231 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> schoolSuspensionMap,
227 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> studentSuspensionMap, 232 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> studentSuspensionMap,
228 - Map<String, Integer> holidayMap) { 233 + Map<String, Integer> dateStrToHolidayStatus) {
229 - final var menu = menuMap.get(diner.getClientId()); 234 + final SingleMealMenuRecordRpcResponse menu = clientIdToLatestMenu.get(diner.getClientId());
230 - final var client = clientMap.get(diner.getClientId()); 235 + final SingleClientRpcResponse client = clientIdToClient.get(diner.getClientId());
231 - final var gradeClass = classMap.get(diner.getClassId()); 236 + final SingleGradeClassRpcResponse gradeClass = classIdToClass.get(diner.getClassId());
232 - final var mpAccountDinerRef = mpAccountDinerRefMap.get(diner.getId()); 237 + final SingleMpAccountDinerRefRpcResponse mpAccountDinerRef = dinerIdToAccountRef.get(diner.getId());
233 if (menu == null || client == null || gradeClass == null) return null; 238 if (menu == null || client == null || gradeClass == null) return null;
234 239
235 - // 获取停餐信息,优先使用学校级别的 240 + // 计算并集停餐窗口(学校/班级/学生维度的最新记录取并集)
236 - SingleDinerMealSuspensionRecordRpcResponse suspensionRecord = schoolSuspensionMap.get(diner.getClientId()); 241 + final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
237 - if (suspensionRecord == null) { 242 + enterpriseId,
238 - suspensionRecord = studentSuspensionMap.get(diner.getId()); 243 + diner.getClientId(),
239 - } 244 + diner.getClassId(),
245 + diner.getId()
246 + );
240 247
241 - // 构建下周的餐食安排 248 + // 构建下周的餐食安排(基于并集时间窗)
242 - List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, suspensionRecord, holidayMap); 249 + List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, unionWindow, dateStrToHolidayStatus);
243 250
244 if (meals.isEmpty()) { 251 if (meals.isEmpty()) {
245 return null; 252 return null;
...@@ -261,41 +268,36 @@ public class AutoCreateMealOrderTask { ...@@ -261,41 +268,36 @@ public class AutoCreateMealOrderTask {
261 .build(); 268 .build();
262 } 269 }
263 270
264 - private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart, 271 + // 已由并集窗口版本替代
265 - SingleDinerMealSuspensionRecordRpcResponse suspensionRecord, 272 +
266 - Map<String, Integer> holidayMap) { 273 + private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
274 + SuspensionService.SuspensionWindow unionWindow,
275 + Map<String, Integer> holidayMap) {
267 return IntStream.range(0, 7) 276 return IntStream.range(0, 7)
268 .mapToObj(i -> { 277 .mapToObj(i -> {
269 final var date = nextWeekStart.plusDays(i); 278 final var date = nextWeekStart.plusDays(i);
270 final var dateStr = date.format(DateUtil.YYYY_MM_DD); 279 final var dateStr = date.format(DateUtil.YYYY_MM_DD);
271 - 280 +
272 // 检查是否为节假日 281 // 检查是否为节假日
273 final var status = holidayMap.get(dateStr); 282 final var status = holidayMap.get(dateStr);
274 if (status != null && status != 0 && status != 2) { 283 if (status != null && status != 0 && status != 2) {
275 - // 如果不是工作日,返回null
276 return null; 284 return null;
277 } 285 }
278 - 286 +
279 - // 如果有停餐信息,检查当前循环的日期是否在停餐时间范围内 287 + if (unionWindow != null) {
280 - if (suspensionRecord != null) {
281 final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli(); 288 final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli();
282 final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli(); 289 final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli();
283 - final var suspensionStart = suspensionRecord.getStartAt(); 290 + if (!(dateEnd < unionWindow.startAt || dateStart > unionWindow.endAt)) {
284 - final var suspensionEnd = suspensionRecord.getEndAt();
285 -
286 - // 如果停餐时间范围与当前日期有重叠,返回null
287 - if (!(dateEnd < suspensionStart || dateStart > suspensionEnd)) {
288 return null; 291 return null;
289 } 292 }
290 } 293 }
291 - 294 +
292 - // 如果是工作日且不在停餐时间内,返回正常菜单
293 return MenuSchedule.newBuilder() 295 return MenuSchedule.newBuilder()
294 .setDate(date.format(DateUtil.YYYY_MM_DD_LEFT_SYMBOL)) 296 .setDate(date.format(DateUtil.YYYY_MM_DD_LEFT_SYMBOL))
295 .setMenu("A") 297 .setMenu("A")
296 .build(); 298 .build();
297 }) 299 })
298 - .filter(Objects::nonNull) // 过滤掉null值 300 + .filter(Objects::nonNull)
299 .toList(); 301 .toList();
300 } 302 }
301 303
......
1 +package com.infoloop.tianting.service;
2 +
3 +import com.infoloop.tianting.mealorderservice.SingleDinerMealSuspensionRecordRpcResponse;
4 +import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse;
5 +
6 +import java.util.List;
7 +import java.util.Map;
8 +
9 +/**
10 + * 公共停餐服务:为单个就餐人提供学校/班级/学生三个维度的最新停餐记录集合
11 + */
12 +public interface SuspensionService {
13 +
14 + /**
15 + * 简单时间窗对象
16 + */
17 + class SuspensionWindow {
18 + public final long startAt;
19 + public final long endAt;
20 +
21 + public SuspensionWindow(long startAt, long endAt) {
22 + this.startAt = startAt;
23 + this.endAt = endAt;
24 + }
25 + }
26 +
27 + /**
28 + * 获取就餐人的停餐记录(学校/班级/学生三个维度各取最新一条),用于做并集判断
29 + *
30 + * @param enterpriseId 企业ID
31 + * @param clientId 学校ID
32 + * @param classId 班级ID
33 + * @param dinerId 就餐人ID
34 + * @return 最多包含3条记录(学校级一条、班级级一条、学生级一条),按存在性返回非空集合
35 + */
36 + List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId);
37 +
38 + /**
39 + * 获取就餐人的停餐并集时间窗(取上述三维度最新记录的并集:min(startAt), max(endAt))。
40 + * 若不存在任何停餐记录则返回 null。
41 + */
42 + SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId);
43 +
44 + /**
45 + * 为多个订餐人批量计算停餐并集时间窗。避免逐个调用,内部一次性查询并用 Map 聚合。
46 + * 返回的 Map key 为 dinerId,仅为存在停餐窗口的订餐人返回条目。
47 + */
48 + Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners);
49 +}
50 +
51 +
...@@ -15,6 +15,7 @@ import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient; ...@@ -15,6 +15,7 @@ import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
15 import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient; 15 import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
16 import com.infoloop.tianting.service.client.MealOrderServiceRpcClient; 16 import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
17 import com.infoloop.tianting.utils.DateUtil; 17 import com.infoloop.tianting.utils.DateUtil;
18 +import com.infoloop.tianting.service.SuspensionService;
18 import lombok.RequiredArgsConstructor; 19 import lombok.RequiredArgsConstructor;
19 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
20 import org.springframework.beans.factory.annotation.Autowired; 21 import org.springframework.beans.factory.annotation.Autowired;
...@@ -31,6 +32,7 @@ import java.util.Date; ...@@ -31,6 +32,7 @@ import java.util.Date;
31 import java.util.List; 32 import java.util.List;
32 import java.util.Map; 33 import java.util.Map;
33 import java.util.Objects; 34 import java.util.Objects;
35 +import java.util.function.Function;
34 import java.util.stream.Collectors; 36 import java.util.stream.Collectors;
35 37
36 @Slf4j 38 @Slf4j
...@@ -44,6 +46,8 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -44,6 +46,8 @@ public class MealOrderServiceImpl implements MealOrderService {
44 46
45 private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient; 47 private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;
46 48
49 + private final SuspensionService suspensionService;
50 +
47 @Override 51 @Override
48 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) { 52 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
49 final var loginInfo = LoginContextHolder.getLoginInfo(); 53 final var loginInfo = LoginContextHolder.getLoginInfo();
...@@ -105,78 +109,16 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -105,78 +109,16 @@ public class MealOrderServiceImpl implements MealOrderService {
105 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND); 109 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
106 } 110 }
107 111
108 - // 5. 校验停餐记录 112 + // 5. 校验停餐记录(学校/班级/学生三个维度最新记录并集)
109 - var currentTime = System.currentTimeMillis(); 113 + final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
110 - // 5.1 先查询学校级别的停餐信息
111 - var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
112 - loginInfo.getEnterpriseId(),
113 - List.of(dinerById.getClientId()),
114 - null,
115 - null
116 - );
117 -
118 - // 直接过滤出学校级别的停餐记录
119 - if (schoolSuspensionRecords != null) {
120 - schoolSuspensionRecords = schoolSuspensionRecords.stream()
121 - .filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
122 - .toList();
123 - }
124 -
125 - if (schoolSuspensionRecords != null && !schoolSuspensionRecords.isEmpty()) {
126 - var latestSchoolSuspension = schoolSuspensionRecords.stream()
127 - .max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
128 - .orElse(null);
129 -
130 - if (latestSchoolSuspension != null &&
131 - currentTime >= latestSchoolSuspension.getStartAt() &&
132 - currentTime <= latestSchoolSuspension.getEndAt()) {
133 - throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
134 - }
135 - }
136 -
137 - // 5.2 再查询班级级别的停餐信息
138 - var classSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
139 loginInfo.getEnterpriseId(), 114 loginInfo.getEnterpriseId(),
140 - List.of(dinerById.getClientId()), 115 + dinerById.getClientId(),
141 - List.of(dinerById.getClassId()), 116 + dinerById.getClassId(),
142 - null 117 + dinerById.getId()
143 ); 118 );
144 - 119 + if (unionWindow != null) {
145 - // 直接过滤出班级级别的停餐记录 120 + final long nowMs = System.currentTimeMillis();
146 - if (classSuspensionRecords != null) { 121 + if (nowMs >= unionWindow.startAt && nowMs <= unionWindow.endAt) {
147 - classSuspensionRecords = classSuspensionRecords.stream()
148 - .filter(r -> r.getDinerId() == 0)
149 - .toList();
150 - }
151 -
152 - if (classSuspensionRecords != null && !classSuspensionRecords.isEmpty()) {
153 - var latestClassSuspension = classSuspensionRecords.stream()
154 - .max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
155 - .orElse(null);
156 -
157 - if (latestClassSuspension != null &&
158 - currentTime >= latestClassSuspension.getStartAt() &&
159 - currentTime <= latestClassSuspension.getEndAt()) {
160 - throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
161 - }
162 - }
163 -
164 - // 5.3 再查询学生级别的停餐信息
165 - var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
166 - loginInfo.getEnterpriseId(),
167 - List.of(dinerById.getClientId()),
168 - List.of(dinerById.getClassId()),
169 - List.of(dinerById.getId())
170 - );
171 -
172 - if (studentSuspensionRecords != null && !studentSuspensionRecords.isEmpty()) {
173 - var latestStudentSuspension = studentSuspensionRecords.stream()
174 - .max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
175 - .orElse(null);
176 -
177 - if (latestStudentSuspension != null &&
178 - currentTime >= latestStudentSuspension.getStartAt() &&
179 - currentTime <= latestStudentSuspension.getEndAt()) {
180 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED); 122 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
181 } 123 }
182 } 124 }
...@@ -304,113 +246,37 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -304,113 +246,37 @@ public class MealOrderServiceImpl implements MealOrderService {
304 246
305 // 3. 获取所有订餐人信息 247 // 3. 获取所有订餐人信息
306 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds); 248 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);
307 - var dinerById = diners.stream().collect(Collectors.toMap(SingleDinerRpcResponse::getId, diner -> diner));
308 249
309 - // 4. 获取所有不重复的clientId 250 + // 4. 使用公共停餐服务:批量计算三维度(学校/班级/学生)“最新记录并集”的停餐窗口,避免逐个调用
310 - var uniqueClientIds = diners.stream() 251 + Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
311 - .map(diner -> Long.valueOf(diner.getClientId())) 252 + .collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
312 - .distinct()
313 - .toList();
314 -
315 - // 5. 批量获取所有学校的停餐信息
316 - final var now = Instant.now();
317 - var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
318 - loginInfo.getEnterpriseId(),
319 - null,
320 - null,
321 - null
322 - ).stream().filter(r -> r.getClassId() == 0).filter(r -> r.getEndAt() > now.toEpochMilli()).toList();
323 -
324 - // 6. 批量获取所有学生的停餐信息
325 - var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
326 - loginInfo.getEnterpriseId(),
327 - null,
328 - null,
329 - dinerIds
330 - ).stream().filter(r -> r.getEndAt() > now.toEpochMilli()).toList();
331 253
332 - // 7. 构建学校停餐信息Map 254 + Map<Integer, SuspensionService.SuspensionWindow> dinerIdToUnionWindow =
333 - var schoolSuspensionMap = schoolSuspensionRecords.stream() 255 + suspensionService.getSuspensionUnionWindowsForDiners(loginInfo.getEnterpriseId(), diners);
334 - .collect(Collectors.groupingBy(
335 - SingleDinerMealSuspensionRecordRpcResponse::getClientId,
336 - Collectors.collectingAndThen(
337 - Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
338 - opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
339 - )
340 - ));
341 256
342 - // 8. 构建学生停餐信息Map 257 + List<DinerMealSuspensionRecordVO> suspensionList = dinerIdToUnionWindow.entrySet().stream()
343 - var studentSuspensionMap = studentSuspensionRecords.stream() 258 + .map(entry -> {
344 - .collect(Collectors.groupingBy( 259 + Integer dinerId = entry.getKey();
345 - SingleDinerMealSuspensionRecordRpcResponse::getDinerId, 260 + SuspensionService.SuspensionWindow unionWindow = entry.getValue();
346 - Collectors.collectingAndThen( 261 + SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
347 - Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
348 - opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
349 - )
350 - ));
351 -
352 - // 9. 获取每个订餐人的停餐信息
353 - return diners.stream()
354 - .map(diner -> {
355 - // 获取学校级别的停餐信息
356 - var schoolSuspension = schoolSuspensionMap.get(Long.valueOf(diner.getClientId()).intValue());
357 - // 获取学生级别的停餐信息
358 - var studentSuspension = studentSuspensionMap.get(Long.valueOf(diner.getId()).intValue());
359 -
360 - // 如果两个都没有停餐信息,返回null
361 - if (schoolSuspension == null && studentSuspension == null) {
362 - return null;
363 - }
364 -
365 - // 如果只有学校级别的停餐信息
366 - if (schoolSuspension != null && studentSuspension == null) {
367 - return schoolSuspension;
368 - }
369 -
370 - // 如果只有学生级别的停餐信息
371 - if (schoolSuspension == null && studentSuspension != null) {
372 - return studentSuspension;
373 - }
374 -
375 - // 如果两个都有停餐信息,合并时间范围
376 return DinerMealSuspensionRecordVO.builder() 262 return DinerMealSuspensionRecordVO.builder()
377 - .id(schoolSuspension.getId()) 263 + .enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId()))
378 - .enterpriseId(schoolSuspension.getEnterpriseId()) 264 + .clientId(Long.valueOf(dinerInfo.getClientId()))
379 - .clientId(schoolSuspension.getClientId()) 265 + .classId(Long.valueOf(dinerInfo.getClassId()))
380 - .classId(studentSuspension.getClassId()) 266 + .dinerId(Long.valueOf(dinerInfo.getId()))
381 - .dinerId(studentSuspension.getDinerId()) 267 + .dinerName(dinerInfo.getName())
382 - .dinerName(studentSuspension.getDinerName()) 268 + .studentNo(dinerInfo.getStudentNo())
383 - .studentNo(studentSuspension.getStudentNo()) 269 + .startAt(new Date(unionWindow.startAt))
384 - .startAt(new Date(Math.min(schoolSuspension.getStartAt().getTime(), studentSuspension.getStartAt().getTime()))) 270 + .endAt(new Date(unionWindow.endAt))
385 - .endAt(new Date(Math.max(schoolSuspension.getEndAt().getTime(), studentSuspension.getEndAt().getTime())))
386 .comment("") 271 .comment("")
387 .build(); 272 .build();
388 }) 273 })
389 - .filter(Objects::nonNull)
390 - .distinct()
391 .toList(); 274 .toList();
275 + return suspensionList;
392 } 276 }
393 277
394 /** 278 /**
395 * 将RPC响应转换为VO对象 279 * 将RPC响应转换为VO对象
396 */ 280 */
397 - private DinerMealSuspensionRecordVO convertToVO(SingleDinerMealSuspensionRecordRpcResponse response, 281 + // 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换
398 - Map<Integer, SingleDinerRpcResponse> dinerById) {
399 - if (response == null) {
400 - return null;
401 - }
402 -
403 - return DinerMealSuspensionRecordVO.builder()
404 - .id(Long.valueOf(response.getId()))
405 - .enterpriseId(Long.valueOf(response.getEnterpriseId()))
406 - .clientId(Long.valueOf(response.getClientId()))
407 - .classId(Long.valueOf(response.getClassId()))
408 - .dinerId(Long.valueOf(response.getDinerId()))
409 - .dinerName(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getName() : null)
410 - .studentNo(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getStudentNo() : null)
411 - .startAt(new Date(response.getStartAt()))
412 - .endAt(new Date(response.getEndAt()))
413 - .comment(response.getComment())
414 - .build();
415 - }
416 } 282 }
......
1 +package com.infoloop.tianting.service.impl;
2 +
3 +import com.infoloop.tianting.mealorderservice.SingleDinerMealSuspensionRecordRpcResponse;
4 +import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse;
5 +import com.infoloop.tianting.service.SuspensionService;
6 +import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
7 +import lombok.RequiredArgsConstructor;
8 +import org.springframework.lang.Nullable;
9 +import org.springframework.stereotype.Service;
10 +
11 +import java.util.ArrayList;
12 +import java.util.Comparator;
13 +import java.util.List;
14 +import java.util.Objects;
15 +import java.util.Map;
16 +import java.util.HashMap;
17 +import java.util.function.Function;
18 +import java.util.stream.Collectors;
19 +
20 +@Service
21 +@RequiredArgsConstructor
22 +public class SuspensionServiceImpl implements SuspensionService {
23 +
24 + private final MealOrderServiceRpcClient mealOrderServiceRpcClient;
25 +
26 + @Override
27 + public List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
28 + // 汇总三种维度(学校/班级/学生)各自“最新一条”停餐记录
29 + List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensionRecords = new ArrayList<>(3);
30 +
31 + // 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤
32 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
33 + enterpriseId,
34 + List.of(clientId),
35 + null,
36 + List.of(0)
37 + );
38 + SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest(
39 + schoolLevelRecords,
40 + record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0
41 + );
42 + if (latestSchoolLevelRecord != null) {
43 + latestSuspensionRecords.add(latestSchoolLevelRecord);
44 + }
45 +
46 + // 班级维度:dinerId == 0,按 clientId + classId 过滤
47 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
48 + enterpriseId,
49 + List.of(clientId),
50 + List.of(classId),
51 + List.of(0)
52 + );
53 + SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
54 + classLevelRecords,
55 + record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
56 + );
57 + if (latestClassLevelRecord != null) {
58 + latestSuspensionRecords.add(latestClassLevelRecord);
59 + }
60 +
61 + // 学生维度:三个 id 都不为 0
62 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
63 + enterpriseId,
64 + List.of(clientId),
65 + List.of(classId),
66 + List.of(dinerId)
67 + );
68 + SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest(
69 + dinerLevelRecords,
70 + record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId
71 + );
72 + if (latestDinerLevelRecord != null) {
73 + latestSuspensionRecords.add(latestDinerLevelRecord);
74 + }
75 +
76 + return latestSuspensionRecords;
77 + }
78 +
79 + @Override
80 + public SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
81 + // 取最新记录集合并生成并集窗口:\[min(startAt), max(endAt)\]
82 + List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensions = getLatestSuspensionsForDiner(enterpriseId, clientId, classId, dinerId);
83 + if (latestSuspensions == null || latestSuspensions.isEmpty()) {
84 + return null;
85 + }
86 + long minimalStartEpochMillis = Long.MAX_VALUE;
87 + long maximalEndEpochMillis = Long.MIN_VALUE;
88 + for (SingleDinerMealSuspensionRecordRpcResponse record : latestSuspensions) {
89 + if (record.getStartAt() < minimalStartEpochMillis) {
90 + minimalStartEpochMillis = record.getStartAt();
91 + }
92 + if (record.getEndAt() > maximalEndEpochMillis) {
93 + maximalEndEpochMillis = record.getEndAt();
94 + }
95 + }
96 + if (minimalStartEpochMillis == Long.MAX_VALUE || maximalEndEpochMillis == Long.MIN_VALUE) {
97 + return null;
98 + }
99 + return new SuspensionWindow(minimalStartEpochMillis, maximalEndEpochMillis);
100 + }
101 +
102 + @Override
103 + public Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
104 + // 基于输入 diners 一次性拉取三类停餐记录,再按 diner 维度聚合出最新记录并计算并集窗口
105 + Map<Integer, SuspensionWindow> dinerIdToWindow = new HashMap<>();
106 + if (diners == null || diners.isEmpty()) {
107 + return dinerIdToWindow;
108 + }
109 +
110 + // 预构建索引集合
111 + List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
112 + List<Integer> classIds = diners.stream().map(SingleDinerRpcResponse::getClassId).distinct().toList();
113 + List<Integer> dinerIds = diners.stream().map(SingleDinerRpcResponse::getId).toList();
114 +
115 + // 查询三类记录
116 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
117 + enterpriseId, clientIds, null, List.of(0));
118 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
119 + enterpriseId, clientIds, classIds, List.of(0));
120 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
121 + enterpriseId, clientIds, classIds, dinerIds);
122 +
123 + // 学校维度:clientId -> latest
124 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
125 + schoolLevelRecords.stream()
126 + .filter(Objects::nonNull)
127 + .filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
128 + .collect(Collectors.toMap(
129 + SingleDinerMealSuspensionRecordRpcResponse::getClientId,
130 + Function.identity(),
131 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
132 + ));
133 +
134 + // 班级维度:clientId+classId -> latest(使用合成 key)
135 + Map<String, SingleDinerMealSuspensionRecordRpcResponse> clientClassToLatest = classLevelRecords == null ? Map.of() :
136 + classLevelRecords.stream()
137 + .filter(Objects::nonNull)
138 + .filter(r -> r.getDinerId() == 0)
139 + .collect(Collectors.toMap(
140 + r -> r.getClientId() + "#" + r.getClassId(),
141 + Function.identity(),
142 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
143 + ));
144 +
145 + // 学生维度:dinerId -> latest
146 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> dinerIdToLatest = dinerLevelRecords == null ? Map.of() :
147 + dinerLevelRecords.stream()
148 + .filter(Objects::nonNull)
149 + .collect(Collectors.toMap(
150 + SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
151 + Function.identity(),
152 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
153 + ));
154 +
155 + // 计算每个 diner 的并集窗口
156 + for (SingleDinerRpcResponse diner : diners) {
157 + SingleDinerMealSuspensionRecordRpcResponse latestSchool = clientIdToLatestSchool.get(diner.getClientId());
158 + SingleDinerMealSuspensionRecordRpcResponse latestClass = clientClassToLatest.get(diner.getClientId() + "#" + diner.getClassId());
159 + SingleDinerMealSuspensionRecordRpcResponse latestDiner = dinerIdToLatest.get(diner.getId());
160 +
161 + long minStart = Long.MAX_VALUE;
162 + long maxEnd = Long.MIN_VALUE;
163 +
164 + if (latestSchool != null) {
165 + if (latestSchool.getStartAt() < minStart) {
166 + minStart = latestSchool.getStartAt();
167 + }
168 + if (latestSchool.getEndAt() > maxEnd) {
169 + maxEnd = latestSchool.getEndAt();
170 + }
171 + }
172 + if (latestClass != null) {
173 + if (latestClass.getStartAt() < minStart) {
174 + minStart = latestClass.getStartAt();
175 + }
176 + if (latestClass.getEndAt() > maxEnd) {
177 + maxEnd = latestClass.getEndAt();
178 + }
179 + }
180 + if (latestDiner != null) {
181 + if (latestDiner.getStartAt() < minStart) {
182 + minStart = latestDiner.getStartAt();
183 + }
184 + if (latestDiner.getEndAt() > maxEnd) {
185 + maxEnd = latestDiner.getEndAt();
186 + }
187 + }
188 +
189 + if (minStart != Long.MAX_VALUE && maxEnd != Long.MIN_VALUE) {
190 + dinerIdToWindow.put(diner.getId(), new SuspensionWindow(minStart, maxEnd));
191 + }
192 + }
193 +
194 + return dinerIdToWindow;
195 + }
196 + @Nullable
197 + private SingleDinerMealSuspensionRecordRpcResponse pickLatest(List<SingleDinerMealSuspensionRecordRpcResponse> records,
198 + java.util.function.Predicate<SingleDinerMealSuspensionRecordRpcResponse> predicate) {
199 + // 在满足谓词条件的集合中,按 createdAt 取最新的一条
200 + if (records == null || records.isEmpty()) {
201 + return null;
202 + }
203 + return records.stream()
204 + .filter(Objects::nonNull)
205 + .filter(predicate)
206 + .max(Comparator.comparingLong(SingleDinerMealSuspensionRecordRpcResponse::getCreatedAt))
207 + .orElse(null);
208 + }
209 +}
210 +
211 +