Jiaqi Xia

feat: 支持批量查询订餐人多段停餐区间(getSuspensionIntervalsForDiners)完善SuspensionServiceImpl相关逻辑

...@@ -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'
......
...@@ -237,16 +237,16 @@ public class AutoCreateMealOrderTask { ...@@ -237,16 +237,16 @@ public class AutoCreateMealOrderTask {
237 final SingleMpAccountDinerRefRpcResponse mpAccountDinerRef = dinerIdToAccountRef.get(diner.getId()); 237 final SingleMpAccountDinerRefRpcResponse mpAccountDinerRef = dinerIdToAccountRef.get(diner.getId());
238 if (menu == null || client == null || gradeClass == null) return null; 238 if (menu == null || client == null || gradeClass == null) return null;
239 239
240 - // 计算并集停餐窗口(学校/班级/学生维度的最新记录取并集 240 + // 计算多段停餐区间(学校/班级/学生维度最新记录的并集,保留间隙
241 - final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner( 241 + final var intervals = suspensionService.getSuspensionIntervalsForDiner(
242 enterpriseId, 242 enterpriseId,
243 diner.getClientId(), 243 diner.getClientId(),
244 diner.getClassId(), 244 diner.getClassId(),
245 diner.getId() 245 diner.getId()
246 ); 246 );
247 247
248 - // 构建下周的餐食安排(基于并集时间窗 248 + // 构建下周的餐食安排(基于多段停餐区间
249 - List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, unionWindow, dateStrToHolidayStatus); 249 + List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, intervals, dateStrToHolidayStatus);
250 250
251 if (meals.isEmpty()) { 251 if (meals.isEmpty()) {
252 return null; 252 return null;
...@@ -271,7 +271,7 @@ public class AutoCreateMealOrderTask { ...@@ -271,7 +271,7 @@ public class AutoCreateMealOrderTask {
271 // 已由并集窗口版本替代 271 // 已由并集窗口版本替代
272 272
273 private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart, 273 private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
274 - SuspensionService.SuspensionWindow unionWindow, 274 + java.util.List<SuspensionService.SuspensionInterval> intervals,
275 Map<String, Integer> holidayMap) { 275 Map<String, Integer> holidayMap) {
276 return IntStream.range(0, 7) 276 return IntStream.range(0, 7)
277 .mapToObj(i -> { 277 .mapToObj(i -> {
...@@ -284,12 +284,12 @@ public class AutoCreateMealOrderTask { ...@@ -284,12 +284,12 @@ public class AutoCreateMealOrderTask {
284 return null; 284 return null;
285 } 285 }
286 286
287 - if (unionWindow != null) { 287 + if (intervals != null && !intervals.isEmpty()) {
288 final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli(); 288 final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli();
289 final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli(); 289 final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli();
290 - if (!(dateEnd < unionWindow.startAt || dateStart > unionWindow.endAt)) { 290 + final boolean overlaps = intervals.stream()
291 - return null; 291 + .anyMatch(interval -> !(dateEnd < interval.startAt || dateStart > interval.endAt));
292 - } 292 + if (overlaps) return null;
293 } 293 }
294 294
295 return MenuSchedule.newBuilder() 295 return MenuSchedule.newBuilder()
......
...@@ -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,6 +39,13 @@ public class DinerMealSuspensionRecordVO { ...@@ -38,6 +39,13 @@ public class DinerMealSuspensionRecordVO {
38 @ApiModelProperty("学号") 39 @ApiModelProperty("学号")
39 private String studentNo; 40 private String studentNo;
40 41
42 + @ApiModelProperty("停餐时间区间列表")
43 + private List<Interval> intervals;
44 +
45 + @Data
46 + @Builder
47 + @ApiModel("停餐时间区间")
48 + public static class Interval {
41 @ApiModelProperty("开始时间") 49 @ApiModelProperty("开始时间")
42 private Date startAt; 50 private Date startAt;
43 51
...@@ -46,4 +54,5 @@ public class DinerMealSuspensionRecordVO { ...@@ -46,4 +54,5 @@ public class DinerMealSuspensionRecordVO {
46 54
47 @ApiModelProperty("备注") 55 @ApiModelProperty("备注")
48 private String comment; 56 private String comment;
57 + }
49 } 58 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -25,6 +25,19 @@ public interface SuspensionService { ...@@ -25,6 +25,19 @@ public interface SuspensionService {
25 } 25 }
26 26
27 /** 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 + /**
28 * 获取就餐人的停餐记录(学校/班级/学生三个维度各取最新一条),用于做并集判断 41 * 获取就餐人的停餐记录(学校/班级/学生三个维度各取最新一条),用于做并集判断
29 * 42 *
30 * @param enterpriseId 企业ID 43 * @param enterpriseId 企业ID
...@@ -46,6 +59,16 @@ public interface SuspensionService { ...@@ -46,6 +59,16 @@ public interface SuspensionService {
46 * 返回的 Map key 为 dinerId,仅为存在停餐窗口的订餐人返回条目。 59 * 返回的 Map key 为 dinerId,仅为存在停餐窗口的订餐人返回条目。
47 */ 60 */
48 Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners); 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);
49 } 72 }
50 73
51 74
......
...@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
22 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
23 23
24 import java.time.DayOfWeek; 24 import java.time.DayOfWeek;
25 -import java.time.Instant;
26 import java.time.LocalDate; 25 import java.time.LocalDate;
27 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
28 import java.time.LocalTime; 27 import java.time.LocalTime;
...@@ -31,7 +30,6 @@ import java.util.Collections; ...@@ -31,7 +30,6 @@ import java.util.Collections;
31 import java.util.Date; 30 import java.util.Date;
32 import java.util.List; 31 import java.util.List;
33 import java.util.Map; 32 import java.util.Map;
34 -import java.util.Objects;
35 import java.util.function.Function; 33 import java.util.function.Function;
36 import java.util.stream.Collectors; 34 import java.util.stream.Collectors;
37 35
...@@ -109,16 +107,18 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -109,16 +107,18 @@ public class MealOrderServiceImpl implements MealOrderService {
109 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND); 107 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
110 } 108 }
111 109
112 - // 5. 校验停餐记录(学校/班级/学生三个维度最新记录并集) 110 + // 5. 校验停餐记录(学校/班级/学生三个维度最新记录的多段并集区间)
113 - final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner( 111 + final long nowMs = System.currentTimeMillis();
112 + final var intervals = suspensionService.getSuspensionIntervalsForDiner(
114 loginInfo.getEnterpriseId(), 113 loginInfo.getEnterpriseId(),
115 dinerById.getClientId(), 114 dinerById.getClientId(),
116 dinerById.getClassId(), 115 dinerById.getClassId(),
117 dinerById.getId() 116 dinerById.getId()
118 ); 117 );
119 - if (unionWindow != null) { 118 + if (intervals != null && !intervals.isEmpty()) {
120 - final long nowMs = System.currentTimeMillis(); 119 + final boolean inSuspension = intervals.stream()
121 - if (nowMs >= unionWindow.startAt && nowMs <= unionWindow.endAt) { 120 + .anyMatch(i -> nowMs >= i.startAt && nowMs <= i.endAt);
121 + if (inSuspension) {
122 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED); 122 throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
123 } 123 }
124 } 124 }
...@@ -247,32 +247,43 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -247,32 +247,43 @@ public class MealOrderServiceImpl implements MealOrderService {
247 // 3. 获取所有订餐人信息 247 // 3. 获取所有订餐人信息
248 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds); 248 var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);
249 249
250 - // 4. 使用公共停餐服务:批量计算三维度(学校/班级/学生)“最新记录并集”的停餐窗口,避免逐个调用 250 + // 4. 使用公共停餐服务:批量计算每个订餐人的多段停餐区间
251 Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream() 251 Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
252 .collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity())); 252 .collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
253 253
254 - Map<Integer, SuspensionService.SuspensionWindow> dinerIdToUnionWindow = 254 + Map<Integer, List<SuspensionService.SuspensionInterval>> dinerIdToIntervals =
255 - suspensionService.getSuspensionUnionWindowsForDiners(loginInfo.getEnterpriseId(), diners); 255 + suspensionService.getSuspensionIntervalsForDiners(loginInfo.getEnterpriseId(), diners);
256 256
257 - List<DinerMealSuspensionRecordVO> suspensionList = dinerIdToUnionWindow.entrySet().stream() 257 + List<DinerMealSuspensionRecordVO> result = new ArrayList<>();
258 - .map(entry -> { 258 + for (Map.Entry<Integer, List<SuspensionService.SuspensionInterval>> entry : dinerIdToIntervals.entrySet()) {
259 Integer dinerId = entry.getKey(); 259 Integer dinerId = entry.getKey();
260 - SuspensionService.SuspensionWindow unionWindow = entry.getValue();
261 SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId); 260 SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
262 - return DinerMealSuspensionRecordVO.builder() 261 + if (dinerInfo == null) {
262 + continue;
263 + }
264 + List<SuspensionService.SuspensionInterval> intervals = entry.getValue();
265 + if (intervals == null || intervals.isEmpty()) {
266 + continue;
267 + }
268 + List<DinerMealSuspensionRecordVO.Interval> voIntervals = intervals.stream()
269 + .map(i -> DinerMealSuspensionRecordVO.Interval.builder()
270 + .startAt(new Date(i.startAt))
271 + .endAt(new Date(i.endAt))
272 + .comment("")
273 + .build())
274 + .toList();
275 +
276 + result.add(DinerMealSuspensionRecordVO.builder()
263 .enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId())) 277 .enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId()))
264 .clientId(Long.valueOf(dinerInfo.getClientId())) 278 .clientId(Long.valueOf(dinerInfo.getClientId()))
265 .classId(Long.valueOf(dinerInfo.getClassId())) 279 .classId(Long.valueOf(dinerInfo.getClassId()))
266 .dinerId(Long.valueOf(dinerInfo.getId())) 280 .dinerId(Long.valueOf(dinerInfo.getId()))
267 .dinerName(dinerInfo.getName()) 281 .dinerName(dinerInfo.getName())
268 .studentNo(dinerInfo.getStudentNo()) 282 .studentNo(dinerInfo.getStudentNo())
269 - .startAt(new Date(unionWindow.startAt)) 283 + .intervals(voIntervals)
270 - .endAt(new Date(unionWindow.endAt)) 284 + .build());
271 - .comment("") 285 + }
272 - .build(); 286 + return result;
273 - })
274 - .toList();
275 - return suspensionList;
276 } 287 }
277 288
278 /** 289 /**
......
...@@ -27,6 +27,7 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -27,6 +27,7 @@ public class SuspensionServiceImpl implements SuspensionService {
27 public List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId) { 27 public List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
28 // 汇总三种维度(学校/班级/学生)各自“最新一条”停餐记录 28 // 汇总三种维度(学校/班级/学生)各自“最新一条”停餐记录
29 List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensionRecords = new ArrayList<>(3); 29 List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensionRecords = new ArrayList<>(3);
30 + final long now = System.currentTimeMillis();
30 31
31 // 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤 32 // 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤
32 List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition( 33 List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
...@@ -34,7 +35,7 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -34,7 +35,7 @@ public class SuspensionServiceImpl implements SuspensionService {
34 List.of(clientId), 35 List.of(clientId),
35 null, 36 null,
36 List.of(0) 37 List.of(0)
37 - ); 38 + ).stream().filter(r -> r.getEndAt() > now).toList();
38 SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest( 39 SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest(
39 schoolLevelRecords, 40 schoolLevelRecords,
40 record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0 41 record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0
...@@ -49,7 +50,7 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -49,7 +50,7 @@ public class SuspensionServiceImpl implements SuspensionService {
49 List.of(clientId), 50 List.of(clientId),
50 List.of(classId), 51 List.of(classId),
51 List.of(0) 52 List.of(0)
52 - ); 53 + ).stream().filter(r -> r.getEndAt() > now).toList();
53 SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest( 54 SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
54 classLevelRecords, 55 classLevelRecords,
55 record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0 56 record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
...@@ -64,7 +65,7 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -64,7 +65,7 @@ public class SuspensionServiceImpl implements SuspensionService {
64 List.of(clientId), 65 List.of(clientId),
65 List.of(classId), 66 List.of(classId),
66 List.of(dinerId) 67 List.of(dinerId)
67 - ); 68 + ).stream().filter(r -> r.getEndAt() > now).toList();
68 SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest( 69 SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest(
69 dinerLevelRecords, 70 dinerLevelRecords,
70 record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId 71 record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId
...@@ -106,6 +107,7 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -106,6 +107,7 @@ public class SuspensionServiceImpl implements SuspensionService {
106 if (diners == null || diners.isEmpty()) { 107 if (diners == null || diners.isEmpty()) {
107 return dinerIdToWindow; 108 return dinerIdToWindow;
108 } 109 }
110 + final long now = System.currentTimeMillis();
109 111
110 // 预构建索引集合 112 // 预构建索引集合
111 List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList(); 113 List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
...@@ -114,11 +116,11 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -114,11 +116,11 @@ public class SuspensionServiceImpl implements SuspensionService {
114 116
115 // 查询三类记录 117 // 查询三类记录
116 List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition( 118 List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
117 - enterpriseId, clientIds, null, List.of(0)); 119 + enterpriseId, clientIds, null, List.of(0)).stream().filter(r -> r.getEndAt() > now).toList();
118 List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition( 120 List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
119 - enterpriseId, clientIds, classIds, List.of(0)); 121 + enterpriseId, clientIds, classIds, List.of(0)).stream().filter(r -> r.getEndAt() > now).toList();
120 List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition( 122 List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
121 - enterpriseId, clientIds, classIds, dinerIds); 123 + enterpriseId, clientIds, classIds, dinerIds).stream().filter(r -> r.getEndAt() > now).toList();
122 124
123 // 学校维度:clientId -> latest 125 // 学校维度:clientId -> latest
124 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() : 126 Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
...@@ -193,6 +195,93 @@ public class SuspensionServiceImpl implements SuspensionService { ...@@ -193,6 +195,93 @@ public class SuspensionServiceImpl implements SuspensionService {
193 195
194 return dinerIdToWindow; 196 return dinerIdToWindow;
195 } 197 }
198 +
199 + @Override
200 + public List<SuspensionInterval> getSuspensionIntervalsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
201 + Map<Integer, List<SuspensionInterval>> map = getSuspensionIntervalsForDiners(enterpriseId, List.of(
202 + SingleDinerRpcResponse.newBuilder()
203 + .setEnterpriseId(clientId) // 注意:此处仅为构造占位,真正需要的是 clientId/classId/dinerId
204 + .setClientId(clientId)
205 + .setClassId(classId)
206 + .setId(dinerId)
207 + .build()
208 + ));
209 + return map.getOrDefault(dinerId, List.of());
210 + }
211 +
212 + @Override
213 + public Map<Integer, List<SuspensionInterval>> getSuspensionIntervalsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
214 + Map<Integer, List<SuspensionInterval>> result = new HashMap<>();
215 + if (diners == null || diners.isEmpty()) {
216 + return result;
217 + }
218 + final long now = System.currentTimeMillis();
219 + List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
220 + List<Integer> classIds = diners.stream().map(SingleDinerRpcResponse::getClassId).distinct().toList();
221 + List<Integer> dinerIds = diners.stream().map(SingleDinerRpcResponse::getId).toList();
222 +
223 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
224 + enterpriseId, clientIds, null, List.of(0));
225 + List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
226 + enterpriseId, clientIds, classIds, List.of(0));
227 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
228 + enterpriseId, clientIds, classIds, dinerIds);
229 +
230 + Map<Integer, List<SingleDinerMealSuspensionRecordRpcResponse>> clientIdToSchool = schoolLevelRecords == null ? Map.of() :
231 + schoolLevelRecords.stream()
232 + .filter(Objects::nonNull)
233 + .filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
234 + .collect(Collectors.groupingBy(SingleDinerMealSuspensionRecordRpcResponse::getClientId));
235 +
236 + Map<String, List<SingleDinerMealSuspensionRecordRpcResponse>> clientClassToRecords = classLevelRecords == null ? Map.of() :
237 + classLevelRecords.stream()
238 + .filter(Objects::nonNull)
239 + .filter(r -> r.getDinerId() == 0)
240 + .collect(Collectors.groupingBy(r -> r.getClientId() + "#" + r.getClassId()));
241 +
242 + Map<Integer, List<SingleDinerMealSuspensionRecordRpcResponse>> dinerIdToRecords = dinerLevelRecords == null ? Map.of() :
243 + dinerLevelRecords.stream()
244 + .filter(Objects::nonNull)
245 + .collect(Collectors.groupingBy(SingleDinerMealSuspensionRecordRpcResponse::getDinerId));
246 +
247 + for (SingleDinerRpcResponse diner : diners) {
248 + List<SuspensionInterval> intervals = new ArrayList<>();
249 +
250 + List<SingleDinerMealSuspensionRecordRpcResponse> schoolRecs = clientIdToSchool.getOrDefault(diner.getClientId(), List.of());
251 + List<SingleDinerMealSuspensionRecordRpcResponse> classRecs = clientClassToRecords.getOrDefault(diner.getClientId() + "#" + diner.getClassId(), List.of());
252 + List<SingleDinerMealSuspensionRecordRpcResponse> dinerRecs = dinerIdToRecords.getOrDefault(diner.getId(), List.of());
253 +
254 + for (SingleDinerMealSuspensionRecordRpcResponse rec : schoolRecs) {
255 + if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.getEndAt()));
256 + }
257 + for (SingleDinerMealSuspensionRecordRpcResponse rec : classRecs) {
258 + if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.getEndAt()));
259 + }
260 + for (SingleDinerMealSuspensionRecordRpcResponse rec : dinerRecs) {
261 + if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.getEndAt()));
262 + }
263 +
264 + // 归并并集:合并重叠区间、保留间隙
265 + intervals.sort(Comparator.comparingLong(i -> i.startAt));
266 + List<SuspensionInterval> merged = new ArrayList<>();
267 + for (SuspensionInterval interval : intervals) {
268 + if (merged.isEmpty() || interval.startAt > merged.get(merged.size() - 1).endAt) {
269 + merged.add(new SuspensionInterval(interval.startAt, interval.endAt));
270 + } else {
271 + SuspensionInterval last = merged.get(merged.size() - 1);
272 + long newEnd = Math.max(last.endAt, interval.endAt);
273 + merged.set(merged.size() - 1, new SuspensionInterval(last.startAt, newEnd));
274 + }
275 + }
276 +
277 + if (!merged.isEmpty()) {
278 + result.put(diner.getId(), merged);
279 + }
280 + }
281 +
282 + return result;
283 + }
284 +
196 @Nullable 285 @Nullable
197 private SingleDinerMealSuspensionRecordRpcResponse pickLatest(List<SingleDinerMealSuspensionRecordRpcResponse> records, 286 private SingleDinerMealSuspensionRecordRpcResponse pickLatest(List<SingleDinerMealSuspensionRecordRpcResponse> records,
198 java.util.function.Predicate<SingleDinerMealSuspensionRecordRpcResponse> predicate) { 287 java.util.function.Predicate<SingleDinerMealSuspensionRecordRpcResponse> predicate) {
......