Jiaqi Xia

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

......@@ -20,6 +20,12 @@ application {
'-Dlogging.path=logs']
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
ext {
grpcVersion = '1.62.2'
protobufVersion = '3.25.3'
......
......@@ -237,16 +237,16 @@ public class AutoCreateMealOrderTask {
final SingleMpAccountDinerRefRpcResponse mpAccountDinerRef = dinerIdToAccountRef.get(diner.getId());
if (menu == null || client == null || gradeClass == null) return null;
// 计算并集停餐窗口(学校/班级/学生维度的最新记录取并集
final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
// 计算多段停餐区间(学校/班级/学生维度最新记录的并集,保留间隙
final var intervals = suspensionService.getSuspensionIntervalsForDiner(
enterpriseId,
diner.getClientId(),
diner.getClassId(),
diner.getId()
);
// 构建下周的餐食安排(基于并集时间窗
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, unionWindow, dateStrToHolidayStatus);
// 构建下周的餐食安排(基于多段停餐区间
List<MenuSchedule> meals = buildNextWeekMeals(startOfNextWeek, intervals, dateStrToHolidayStatus);
if (meals.isEmpty()) {
return null;
......@@ -271,7 +271,7 @@ public class AutoCreateMealOrderTask {
// 已由并集窗口版本替代
private List<MenuSchedule> buildNextWeekMeals(ZonedDateTime nextWeekStart,
SuspensionService.SuspensionWindow unionWindow,
java.util.List<SuspensionService.SuspensionInterval> intervals,
Map<String, Integer> holidayMap) {
return IntStream.range(0, 7)
.mapToObj(i -> {
......@@ -284,12 +284,12 @@ public class AutoCreateMealOrderTask {
return null;
}
if (unionWindow != null) {
if (intervals != null && !intervals.isEmpty()) {
final var dateStart = date.with(LocalTime.MIN).toInstant().toEpochMilli();
final var dateEnd = date.with(LocalTime.MAX).toInstant().toEpochMilli();
if (!(dateEnd < unionWindow.startAt || dateStart > unionWindow.endAt)) {
return null;
}
final boolean overlaps = intervals.stream()
.anyMatch(interval -> !(dateEnd < interval.startAt || dateStart > interval.endAt));
if (overlaps) return null;
}
return MenuSchedule.newBuilder()
......
......@@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@Builder
......@@ -38,12 +39,20 @@ public class DinerMealSuspensionRecordVO {
@ApiModelProperty("学号")
private String studentNo;
@ApiModelProperty("开始时间")
private Date startAt;
@ApiModelProperty("停餐时间区间列表")
private List<Interval> intervals;
@ApiModelProperty("结束时间")
private Date endAt;
@Data
@Builder
@ApiModel("停餐时间区间")
public static class Interval {
@ApiModelProperty("开始时间")
private Date startAt;
@ApiModelProperty("备注")
private String comment;
@ApiModelProperty("结束时间")
private Date endAt;
@ApiModelProperty("备注")
private String comment;
}
}
\ No newline at end of file
......
......@@ -25,6 +25,19 @@ public interface SuspensionService {
}
/**
* 多段时间窗对象(标准闭区间 \[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
......@@ -46,6 +59,16 @@ public interface SuspensionService {
* 返回的 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);
}
......
......@@ -22,7 +22,6 @@ 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;
......@@ -31,7 +30,6 @@ 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;
......@@ -109,16 +107,18 @@ public class MealOrderServiceImpl implements MealOrderService {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.GRADE_CLASS_NOT_FOUND);
}
// 5. 校验停餐记录(学校/班级/学生三个维度最新记录并集)
final var unionWindow = suspensionService.getSuspensionUnionWindowForDiner(
// 5. 校验停餐记录(学校/班级/学生三个维度最新记录的多段并集区间)
final long nowMs = System.currentTimeMillis();
final var intervals = suspensionService.getSuspensionIntervalsForDiner(
loginInfo.getEnterpriseId(),
dinerById.getClientId(),
dinerById.getClassId(),
dinerById.getId()
);
if (unionWindow != null) {
final long nowMs = System.currentTimeMillis();
if (nowMs >= unionWindow.startAt && nowMs <= unionWindow.endAt) {
if (intervals != null && !intervals.isEmpty()) {
final boolean inSuspension = intervals.stream()
.anyMatch(i -> nowMs >= i.startAt && nowMs <= i.endAt);
if (inSuspension) {
throw ClientEndExceptions.IncorrectRequestValue.build(ErrorCodeEnum.DINER_MEAL_SUSPENDED);
}
}
......@@ -247,32 +247,43 @@ public class MealOrderServiceImpl implements MealOrderService {
// 3. 获取所有订餐人信息
var diners = mealOrderServiceRpcClient.getDinersByIds(loginInfo.getEnterpriseId(), dinerIds);
// 4. 使用公共停餐服务:批量计算三维度(学校/班级/学生)“最新记录并集”的停餐窗口,避免逐个调用
// 4. 使用公共停餐服务:批量计算每个订餐人的多段停餐区间
Map<Integer, SingleDinerRpcResponse> dinerIdToDiner = diners.stream()
.collect(Collectors.toMap(SingleDinerRpcResponse::getId, Function.identity()));
Map<Integer, SuspensionService.SuspensionWindow> dinerIdToUnionWindow =
suspensionService.getSuspensionUnionWindowsForDiners(loginInfo.getEnterpriseId(), diners);
Map<Integer, List<SuspensionService.SuspensionInterval>> dinerIdToIntervals =
suspensionService.getSuspensionIntervalsForDiners(loginInfo.getEnterpriseId(), diners);
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()
.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))
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;
}
List<SuspensionService.SuspensionInterval> intervals = entry.getValue();
if (intervals == null || intervals.isEmpty()) {
continue;
}
List<DinerMealSuspensionRecordVO.Interval> voIntervals = intervals.stream()
.map(i -> DinerMealSuspensionRecordVO.Interval.builder()
.startAt(new Date(i.startAt))
.endAt(new Date(i.endAt))
.comment("")
.build();
})
.toList();
return suspensionList;
.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;
}
/**
......
......@@ -27,6 +27,7 @@ public class SuspensionServiceImpl implements SuspensionService {
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> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
......@@ -34,7 +35,7 @@ public class SuspensionServiceImpl implements SuspensionService {
List.of(clientId),
null,
List.of(0)
);
).stream().filter(r -> r.getEndAt() > now).toList();
SingleDinerMealSuspensionRecordRpcResponse latestSchoolLevelRecord = pickLatest(
schoolLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == 0 && record.getDinerId() == 0
......@@ -49,7 +50,7 @@ public class SuspensionServiceImpl implements SuspensionService {
List.of(clientId),
List.of(classId),
List.of(0)
);
).stream().filter(r -> r.getEndAt() > now).toList();
SingleDinerMealSuspensionRecordRpcResponse latestClassLevelRecord = pickLatest(
classLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == 0
......@@ -64,7 +65,7 @@ public class SuspensionServiceImpl implements SuspensionService {
List.of(clientId),
List.of(classId),
List.of(dinerId)
);
).stream().filter(r -> r.getEndAt() > now).toList();
SingleDinerMealSuspensionRecordRpcResponse latestDinerLevelRecord = pickLatest(
dinerLevelRecords,
record -> record.getClientId() == clientId && record.getClassId() == classId && record.getDinerId() == dinerId
......@@ -106,6 +107,7 @@ public class SuspensionServiceImpl implements SuspensionService {
if (diners == null || diners.isEmpty()) {
return dinerIdToWindow;
}
final long now = System.currentTimeMillis();
// 预构建索引集合
List<Integer> clientIds = diners.stream().map(SingleDinerRpcResponse::getClientId).distinct().toList();
......@@ -114,11 +116,11 @@ public class SuspensionServiceImpl implements SuspensionService {
// 查询三类记录
List<SingleDinerMealSuspensionRecordRpcResponse> schoolLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, null, List.of(0));
enterpriseId, clientIds, null, List.of(0)).stream().filter(r -> r.getEndAt() > now).toList();
List<SingleDinerMealSuspensionRecordRpcResponse> classLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, List.of(0));
enterpriseId, clientIds, classIds, List.of(0)).stream().filter(r -> r.getEndAt() > now).toList();
List<SingleDinerMealSuspensionRecordRpcResponse> dinerLevelRecords = mealOrderServiceRpcClient.queryDinerMealSuspensionRecordsByCondition(
enterpriseId, clientIds, classIds, dinerIds);
enterpriseId, clientIds, classIds, dinerIds).stream().filter(r -> r.getEndAt() > now).toList();
// 学校维度:clientId -> latest
Map<Integer, SingleDinerMealSuspensionRecordRpcResponse> clientIdToLatestSchool = schoolLevelRecords == null ? Map.of() :
......@@ -193,6 +195,93 @@ public class SuspensionServiceImpl implements SuspensionService {
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(clientId) // 注意:此处仅为构造占位,真正需要的是 clientId/classId/dinerId
.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);
Map<Integer, List<SingleDinerMealSuspensionRecordRpcResponse>> clientIdToSchool = schoolLevelRecords == null ? Map.of() :
schoolLevelRecords.stream()
.filter(Objects::nonNull)
.filter(r -> r.getClassId() == 0 && r.getDinerId() == 0)
.collect(Collectors.groupingBy(SingleDinerMealSuspensionRecordRpcResponse::getClientId));
Map<String, List<SingleDinerMealSuspensionRecordRpcResponse>> clientClassToRecords = classLevelRecords == null ? Map.of() :
classLevelRecords.stream()
.filter(Objects::nonNull)
.filter(r -> r.getDinerId() == 0)
.collect(Collectors.groupingBy(r -> r.getClientId() + "#" + r.getClassId()));
Map<Integer, List<SingleDinerMealSuspensionRecordRpcResponse>> dinerIdToRecords = dinerLevelRecords == null ? Map.of() :
dinerLevelRecords.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(SingleDinerMealSuspensionRecordRpcResponse::getDinerId));
for (SingleDinerRpcResponse diner : diners) {
List<SuspensionInterval> intervals = new ArrayList<>();
List<SingleDinerMealSuspensionRecordRpcResponse> schoolRecs = clientIdToSchool.getOrDefault(diner.getClientId(), List.of());
List<SingleDinerMealSuspensionRecordRpcResponse> classRecs = clientClassToRecords.getOrDefault(diner.getClientId() + "#" + diner.getClassId(), List.of());
List<SingleDinerMealSuspensionRecordRpcResponse> dinerRecs = dinerIdToRecords.getOrDefault(diner.getId(), List.of());
for (SingleDinerMealSuspensionRecordRpcResponse rec : schoolRecs) {
if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.getEndAt()));
}
for (SingleDinerMealSuspensionRecordRpcResponse rec : classRecs) {
if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.getEndAt()));
}
for (SingleDinerMealSuspensionRecordRpcResponse rec : dinerRecs) {
if (rec.getEndAt() > now) intervals.add(new SuspensionInterval(rec.getStartAt(), rec.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) {
......