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,8 +268,10 @@ public class AutoCreateMealOrderTask { ...@@ -261,8 +268,10 @@ public class AutoCreateMealOrderTask {
261 .build(); 268 .build();
262 } 269 }
263 270
271 + // 已由并集窗口版本替代
272 +
264 private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart, 273 private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
265 - SingleDinerMealSuspensionRecordRpcResponse suspensionRecord, 274 + SuspensionService.SuspensionWindow unionWindow,
266 Map<String, Integer> holidayMap) { 275 Map<String, Integer> holidayMap) {
267 return IntStream.range(0, 7) 276 return IntStream.range(0, 7)
268 .mapToObj(i -> { 277 .mapToObj(i -> {
...@@ -272,30 +281,23 @@ public class AutoCreateMealOrderTask { ...@@ -272,30 +281,23 @@ public class AutoCreateMealOrderTask {
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 +