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);
}