Jiaqi Xia

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

Dev/nongxin adjust



See merge request !8
......@@ -20,6 +20,12 @@ application {
'-Dlogging.path=logs']
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
ext {
grpcVersion = '1.62.2'
protobufVersion = '3.25.3'
......
......@@ -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 intervals = suspensionService.getSuspensionIntervalsForDiner(
enterpriseId,
diner.getClientId(),
diner.getClassId(),
diner.getId()
);
// 构建下周的餐食安排
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, suspensionRecord, holidayMap);
// 构建下周的餐食安排(基于多段停餐区间)
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, intervals, dateStrToHolidayStatus);
if (meals.isEmpty()) {
return null;
......@@ -261,8 +268,10 @@ public class AutoCreateMealOrderTask {
.build();
}
// 已由并集窗口版本替代
private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
SingleDinerMealSuspensionRecordRpcResponse suspensionRecord,
java.util.List<SuspensionService.SuspensionInterval> intervals,
Map<String, Integer> holidayMap) {
return IntStream.range(0, 7)
.mapToObj(i -> {
......@@ -272,30 +281,23 @@ public class AutoCreateMealOrderTask {
// 检查是否为节假日
final var status = holidayMap.get(dateStr);
if (status != null && status != 0 && status != 2) {
// 如果不是工作日,返回null
return null;
}
// 如果有停餐信息,检查当前循环的日期是否在停餐时间范围内
if (suspensionRecord != null) {
if (intervals != null && !intervals.isEmpty()) {
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)) {
return null;
}
final boolean overlaps = intervals.stream()
.anyMatch(interval -> !(dateEnd < interval.startAt || dateStart > interval.endAt));
if (overlaps) 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();
}
......
......@@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@Builder
......@@ -38,6 +39,13 @@ public class DinerMealSuspensionRecordVO {
@ApiModelProperty("学号")
private String studentNo;
@ApiModelProperty("停餐时间区间列表")
private List<Interval> intervals;
@Data
@Builder
@ApiModel("停餐时间区间")
public static class Interval {
@ApiModelProperty("开始时间")
private Date startAt;
......@@ -46,4 +54,5 @@ public class DinerMealSuspensionRecordVO {
@ApiModelProperty("备注")
private String comment;
}
}
\ No newline at end of file
......
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;
}
}
/**
* 多段时间窗对象(标准闭区间 \[startAt, endAt\]),用于表示并集后可能出现的多段停餐区间
*/
class SuspensionInterval {
public final long startAt;
public final long endAt;
public SuspensionInterval(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);
/**
* 获取单个订餐人的停餐多段时间区间(对学校/班级/学生三个维度的最新记录做标准并集,保留间隙)
*/
List<SuspensionInterval> getSuspensionIntervalsForDiner(int enterpriseId, int clientId, int classId, int dinerId);
/**
* 批量获取多个订餐人的停餐多段时间区间。返回 Map<dinerId, List<Interval>>。
*/
Map<Integer, List<SuspensionInterval>> getSuspensionIntervalsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners);
}
......@@ -15,13 +15,13 @@ 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;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
......@@ -30,7 +30,7 @@ import java.util.Collections;
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 +44,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,80 +107,23 @@ 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(
// 5. 校验停餐记录(学校/班级/学生三个维度最新记录的多段并集区间)
final var intervals = suspensionService.getSuspensionIntervalsForDiner(
loginInfo.getEnterpriseId(),
List.of(dinerById.getClientId()),
null,
null
dinerById.getClientId(),
dinerById.getClassId(),
dinerById.getId()
);
// 直接过滤出学校级别的停餐记录
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()) {
if (intervals != null && !intervals.isEmpty()) {
// 校验每个订餐日期是否在停餐时间段内
for (var meal : createMealOrderDTO.getMeals()) {
final long mealDateMs = DateUtil.parseDate(meal.getDate()).getTime();
final boolean inSuspension = intervals.stream()
.anyMatch(i -> mealDateMs >= i.startAt && mealDateMs <= i.endAt);
if (inSuspension) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
// 5.2 再查询班级级别的停餐信息
var classSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
List.of(dinerById.getClientId()),
List.of(dinerById.getClassId()),
null
);
// 直接过滤出班级级别的停餐记录
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()) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
// 6. 校验菜单信息
......@@ -304,113 +249,48 @@ 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();
// 4. 使用公共停餐服务:批量计算每个订餐人的多段停餐区间
Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
.collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
// 6. 批量获取所有学生的停餐信息
var studentSuspensionRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
loginInfo.getEnterpriseId(),
null,
null,
dinerIds
).stream().filter(r -> r.getEndAt() > now.toEpochMilli()).toList();
Map<Integer, List<SuspensionService.SuspensionInterval>> dinerIdToIntervals =
suspensionService.getSuspensionIntervalsForDiners(loginInfo.getEnterpriseId(), diners);
// 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)
)
));
// 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;
List<DinerMealSuspensionRecordVO> result = new ArrayList<>();
for (Map.Entry<Integer, List<SuspensionService.SuspensionInterval>> entry : dinerIdToIntervals.entrySet()) {
Integer dinerId = entry.getKey();
SingleDinerRpcResponse dinerInfo = dinerIdToDiner.get(dinerId);
if (dinerInfo == null) {
continue;
}
// 如果只有学校级别的停餐信息
if (schoolSuspension != null && studentSuspension == null) {
return schoolSuspension;
List<SuspensionService.SuspensionInterval> intervals = entry.getValue();
if (intervals == null || intervals.isEmpty()) {
continue;
}
// 如果只有学生级别的停餐信息
if (schoolSuspension == null && studentSuspension != null) {
return studentSuspension;
}
// 如果两个都有停餐信息,合并时间范围
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())))
List<DinerMealSuspensionRecordVO.Interval> voIntervals = intervals.stream()
.map(i -> DinerMealSuspensionRecordVO.Interval.builder()
.startAt(new Date(i.startAt))
.endAt(new Date(i.endAt))
.comment("")
.build();
})
.filter(Objects::nonNull)
.distinct()
.build())
.toList();
result.add(DinerMealSuspensionRecordVO.builder()
.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())
.intervals(voIntervals)
.build());
}
return result;
}
/**
* 将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);
final long now = System.currentTimeMillis();
// 学校维度:classId == 0 && dinerId == 0,按 clientId 过滤
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
null,
List.of(0)
);
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = schoolLevelRecordsRaw == null ? List.of() :
schoolLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
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> classLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
List.of(classId),
List.of(0)
);
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = classLevelRecordsRaw == null ? List.of() :
classLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
classLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
);
if (latestClassLevelRecord != null) {
latestSuspensionRecords.add(latestClassLevelRecord);
}
// 学生维度:三个 id 都不为 0
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecordsRaw = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId,
List.of(clientId),
List.of(classId),
List.of(dinerId)
);
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = dinerLevelRecordsRaw == null ? List.of() :
dinerLevelRecordsRaw.stream().filter(r -> r.getEndAt() > now).toList();
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;
}
final long now = System.currentTimeMillis();
// 预构建索引集合
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> schoolLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, null, List.of(0));
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, List.of(0));
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecordsRawBatch = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, dinerIds);
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = schoolLevelRecordsRawBatch == null ? List.of() :
schoolLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = classLevelRecordsRawBatch == null ? List.of() :
classLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = dinerLevelRecordsRawBatch == null ? List.of() :
dinerLevelRecordsRawBatch.stream().filter(r -> r.getEndAt() > now).toList();
// 学校维度: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;
}
@Override
public List<SuspensionInterval> getSuspensionIntervalsForDiner(int enterpriseId, int clientId, int classId, int dinerId) {
Map<Integer, List<SuspensionInterval>> map = getSuspensionIntervalsForDiners(enterpriseId, List.of(
SingleDinerRpcResponse.newBuilder()
.setEnterpriseId(enterpriseId)
.setClientId(clientId)
.setClassId(classId)
.setId(dinerId)
.build()
));
return map.getOrDefault(dinerId, List.of());
}
@Override
public Map<Integer, List<SuspensionInterval>> getSuspensionIntervalsForDiners(int enterpriseId, List<SingleDinerRpcResponse> diners) {
Map<Integer, List<SuspensionInterval>> result = new HashMap<>();
if (diners == null || diners.isEmpty()) {
return result;
}
final long now = System.currentTimeMillis();
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);
// 仅保留每个维度“最新一条”有效记录(createdAt 最大)
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
));
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
));
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
));
for (SingleDinerRpcResponse diner : diners) {
List<SuspensionInterval> intervals = new ArrayList<>();
SingleDinerMealSuspensionRecordRpcResponse latestSchool = clientIdToLatestSchool.get(diner.getClientId());
SingleDinerMealSuspensionRecordRpcResponse latestClass = clientClassToLatest.get(diner.getClientId() + "#" + diner.getClassId());
SingleDinerMealSuspensionRecordRpcResponse latestDiner = dinerIdToLatest.get(diner.getId());
if (latestSchool != null && latestSchool.getEndAt() > now) {
intervals.add(new SuspensionInterval(latestSchool.getStartAt(), latestSchool.getEndAt()));
}
if (latestClass != null && latestClass.getEndAt() > now) {
intervals.add(new SuspensionInterval(latestClass.getStartAt(), latestClass.getEndAt()));
}
if (latestDiner != null && latestDiner.getEndAt() > now) {
intervals.add(new SuspensionInterval(latestDiner.getStartAt(), latestDiner.getEndAt()));
}
// 归并并集:合并重叠区间、保留间隙
intervals.sort(Comparator.comparingLong(i -> i.startAt));
List<SuspensionInterval> merged = new ArrayList<>();
for (SuspensionInterval interval : intervals) {
if (merged.isEmpty() || interval.startAt > merged.get(merged.size() - 1).endAt) {
merged.add(new SuspensionInterval(interval.startAt, interval.endAt));
} else {
SuspensionInterval last = merged.get(merged.size() - 1);
long newEnd = Math.max(last.endAt, interval.endAt);
merged.set(merged.size() - 1, new SuspensionInterval(last.startAt, newEnd));
}
}
if (!merged.isEmpty()) {
result.put(diner.getId(), merged);
}
}
return result;
}
@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);
}
}