Jiaqi Xia

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

......@@ -7,6 +7,7 @@ import com.infoloop.tianting.mealorderservice.*;
import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import com.infoloop.tianting.service.SuspensionService;
import com.infoloop.tianting.store.WeeklyTaskStore;
import com.infoloop.tianting.utils.DateUtil;
import lombok.RequiredArgsConstructor;
......@@ -46,6 +47,8 @@ public class AutoCreateMealOrderTask {
private final WeeklyTaskStore weeklyTaskStore;
private final SuspensionService suspensionService;
@Scheduled(cron = "0 0/5 * * * ?")
public void executeTask() {
final int enterpriseId = 449;
......@@ -188,6 +191,7 @@ public class AutoCreateMealOrderTask {
// 构建创建请求
final var creations = notReservedDiners.stream()
.map(diner -> toMealOrderCreation(
enterpriseId,
diner,
clientMap,
gradeClassMap,
......@@ -217,29 +221,32 @@ public class AutoCreateMealOrderTask {
}
}
private MealOrderCreation toMealOrderCreation(SingleDinerRpcResponse diner,
Map<Integer, SingleClientRpcResponse> clientMap,
Map<Integer, SingleGradeClassRpcResponse> classMap,
Map<Integer, SingleMealMenuRecordRpcResponse> menuMap,
Map<Integer, SingleMpAccountDinerRefRpcResponse> mpAccountDinerRefMap,
private MealOrderCreation toMealOrderCreation(int enterpriseId,
SingleDinerRpcResponse diner,
Map<Integer, SingleClientRpcResponse> clientIdToClient,
Map<Integer, SingleGradeClassRpcResponse> classIdToClass,
Map<Integer, SingleMealMenuRecordRpcResponse> clientIdToLatestMenu,
Map<Integer, SingleMpAccountDinerRefRpcResponse> dinerIdToAccountRef,
ZonedDateTime startOfNextWeek,
Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> schoolSuspensionMap,
Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> studentSuspensionMap,
Map<String, Integer> holidayMap) {
final var menu = menuMap.get(diner.getClientId());
final var client = clientMap.get(diner.getClientId());
final var gradeClass = classMap.get(diner.getClassId());
final var mpAccountDinerRef = mpAccountDinerRefMap.get(diner.getId());
Map<String, Integer> dateStrToHolidayStatus) {
final SingleMealMenuRecordRpcResponse menu = clientIdToLatestMenu.get(diner.getClientId());
final SingleClientRpcResponse client = clientIdToClient.get(diner.getClientId());
final SingleGradeClassRpcResponse gradeClass = classIdToClass.get(diner.getClassId());
final SingleMpAccountDinerRefRpcResponse mpAccountDinerRef = dinerIdToAccountRef.get(diner.getId());
if (menu == null || client == null || gradeClass == null) return null;
// 获取停餐信息,优先使用学校级别的
SingleDinerMealSuspensionRecordRpcResponse suspensionRecord = schoolSuspensionMap.get(diner.getClientId());
if (suspensionRecord == null) {
suspensionRecord = studentSuspensionMap.get(diner.getId());
}
// 计算并集停餐窗口(学校/班级/学生维度的最新记录取并集)
final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
enterpriseId,
diner.getClientId(),
diner.getClassId(),
diner.getId()
);
// 构建下周的餐食安排
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, suspensionRecord, holidayMap);
// 构建下周的餐食安排(基于并集时间窗)
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, unionWindow, dateStrToHolidayStatus);
if (meals.isEmpty()) {
return null;
......@@ -261,41 +268,36 @@ public class AutoCreateMealOrderTask {
.build();
}
private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
SingleDinerMealSuspensionRecordRpcResponse suspensionRecord,
Map<String, Integer> holidayMap) {
// 已由并集窗口版本替代
private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
SuspensionService.SuspensionWindow unionWindow,
Map<String, Integer> holidayMap) {
return IntStream.range(0, 7)
.mapToObj(i -> {
final var date = nextWeekStart.plusDays(i);
final var dateStr = date.format(DateUtil.YYYY_MM_DD);
// 检查是否为节假日
final var status = holidayMap.get(dateStr);
if (status != null && status != 0 && status != 2) {
// 如果不是工作日,返回null
return null;
}
// 如果有停餐信息,检查当前循环的日期是否在停餐时间范围内
if (suspensionRecord != null) {
if (unionWindow != null) {
final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli();
final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli();
final var suspensionStart = suspensionRecord.getStartAt();
final var suspensionEnd = suspensionRecord.getEndAt();
// 如果停餐时间范围与当前日期有重叠,返回null
if (!(dateEnd < suspensionStart || dateStart > suspensionEnd)) {
if (!(dateEnd < unionWindow.startAt || dateStart > unionWindow.endAt)) {
return null;
}
}
// 如果是工作日且不在停餐时间内,返回正常菜单
return MenuSchedule.newBuilder()
.setDate(date.format(DateUtil.YYYY_MM_DD_LEFT_SYMBOL))
.setMenu("A")
.build();
})
.filter(Objects::nonNull) // 过滤掉null值
.filter(Objects::nonNull)
.toList();
}
......
package com.infoloop.tianting.service;
import com.infoloop.tianting.mealorderservice.SingleDinerMealSuspensionRecordRpcResponse;
import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse;
import java.util.List;
import java.util.Map;
/**
* 公共停餐服务:为单个就餐人提供学校/班级/学生三个维度的最新停餐记录集合
*/
public interface SuspensionService {
/**
* 简单时间窗对象
*/
class SuspensionWindow {
public final long startAt;
public final long endAt;
public SuspensionWindow(long startAt, long endAt) {
this.startAt = startAt;
this.endAt = endAt;
}
}
/**
* 获取就餐人的停餐记录(学校/班级/学生三个维度各取最新一条),用于做并集判断
*
* @param enterpriseId 企业ID
* @param clientId 学校ID
* @param classId 班级ID
* @param dinerId 就餐人ID
* @return 最多包含3条记录(学校级一条、班级级一条、学生级一条),按存在性返回非空集合
*/
List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId);
/**
* 获取就餐人的停餐并集时间窗(取上述三维度最新记录的并集:min(startAt), max(endAt))。
* 若不存在任何停餐记录则返回 null。
*/
SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId);
/**
* 为多个订餐人批量计算停餐并集时间窗。避免逐个调用,内部一次性查询并用 Map 聚合。
* 返回的 Map key 为 dinerId,仅为存在停餐窗口的订餐人返回条目。
*/
Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners);
}
......@@ -15,6 +15,7 @@ import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import com.infoloop.tianting.utils.DateUtil;
import com.infoloop.tianting.service.SuspensionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -31,6 +32,7 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
......@@ -44,6 +46,8 @@ public class MealOrderServiceImpl implements MealOrderService {
private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;
private final SuspensionService suspensionService;
@Override
public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
final var loginInfo = LoginContextHolder.getLoginInfo();
......@@ -105,78 +109,16 @@ public class MealOrderServiceImpl implements MealOrderService {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
}
// 5. 校验停餐记录
var currentTime = System.currentTimeMillis();
// 5.1 先查询学校级别的停餐信息
var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
List.of(dinerById.getClientId()),
null,
null
);
// 直接过滤出学校级别的停餐记录
if (schoolSuspensionRecords != null) {
schoolSuspensionRecords = schoolSuspensionRecords.stream()
.filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
.toList();
}
if (schoolSuspensionRecords != null && !schoolSuspensionRecords.isEmpty()) {
var latestSchoolSuspension = schoolSuspensionRecords.stream()
.max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
.orElse(null);
if (latestSchoolSuspension != null &&
currentTime >= latestSchoolSuspension.getStartAt() &&
currentTime <= latestSchoolSuspension.getEndAt()) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
// 5.2 再查询班级级别的停餐信息
var classSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
// 5. 校验停餐记录(学校/班级/学生三个维度最新记录并集)
final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
loginInfo.getEnterpriseId(),
List.of(dinerById.getClientId()),
List.of(dinerById.getClassId()),
null
dinerById.getClientId(),
dinerById.getClassId(),
dinerById.getId()
);
// 直接过滤出班级级别的停餐记录
if (classSuspensionRecords != null) {
classSuspensionRecords = classSuspensionRecords.stream()
.filter(r -> r.getDinerId() == 0)
.toList();
}
if (classSuspensionRecords != null && !classSuspensionRecords.isEmpty()) {
var latestClassSuspension = classSuspensionRecords.stream()
.max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
.orElse(null);
if (latestClassSuspension != null &&
currentTime >= latestClassSuspension.getStartAt() &&
currentTime <= latestClassSuspension.getEndAt()) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
// 5.3 再查询学生级别的停餐信息
var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
List.of(dinerById.getClientId()),
List.of(dinerById.getClassId()),
List.of(dinerById.getId())
);
if (studentSuspensionRecords != null && !studentSuspensionRecords.isEmpty()) {
var latestStudentSuspension = studentSuspensionRecords.stream()
.max((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt()))
.orElse(null);
if (latestStudentSuspension != null &&
currentTime >= latestStudentSuspension.getStartAt() &&
currentTime <= latestStudentSuspension.getEndAt()) {
if (unionWindow != null) {
final long nowMs = System.currentTimeMillis();
if (nowMs >= unionWindow.startAt && nowMs <= unionWindow.endAt) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
......@@ -304,113 +246,37 @@ public class MealOrderServiceImpl implements MealOrderService {
// 3. 获取所有订餐人信息
var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);
var dinerById = diners.stream().collect(Collectors.toMap(SingleDinerRpcResponse::getId, diner -> diner));
// 4. 获取所有不重复的clientId
var uniqueClientIds = diners.stream()
.map(diner -> Long.valueOf(diner.getClientId()))
.distinct()
.toList();
// 5. 批量获取所有学校的停餐信息
final var now = Instant.now();
var schoolSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
null,
null,
null
).stream().filter(r -> r.getClassId() == 0).filter(r -> r.getEndAt() > now.toEpochMilli()).toList();
// 6. 批量获取所有学生的停餐信息
var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
null,
null,
dinerIds
).stream().filter(r -> r.getEndAt() > now.toEpochMilli()).toList();
// 4. 使用公共停餐服务:批量计算三维度(学校/班级/学生)“最新记录并集”的停餐窗口,避免逐个调用
Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
.collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
// 7. 构建学校停餐信息Map
var schoolSuspensionMap = schoolSuspensionRecords.stream()
.collect(Collectors.groupingBy(
SingleDinerMealSuspensionRecordRpcResponse::getClientId,
Collectors.collectingAndThen(
Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
)
));
Map<Integer, SuspensionService.SuspensionWindow> dinerIdToUnionWindow =
suspensionService.getSuspensionUnionWindowsForDiners(loginInfo.getEnterpriseId(), diners);
// 8. 构建学生停餐信息Map
var studentSuspensionMap = studentSuspensionRecords.stream()
.collect(Collectors.groupingBy(
SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
Collectors.collectingAndThen(
Collectors.maxBy((a, b) -> Long.compare(a.getCreatedAt(), b.getCreatedAt())),
opt -> opt.map(o -> convertToVO(o, dinerById)).orElse(null)
)
));
// 9. 获取每个订餐人的停餐信息
return diners.stream()
.map(diner -> {
// 获取学校级别的停餐信息
var schoolSuspension = schoolSuspensionMap.get(Long.valueOf(diner.getClientId()).intValue());
// 获取学生级别的停餐信息
var studentSuspension = studentSuspensionMap.get(Long.valueOf(diner.getId()).intValue());
// 如果两个都没有停餐信息,返回null
if (schoolSuspension == null && studentSuspension == null) {
return null;
}
// 如果只有学校级别的停餐信息
if (schoolSuspension != null && studentSuspension == null) {
return schoolSuspension;
}
// 如果只有学生级别的停餐信息
if (schoolSuspension == null && studentSuspension != null) {
return studentSuspension;
}
// 如果两个都有停餐信息,合并时间范围
List<DinerMealSuspensionRecordVO> suspensionList = dinerIdToUnionWindow.entrySet().stream()
.map(entry -> {
Integer dinerId = entry.getKey();
SuspensionService.SuspensionWindow unionWindow = entry.getValue();
SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
return DinerMealSuspensionRecordVO.builder()
.id(schoolSuspension.getId())
.enterpriseId(schoolSuspension.getEnterpriseId())
.clientId(schoolSuspension.getClientId())
.classId(studentSuspension.getClassId())
.dinerId(studentSuspension.getDinerId())
.dinerName(studentSuspension.getDinerName())
.studentNo(studentSuspension.getStudentNo())
.startAt(new Date(Math.min(schoolSuspension.getStartAt().getTime(), studentSuspension.getStartAt().getTime())))
.endAt(new Date(Math.max(schoolSuspension.getEndAt().getTime(), studentSuspension.getEndAt().getTime())))
.enterpriseId(Long.valueOf(dinerInfo.getEnterpriseId()))
.clientId(Long.valueOf(dinerInfo.getClientId()))
.classId(Long.valueOf(dinerInfo.getClassId()))
.dinerId(Long.valueOf(dinerInfo.getId()))
.dinerName(dinerInfo.getName())
.studentNo(dinerInfo.getStudentNo())
.startAt(new Date(unionWindow.startAt))
.endAt(new Date(unionWindow.endAt))
.comment("")
.build();
})
.filter(Objects::nonNull)
.distinct()
.toList();
return suspensionList;
}
/**
* 将RPC响应转换为VO对象
*/
private DinerMealSuspensionRecordVO convertToVO(SingleDinerMealSuspensionRecordRpcResponse response,
Map<Integer, SingleDinerRpcResponse> dinerById) {
if (response == null) {
return null;
}
return DinerMealSuspensionRecordVO.builder()
.id(Long.valueOf(response.getId()))
.enterpriseId(Long.valueOf(response.getEnterpriseId()))
.clientId(Long.valueOf(response.getClientId()))
.classId(Long.valueOf(response.getClassId()))
.dinerId(Long.valueOf(response.getDinerId()))
.dinerName(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getName() : null)
.studentNo(dinerById.get(response.getDinerId()) != null ? dinerById.get(response.getDinerId()).getStudentNo() : null)
.startAt(new Date(response.getStartAt()))
.endAt(new Date(response.getEndAt()))
.comment(response.getComment())
.build();
}
// 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换
}
......
package com.infoloop.tianting.service.impl;
import com.infoloop.tianting.mealorderservice.SingleDinerMealSuspensionRecordRpcResponse;
import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse;
import com.infoloop.tianting.service.SuspensionService;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Map;
import java.util.HashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class SuspensionServiceImpl implements SuspensionService {
private final MealOrderServiceRpcClient mealOrderServiceRpcClient;
@Override
public List<SingleDinerMealSuspensionRecordRpcResponse> getLatestSuspensionsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
// 汇总三种维度(学校/班级/学生)各自“最新一条”停餐记录
List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensionRecords = new ArrayList<>(3);
// 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
null,
List.of(0)
);
SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest(
schoolLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0
);
if (latestSchoolLevelRecord != null) {
latestSuspensionRecords.add(latestSchoolLevelRecord);
}
// 班级维度:dinerId == 0,按 clientId + classId 过滤
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
List.of(classId),
List.of(0)
);
SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
classLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
);
if (latestClassLevelRecord != null) {
latestSuspensionRecords.add(latestClassLevelRecord);
}
// 学生维度:三个 id 都不为 0
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
List.of(classId),
List.of(dinerId)
);
SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest(
dinerLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId
);
if (latestDinerLevelRecord != null) {
latestSuspensionRecords.add(latestDinerLevelRecord);
}
return latestSuspensionRecords;
}
@Override
public SuspensionWindow getSuspensionUnionWindowForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
// 取最新记录集合并生成并集窗口:\[min(startAt), max(endAt)\]
List<SingleDinerMealSuspensionRecordRpcResponse> latestSuspensions = getLatestSuspensionsForDiner(enterpriseId, clientId, classId, dinerId);
if (latestSuspensions == null || latestSuspensions.isEmpty()) {
return null;
}
long minimalStartEpochMillis = Long.MAX_VALUE;
long maximalEndEpochMillis = Long.MIN_VALUE;
for (SingleDinerMealSuspensionRecordRpcResponse record : latestSuspensions) {
if (record.getStartAt() < minimalStartEpochMillis) {
minimalStartEpochMillis = record.getStartAt();
}
if (record.getEndAt() > maximalEndEpochMillis) {
maximalEndEpochMillis = record.getEndAt();
}
}
if (minimalStartEpochMillis == Long.MAX_VALUE || maximalEndEpochMillis == Long.MIN_VALUE) {
return null;
}
return new SuspensionWindow(minimalStartEpochMillis, maximalEndEpochMillis);
}
@Override
public Map<Integer, SuspensionWindow> getSuspensionUnionWindowsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
// 基于输入 diners 一次性拉取三类停餐记录,再按 diner 维度聚合出最新记录并计算并集窗口
Map<Integer, SuspensionWindow> dinerIdToWindow = new HashMap<>();
if (diners == null || diners.isEmpty()) {
return dinerIdToWindow;
}
// 预构建索引集合
List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
List<Integer> classIds = diners.stream().map(SingleDinerRpcResponse::getClassId).distinct().toList();
List<Integer> dinerIds = diners.stream().map(SingleDinerRpcResponse::getId).toList();
// 查询三类记录
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, null, List.of(0));
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, List.of(0));
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, dinerIds);
// 学校维度:clientId -> latest
Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
schoolLevelRecords.stream()
.filter(Objects::nonNull)
.filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
.collect(Collectors.toMap(
SingleDinerMealSuspensionRecordRpcResponse::getClientId,
Function.identity(),
(a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
));
// 班级维度:clientId+classId -> latest(使用合成 key)
Map<String, SingleDinerMealSuspensionRecordRpcResponse> clientClassToLatest = classLevelRecords == null ? Map.of() :
classLevelRecords.stream()
.filter(Objects::nonNull)
.filter(r -> r.getDinerId() == 0)
.collect(Collectors.toMap(
r -> r.getClientId() + "#" + r.getClassId(),
Function.identity(),
(a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
));
// 学生维度:dinerId -> latest
Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> dinerIdToLatest = dinerLevelRecords == null ? Map.of() :
dinerLevelRecords.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(
SingleDinerMealSuspensionRecordRpcResponse::getDinerId,
Function.identity(),
(a, b) -> a.getCreatedAt() >= b.getCreatedAt() ? a : b
));
// 计算每个 diner 的并集窗口
for (SingleDinerRpcResponse diner : diners) {
SingleDinerMealSuspensionRecordRpcResponse latestSchool = clientIdToLatestSchool.get(diner.getClientId());
SingleDinerMealSuspensionRecordRpcResponse latestClass = clientClassToLatest.get(diner.getClientId() + "#" + diner.getClassId());
SingleDinerMealSuspensionRecordRpcResponse latestDiner = dinerIdToLatest.get(diner.getId());
long minStart = Long.MAX_VALUE;
long maxEnd = Long.MIN_VALUE;
if (latestSchool != null) {
if (latestSchool.getStartAt() < minStart) {
minStart = latestSchool.getStartAt();
}
if (latestSchool.getEndAt() > maxEnd) {
maxEnd = latestSchool.getEndAt();
}
}
if (latestClass != null) {
if (latestClass.getStartAt() < minStart) {
minStart = latestClass.getStartAt();
}
if (latestClass.getEndAt() > maxEnd) {
maxEnd = latestClass.getEndAt();
}
}
if (latestDiner != null) {
if (latestDiner.getStartAt() < minStart) {
minStart = latestDiner.getStartAt();
}
if (latestDiner.getEndAt() > maxEnd) {
maxEnd = latestDiner.getEndAt();
}
}
if (minStart != Long.MAX_VALUE && maxEnd != Long.MIN_VALUE) {
dinerIdToWindow.put(diner.getId(), new SuspensionWindow(minStart, maxEnd));
}
}
return dinerIdToWindow;
}
@Nullable
private SingleDinerMealSuspensionRecordRpcResponse pickLatest(List<SingleDinerMealSuspensionRecordRpcResponse> records,
java.util.function.Predicate<SingleDinerMealSuspensionRecordRpcResponse> predicate) {
// 在满足谓词条件的集合中,按 createdAt 取最新的一条
if (records == null || records.isEmpty()) {
return null;
}
return records.stream()
.filter(Objects::nonNull)
.filter(predicate)
.max(Comparator.comparingLong(SingleDinerMealSuspensionRecordRpcResponse::getCreatedAt))
.orElse(null);
}
}