Jiaqi Xia

Merge branch 'dev/nongxin-adjust' into 'master'

Dev/nongxin adjust



See merge request !8
...@@ -20,6 +20,12 @@ application { ...@@ -20,6 +20,12 @@ application {
20 '-Dlogging.path=logs'] 20 '-Dlogging.path=logs']
21 } 21 }
22 22
23 +java {
24 + toolchain {
25 + languageVersion = JavaLanguageVersion.of(17)
26 + }
27 +}
28 +
23 ext { 29 ext {
24 grpcVersion = '1.62.2' 30 grpcVersion = '1.62.2'
25 protobufVersion = '3.25.3' 31 protobufVersion = '3.25.3'
......
...@@ -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 intervals = suspensionService.getSuspensionIntervalsForDiner(
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, intervals, 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 + java.util.List<SuspensionService.SuspensionInterval> intervals,
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 (intervals != null && !intervals.isEmpty()) {
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 + final boolean overlaps = intervals.stream()
284 - final var suspensionEnd = suspensionRecord.getEndAt(); 291 + .anyMatch(interval -> !(dateEnd < interval.startAt || dateStart > interval.endAt));
285 - 292 + if (overlaps) return null;
286 - // 如果停餐时间范围与当前日期有重叠,返回null
287 - if (!(dateEnd < suspensionStart || dateStart > suspensionEnd)) {
288 - return null;
289 - }
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
......
...@@ -6,6 +6,7 @@ import lombok.Builder; ...@@ -6,6 +6,7 @@ import lombok.Builder;
6 import lombok.Data; 6 import lombok.Data;
7 7
8 import java.util.Date; 8 import java.util.Date;
9 +import java.util.List;
9 10
10 @Data 11 @Data
11 @Builder 12 @Builder
...@@ -38,12 +39,20 @@ public class DinerMealSuspensionRecordVO { ...@@ -38,12 +39,20 @@ public class DinerMealSuspensionRecordVO {
38 @ApiModelProperty("学号") 39 @ApiModelProperty("学号")
39 private String studentNo; 40 private String studentNo;
40 41
41 - @ApiModelProperty("开始时间") 42 + @ApiModelProperty("停餐时间区间列表")
42 - private Date startAt; 43 + private List<Interval> intervals;
43 44
44 - @ApiModelProperty("结束时间") 45 + @Data
45 - private Date endAt; 46 + @Builder
47 + @ApiModel("停餐时间区间")
48 + public static class Interval {
49 + @ApiModelProperty("开始时间")
50 + private Date startAt;
46 51
47 - @ApiModelProperty("备注") 52 + @ApiModelProperty("结束时间")
48 - private String comment; 53 + private Date endAt;
54 +
55 + @ApiModelProperty("备注")
56 + private String comment;
57 + }
49 } 58 }
...\ No newline at end of file ...\ No newline at end of file
......
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 + * 多段时间窗对象(标准闭区间 \[startAt, endAt\]),用于表示并集后可能出现的多段停餐区间
29 + */
30 + class SuspensionInterval {
31 + public final long startAt;
32 + public final long endAt;
33 +
34 + public SuspensionInterval(long startAt, long endAt) {
35 + this.startAt = startAt;
36 + this.endAt = endAt;
37 + }
38 + }
39 +
40 + /**
41 + * 获取就餐人的停餐记录(学校/班级/学生三个维度各取最新一条),用于做并集判断
42 + *
43 + * @param enterpriseId 企业ID
44 + * @param clientId 学校ID
45 + * @param classId 班级ID
46 + * @param dinerId 就餐人ID
47 + * @return 最多包含3条记录(学校级一条、班级级一条、学生级一条),按存在性返回非空集合
48 + */
49 + List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId);
50 +
51 + /**
52 + * 获取就餐人的停餐并集时间窗(取上述三维度最新记录的并集:min(startAt), max(endAt))。
53 + * 若不存在任何停餐记录则返回 null。
54 + */
55 + SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId);
56 +
57 + /**
58 + * 为多个订餐人批量计算停餐并集时间窗。避免逐个调用,内部一次性查询并用 Map 聚合。
59 + * 返回的 Map key 为 dinerId,仅为存在停餐窗口的订餐人返回条目。
60 + */
61 + Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners);
62 +
63 + /**
64 + * 获取单个订餐人的停餐多段时间区间(对学校/班级/学生三个维度的最新记录做标准并集,保留间隙)
65 + */
66 + List<SuspensionInterval> getSuspensionIntervalsForDiner(int enterpriseId, int clientId, int classId, int dinerId);
67 +
68 + /**
69 + * 批量获取多个订餐人的停餐多段时间区间。返回 Map<dinerId, List<Interval>>。
70 + */
71 + Map<Integer, List<SuspensionInterval>> getSuspensionIntervalsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners);
72 +}
73 +
74 +
...@@ -15,13 +15,13 @@ import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient; ...@@ -15,13 +15,13 @@ 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;
21 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
22 23
23 import java.time.DayOfWeek; 24 import java.time.DayOfWeek;
24 -import java.time.Instant;
25 import java.time.LocalDate; 25 import java.time.LocalDate;
26 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
27 import java.time.LocalTime; 27 import java.time.LocalTime;
...@@ -30,7 +30,7 @@ import java.util.Collections; ...@@ -30,7 +30,7 @@ import java.util.Collections;
30 import java.util.Date; 30 import java.util.Date;
31 import java.util.List; 31 import java.util.List;
32 import java.util.Map; 32 import java.util.Map;
33 -import java.util.Objects; 33 +import java.util.function.Function;
34 import java.util.stream.Collectors; 34 import java.util.stream.Collectors;
35 35
36 @Slf4j 36 @Slf4j
...@@ -44,6 +44,8 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -44,6 +44,8 @@ public class MealOrderServiceImpl implements MealOrderService {
44 44
45 private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient; 45 private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;
46 46
47 + private final SuspensionService suspensionService;
48 +
47 @Override 49 @Override
48 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) { 50 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
49 final var loginInfo = LoginContextHolder.getLoginInfo(); 51 final var loginInfo = LoginContextHolder.getLoginInfo();
...@@ -105,79 +107,22 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -105,79 +107,22 @@ public class MealOrderServiceImpl implements MealOrderService {
105 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND); 107 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
106 } 108 }
107 109
108 - // 5. 校验停餐记录 110 + // 5. 校验停餐记录(学校/班级/学生三个维度最新记录的多段并集区间)
109 - var currentTime = System.currentTimeMillis(); 111 + final var intervals = suspensionService.getSuspensionIntervalsForDiner(
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(), 112 loginInfo.getEnterpriseId(),
140 - List.of(dinerById.getClientId()), 113 + dinerById.getClientId(),
141 - List.of(dinerById.getClassId()), 114 + dinerById.getClassId(),
142 - null 115 + dinerById.getId()
143 ); 116 );
144 - 117 + if (intervals != null && !intervals.isEmpty()) {
145 - // 直接过滤出班级级别的停餐记录 118 + // 校验每个订餐日期是否在停餐时间段内
146 - if (classSuspensionRecords != null) { 119 + for (var meal : createMealOrderDTO.getMeals()) {
147 - classSuspensionRecords = classSuspensionRecords.stream() 120 + final long mealDateMs = DateUtil.parseDate(meal.getDate()).getTime();
148 - .filter(r -> r.getDinerId() == 0) 121 + final boolean inSuspension = intervals.stream()
149 - .toList(); 122 + .anyMatch(i -> mealDateMs >= i.startAt && mealDateMs <= i.endAt);
150 - } 123 + if (inSuspension) {
151 - 124 + throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
152 - if (classSuspensionRecords != null && !classSuspensionRecords.isEmpty()) { 125 + }
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);
181 } 126 }
182 } 127 }
183 128
...@@ -304,113 +249,48 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -304,113 +249,48 @@ public class MealOrderServiceImpl implements MealOrderService {
304 249
305 // 3. 获取所有订餐人信息 250 // 3. 获取所有订餐人信息
306 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds); 251 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);
307 - var dinerById = diners.stream().collect(Collectors.toMap(SingleDinerRpcResponse::getId, diner -> diner));
308 -
309 - // 4. 获取所有不重复的clientId
310 - var uniqueClientIds = diners.stream()
311 - .map(diner -> Long.valueOf(diner.getClientId()))
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 -
332 - // 7. 构建学校停餐信息Map
333 - var schoolSuspensionMap = schoolSuspensionRecords.stream()
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 -
342 - // 8. 构建学生停餐信息Map
343 - var studentSuspensionMap = studentSuspensionRecords.stream()
344 - .collect(Collectors.groupingBy(
345 - SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
346 - Collectors.collectingAndThen(
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 252
365 - // 如果只有学校级别的停餐信息 253 + // 4. 使用公共停餐服务:批量计算每个订餐人的多段停餐区间
366 - if (schoolSuspension != null && studentSuspension == null) { 254 + Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
367 - return schoolSuspension; 255 + .collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
368 - }
369 256
370 - // 如果只有学生级别的停餐信息 257 + Map<Integer, List<SuspensionService.SuspensionInterval>> dinerIdToIntervals =
371 - if (schoolSuspension == null && studentSuspension != null) { 258 + suspensionService.getSuspensionIntervalsForDiners(loginInfo.getEnterpriseId(), diners);
372 - return studentSuspension;
373 - }
374 259
375 - // 如果两个都有停餐信息,合并时间范围 260 + List<DinerMealSuspensionRecordVO> result = new ArrayList<>();
376 - return DinerMealSuspensionRecordVO.builder() 261 + for (Map.Entry<Integer, List<SuspensionService.SuspensionInterval>> entry : dinerIdToIntervals.entrySet()) {
377 - .id(schoolSuspension.getId()) 262 + Integer dinerId = entry.getKey();
378 - .enterpriseId(schoolSuspension.getEnterpriseId()) 263 + SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
379 - .clientId(schoolSuspension.getClientId()) 264 + if (dinerInfo == null) {
380 - .classId(studentSuspension.getClassId()) 265 + continue;
381 - .dinerId(studentSuspension.getDinerId()) 266 + }
382 - .dinerName(studentSuspension.getDinerName()) 267 + List<SuspensionService.SuspensionInterval> intervals = entry.getValue();
383 - .studentNo(studentSuspension.getStudentNo()) 268 + if (intervals == null || intervals.isEmpty()) {
384 - .startAt(new Date(Math.min(schoolSuspension.getStartAt().getTime(), studentSuspension.getStartAt().getTime()))) 269 + continue;
385 - .endAt(new Date(Math.max(schoolSuspension.getEndAt().getTime(), studentSuspension.getEndAt().getTime()))) 270 + }
271 + List<DinerMealSuspensionRecordVO.Interval> voIntervals = intervals.stream()
272 + .map(i -> DinerMealSuspensionRecordVO.Interval.builder()
273 + .startAt(new Date(i.startAt))
274 + .endAt(new Date(i.endAt))
386 .comment("") 275 .comment("")
387 - .build(); 276 + .build())
388 - }) 277 + .toList();
389 - .filter(Objects::nonNull) 278 +
390 - .distinct() 279 + result.add(DinerMealSuspensionRecordVO.builder()
391 - .toList(); 280 + .enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId()))
281 + .clientId(Long.valueOf(dinerInfo.getClientId()))
282 + .classId(Long.valueOf(dinerInfo.getClassId()))
283 + .dinerId(Long.valueOf(dinerInfo.getId()))
284 + .dinerName(dinerInfo.getName())
285 + .studentNo(dinerInfo.getStudentNo())
286 + .intervals(voIntervals)
287 + .build());
288 + }
289 + return result;
392 } 290 }
393 291
394 /** 292 /**
395 * 将RPC响应转换为VO对象 293 * 将RPC响应转换为VO对象
396 */ 294 */
397 - private DinerMealSuspensionRecordVO convertToVO(SingleDinerMealSuspensionRecordRpcResponse response, 295 + // 已改为统一从 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 } 296 }
......
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 + final long now = System.currentTimeMillis();
31 +
32 + // 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤
33 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
34 + enterpriseId,
35 + List.of(clientId),
36 + null,
37 + List.of(0)
38 + );
39 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = schoolLevelRecordsRaw == null ? List.of() :
40 + schoolLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
41 + SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest(
42 + schoolLevelRecords,
43 + record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0
44 + );
45 + if (latestSchoolLevelRecord != null) {
46 + latestSuspensionRecords.add(latestSchoolLevelRecord);
47 + }
48 +
49 + // 班级维度:dinerId == 0,按 clientId + classId 过滤
50 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
51 + enterpriseId,
52 + List.of(clientId),
53 + List.of(classId),
54 + List.of(0)
55 + );
56 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = classLevelRecordsRaw == null ? List.of() :
57 + classLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
58 + SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
59 + classLevelRecords,
60 + record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
61 + );
62 + if (latestClassLevelRecord != null) {
63 + latestSuspensionRecords.add(latestClassLevelRecord);
64 + }
65 +
66 + // 学生维度:三个 id 都不为 0
67 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
68 + enterpriseId,
69 + List.of(clientId),
70 + List.of(classId),
71 + List.of(dinerId)
72 + );
73 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = dinerLevelRecordsRaw == null ? List.of() :
74 + dinerLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
75 + SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest(
76 + dinerLevelRecords,
77 + record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId
78 + );
79 + if (latestDinerLevelRecord != null) {
80 + latestSuspensionRecords.add(latestDinerLevelRecord);
81 + }
82 +
83 + return latestSuspensionRecords;
84 + }
85 +
86 + @Override
87 + public SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
88 + // 取最新记录集合并生成并集窗口:\[min(startAt), max(endAt)\]
89 + List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensions = getLatestSuspensionsForDiner(enterpriseId, clientId, classId, dinerId);
90 + if (latestSuspensions == null || latestSuspensions.isEmpty()) {
91 + return null;
92 + }
93 + long minimalStartEpochMillis = Long.MAX_VALUE;
94 + long maximalEndEpochMillis = Long.MIN_VALUE;
95 + for (SingleDinerMealSuspensionRecordRpcResponse record : latestSuspensions) {
96 + if (record.getStartAt() < minimalStartEpochMillis) {
97 + minimalStartEpochMillis = record.getStartAt();
98 + }
99 + if (record.getEndAt() > maximalEndEpochMillis) {
100 + maximalEndEpochMillis = record.getEndAt();
101 + }
102 + }
103 + if (minimalStartEpochMillis == Long.MAX_VALUE || maximalEndEpochMillis == Long.MIN_VALUE) {
104 + return null;
105 + }
106 + return new SuspensionWindow(minimalStartEpochMillis, maximalEndEpochMillis);
107 + }
108 +
109 + @Override
110 + public Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
111 + // 基于输入 diners 一次性拉取三类停餐记录,再按 diner 维度聚合出最新记录并计算并集窗口
112 + Map<Integer, SuspensionWindow> dinerIdToWindow = new HashMap<>();
113 + if (diners == null || diners.isEmpty()) {
114 + return dinerIdToWindow;
115 + }
116 + final long now = System.currentTimeMillis();
117 +
118 + // 预构建索引集合
119 + List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
120 + List<Integer> classIds = diners.stream().map(SingleDinerRpcResponse::getClassId).distinct().toList();
121 + List<Integer> dinerIds = diners.stream().map(SingleDinerRpcResponse::getId).toList();
122 +
123 + // 查询三类记录
124 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
125 + enterpriseId, clientIds, null, List.of(0));
126 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
127 + enterpriseId, clientIds, classIds, List.of(0));
128 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
129 + enterpriseId, clientIds, classIds, dinerIds);
130 +
131 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = schoolLevelRecordsRawBatch == null ? List.of() :
132 + schoolLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
133 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = classLevelRecordsRawBatch == null ? List.of() :
134 + classLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
135 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = dinerLevelRecordsRawBatch == null ? List.of() :
136 + dinerLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
137 +
138 + // 学校维度:clientId -> latest
139 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
140 + schoolLevelRecords.stream()
141 + .filter(Objects::nonNull)
142 + .filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
143 + .collect(Collectors.toMap(
144 + SingleDinerMealSuspensionRecordRpcResponse::getClientId,
145 + Function.identity(),
146 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
147 + ));
148 +
149 + // 班级维度:clientId+classId -> latest(使用合成 key)
150 + Map<String, SingleDinerMealSuspensionRecordRpcResponse> clientClassToLatest = classLevelRecords == null ? Map.of() :
151 + classLevelRecords.stream()
152 + .filter(Objects::nonNull)
153 + .filter(r -> r.getDinerId() == 0)
154 + .collect(Collectors.toMap(
155 + r -> r.getClientId() + "#" + r.getClassId(),
156 + Function.identity(),
157 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
158 + ));
159 +
160 + // 学生维度:dinerId -> latest
161 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> dinerIdToLatest = dinerLevelRecords == null ? Map.of() :
162 + dinerLevelRecords.stream()
163 + .filter(Objects::nonNull)
164 + .collect(Collectors.toMap(
165 + SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
166 + Function.identity(),
167 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
168 + ));
169 +
170 + // 计算每个 diner 的并集窗口
171 + for (SingleDinerRpcResponse diner : diners) {
172 + SingleDinerMealSuspensionRecordRpcResponse latestSchool = clientIdToLatestSchool.get(diner.getClientId());
173 + SingleDinerMealSuspensionRecordRpcResponse latestClass = clientClassToLatest.get(diner.getClientId() + "#" + diner.getClassId());
174 + SingleDinerMealSuspensionRecordRpcResponse latestDiner = dinerIdToLatest.get(diner.getId());
175 +
176 + long minStart = Long.MAX_VALUE;
177 + long maxEnd = Long.MIN_VALUE;
178 +
179 + if (latestSchool != null) {
180 + if (latestSchool.getStartAt() < minStart) {
181 + minStart = latestSchool.getStartAt();
182 + }
183 + if (latestSchool.getEndAt() > maxEnd) {
184 + maxEnd = latestSchool.getEndAt();
185 + }
186 + }
187 + if (latestClass != null) {
188 + if (latestClass.getStartAt() < minStart) {
189 + minStart = latestClass.getStartAt();
190 + }
191 + if (latestClass.getEndAt() > maxEnd) {
192 + maxEnd = latestClass.getEndAt();
193 + }
194 + }
195 + if (latestDiner != null) {
196 + if (latestDiner.getStartAt() < minStart) {
197 + minStart = latestDiner.getStartAt();
198 + }
199 + if (latestDiner.getEndAt() > maxEnd) {
200 + maxEnd = latestDiner.getEndAt();
201 + }
202 + }
203 +
204 + if (minStart != Long.MAX_VALUE && maxEnd != Long.MIN_VALUE) {
205 + dinerIdToWindow.put(diner.getId(), new SuspensionWindow(minStart, maxEnd));
206 + }
207 + }
208 +
209 + return dinerIdToWindow;
210 + }
211 +
212 + @Override
213 + public List<SuspensionInterval> getSuspensionIntervalsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
214 + Map<Integer, List<SuspensionInterval>> map = getSuspensionIntervalsForDiners(enterpriseId, List.of(
215 + SingleDinerRpcResponse.newBuilder()
216 + .setEnterpriseId(enterpriseId)
217 + .setClientId(clientId)
218 + .setClassId(classId)
219 + .setId(dinerId)
220 + .build()
221 + ));
222 + return map.getOrDefault(dinerId, List.of());
223 + }
224 +
225 + @Override
226 + public Map<Integer, List<SuspensionInterval>> getSuspensionIntervalsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
227 + Map<Integer, List<SuspensionInterval>> result = new HashMap<>();
228 + if (diners == null || diners.isEmpty()) {
229 + return result;
230 + }
231 + final long now = System.currentTimeMillis();
232 + List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
233 + List<Integer> classIds = diners.stream().map(SingleDinerRpcResponse::getClassId).distinct().toList();
234 + List<Integer> dinerIds = diners.stream().map(SingleDinerRpcResponse::getId).toList();
235 +
236 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
237 + enterpriseId, clientIds, null, List.of(0));
238 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
239 + enterpriseId, clientIds, classIds, List.of(0));
240 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
241 + enterpriseId, clientIds, classIds, dinerIds);
242 +
243 + // 仅保留每个维度“最新一条”有效记录(createdAt 最大)
244 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
245 + schoolLevelRecords.stream()
246 + .filter(Objects::nonNull)
247 + .filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
248 + .collect(Collectors.toMap(
249 + SingleDinerMealSuspensionRecordRpcResponse::getClientId,
250 + Function.identity(),
251 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
252 + ));
253 +
254 + Map<String, SingleDinerMealSuspensionRecordRpcResponse> clientClassToLatest = classLevelRecords == null ? Map.of() :
255 + classLevelRecords.stream()
256 + .filter(Objects::nonNull)
257 + .filter(r -> r.getDinerId() == 0)
258 + .collect(Collectors.toMap(
259 + r -> r.getClientId() + "#" + r.getClassId(),
260 + Function.identity(),
261 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
262 + ));
263 +
264 + Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> dinerIdToLatest = dinerLevelRecords == null ? Map.of() :
265 + dinerLevelRecords.stream()
266 + .filter(Objects::nonNull)
267 + .collect(Collectors.toMap(
268 + SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
269 + Function.identity(),
270 + (a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
271 + ));
272 +
273 + for (SingleDinerRpcResponse diner : diners) {
274 + List<SuspensionInterval> intervals = new ArrayList<>();
275 +
276 + SingleDinerMealSuspensionRecordRpcResponse latestSchool = clientIdToLatestSchool.get(diner.getClientId());
277 + SingleDinerMealSuspensionRecordRpcResponse latestClass = clientClassToLatest.get(diner.getClientId() + "#" + diner.getClassId());
278 + SingleDinerMealSuspensionRecordRpcResponse latestDiner = dinerIdToLatest.get(diner.getId());
279 +
280 + if (latestSchool != null && latestSchool.getEndAt() > now) {
281 + intervals.add(new SuspensionInterval(latestSchool.getStartAt(), latestSchool.getEndAt()));
282 + }
283 + if (latestClass != null && latestClass.getEndAt() > now) {
284 + intervals.add(new SuspensionInterval(latestClass.getStartAt(), latestClass.getEndAt()));
285 + }
286 + if (latestDiner != null && latestDiner.getEndAt() > now) {
287 + intervals.add(new SuspensionInterval(latestDiner.getStartAt(), latestDiner.getEndAt()));
288 + }
289 +
290 + // 归并并集:合并重叠区间、保留间隙
291 + intervals.sort(Comparator.comparingLong(i -> i.startAt));
292 + List<SuspensionInterval> merged = new ArrayList<>();
293 + for (SuspensionInterval interval : intervals) {
294 + if (merged.isEmpty() || interval.startAt > merged.get(merged.size() - 1).endAt) {
295 + merged.add(new SuspensionInterval(interval.startAt, interval.endAt));
296 + } else {
297 + SuspensionInterval last = merged.get(merged.size() - 1);
298 + long newEnd = Math.max(last.endAt, interval.endAt);
299 + merged.set(merged.size() - 1, new SuspensionInterval(last.startAt, newEnd));
300 + }
301 + }
302 +
303 + if (!merged.isEmpty()) {
304 + result.put(diner.getId(), merged);
305 + }
306 + }
307 +
308 + return result;
309 + }
310 +
311 + @Nullable
312 + private SingleDinerMealSuspensionRecordRpcResponse pickLatest(List<SingleDinerMealSuspensionRecordRpcResponse> records,
313 + java.util.function.Predicate<SingleDinerMealSuspensionRecordRpcResponse> predicate) {
314 + // 在满足谓词条件的集合中,按 createdAt 取最新的一条
315 + if (records == null || records.isEmpty()) {
316 + return null;
317 + }
318 + return records.stream()
319 + .filter(Objects::nonNull)
320 + .filter(predicate)
321 + .max(Comparator.comparingLong(SingleDinerMealSuspensionRecordRpcResponse::getCreatedAt))
322 + .orElse(null);
323 + }
324 +}
325 +
326 +