feat:订餐时间过滤
This commit is contained in:
1 parent
617bd2ef24
commit
d5f46aed99
2 files changed
+127
-6
No files matched your search
+16
-6
@@ -63,19 +63,20 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
private final HardApptCycleRuleService cycleRuleService;
|
||||
private final HardApptMerchantModeService merchantModeService;
|
||||
|
||||
/** 返回当前商户的日期餐段,动态模式使用统一解析器,按周模式保留原批次计算。 */
|
||||
/** 返回当前商户的 App 日期餐段,动态模式隐藏已截止目标,按周模式保留原批次计算。 */
|
||||
@Override
|
||||
public List<HardApptDateOptionsVo> listOptions(Long mchId) {
|
||||
return listOptions(mchId, nowInShanghai());
|
||||
}
|
||||
|
||||
/** 按固定北京时间生成 App 列表,保留当前开放及未来将开放的目标,便于核对截止边界。 */
|
||||
List<HardApptDateOptionsVo> listOptions(Long mchId, LocalDateTime now) {
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
LocalDate today = now.toLocalDate();
|
||||
Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate = new TreeMap<>();
|
||||
|
||||
if (merchantMode == HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
addDynamicOptions(mchId, now, today, null, optionsByDate);
|
||||
addDynamicOptions(mchId, now, today, null, false, optionsByDate);
|
||||
} else {
|
||||
for (HardApptCycleRuleVo rule : cycleRuleService.listEnabledConfigurations(mchId)) {
|
||||
HardApptCycleRuleCalculator.CycleBatch batch;
|
||||
@@ -134,7 +135,7 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
}
|
||||
|
||||
if (merchantMode == HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
addDynamicOptions(mchId, now, startDate, endDate, optionsByDate);
|
||||
addDynamicOptions(mchId, now, startDate, endDate, true, optionsByDate);
|
||||
} else {
|
||||
addCyclePreviewOptions(mchId, now, startDate, endDate, optionsByDate);
|
||||
}
|
||||
@@ -147,13 +148,22 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日期餐段汇总商户全部启用窗口,每个目标只返回一个选项;推演范围只筛选用餐日期,
|
||||
* 不能提前过滤未来开放窗口,否则最终预约范围和退款截止会随查询时刻漂移。
|
||||
* 按日期餐段汇总商户全部启用窗口,每个目标只返回一个选项。
|
||||
* 必须先合并所有窗口,再决定是否展示已截止目标,避免改变连续开放区间及最终退款截止。
|
||||
*
|
||||
* @param mchId 当前租户内的商户 ID
|
||||
* @param now 同一次请求固定的北京时间
|
||||
* @param startDate 最早展示的实际用餐日期,包含当天
|
||||
* @param endDate 最晚展示的实际用餐日期,包含当天;null 表示不限制上界
|
||||
* @param includeClosed 管理端推演为 true,保留已截止目标;App 列表为 false
|
||||
* @param optionsByDate 按用餐日期汇总的输出容器,只向其中添加符合条件的餐段
|
||||
*/
|
||||
private void addDynamicOptions(Long mchId, LocalDateTime now, LocalDate startDate, LocalDate endDate,
|
||||
Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate) {
|
||||
boolean includeClosed, Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate) {
|
||||
HardApptWindowResolver.resolve(hardApptService.listEnabledWindows(mchId), now).forEach((target, resolved) -> {
|
||||
if (target.date().isBefore(startDate) || endDate != null && target.date().isAfter(endDate)) return;
|
||||
// 开放区间采用闭区间:截止准点仍展示;未来窗口和两次开放间的空档仍保留为尚未开放。
|
||||
if (!includeClosed && resolved.end().isBefore(now)) return;
|
||||
optionsByDate.computeIfAbsent(target.date(), key -> new LinkedHashMap<>())
|
||||
.put("appt:" + target.hourType(), HardApptWindowResolver.toOption(target, resolved, now));
|
||||
});
|
||||
|
||||
+111
@@ -16,6 +16,7 @@ import com.bsd.pjcloud.hard.service.HardApptService;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
|
||||
import com.bsd.pjcloud.common.core.exception.I18nBizException;
|
||||
import com.bsd.pjcloud.common.core.util.MsgUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.bsd.pjcloud.hard.service.support.HardApptCycleRuleCalculator;
|
||||
import com.bsd.pjcloud.hard.service.support.HardApptWindowResolver.WindowSource;
|
||||
@@ -35,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -164,6 +166,106 @@ class HardApptReservationServiceImplTest {
|
||||
&& option.getStartTime() != null && option.getEndTime() != null));
|
||||
}
|
||||
|
||||
/** 已截止窗口即使用餐日在今天或未来也不输出;同日其他有效餐段仍保留,空日期整体移除。 */
|
||||
@Test
|
||||
void v3ListOmitsExpiredTargetsAndKeepsOtherMeals() {
|
||||
LocalDate today = LocalDate.of(2026, 10, 23);
|
||||
HardApptWindowEntity closed = wideWindow();
|
||||
closed.setStartTime(today.minusDays(4).atStartOfDay());
|
||||
closed.setEndTime(today.minusDays(3).atTime(12, 0));
|
||||
HardApptWindowEntity active = wideWindow();
|
||||
active.setId(2L);
|
||||
active.setStartTime(today.minusDays(3).atStartOfDay());
|
||||
active.setEndTime(today.plusDays(1).atTime(12, 0));
|
||||
HardApptReservationServiceImpl service = dynamicOptionsService(List.of(
|
||||
new WindowSource(closed, List.of(windowItem(1L, today, "1"),
|
||||
windowItem(1L, today.plusDays(1), "1"), windowItem(1L, today.plusDays(2), "1"))),
|
||||
new WindowSource(active, List.of(windowItem(2L, today.plusDays(2), "2")))));
|
||||
|
||||
try (var messages = mockStatic(MsgUtils.class)) {
|
||||
messages.when(() -> MsgUtils.getMessage(any(String.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
List<HardApptDateOptionsVo> result = service.listOptions(10L, today.atTime(8, 0));
|
||||
|
||||
assertEquals(List.of(today.plusDays(2)), result.stream().map(HardApptDateOptionsVo::getApptDate).toList());
|
||||
assertEquals(List.of("2"), result.get(0).getApptList().stream().map(HardApptOptionVo::getHourType).toList());
|
||||
assertTrue(result.get(0).getApptList().get(0).getAvailable());
|
||||
}
|
||||
}
|
||||
|
||||
/** App 列表与下单保持闭区间:截止准点仍展示,结束一纳秒后隐藏且不留下空日期。 */
|
||||
@Test
|
||||
void v3ListIncludesWindowEndAndOmitsOneNanosecondAfter() {
|
||||
LocalDateTime end = LocalDateTime.of(2026, 10, 23, 12, 0);
|
||||
HardApptWindowEntity window = wideWindow();
|
||||
window.setStartTime(end.minusDays(1));
|
||||
window.setEndTime(end);
|
||||
HardApptReservationServiceImpl service = dynamicOptionsService(List.of(
|
||||
new WindowSource(window, List.of(windowItem(1L, end.toLocalDate(), "1")))));
|
||||
|
||||
try (var messages = mockStatic(MsgUtils.class)) {
|
||||
messages.when(() -> MsgUtils.getMessage(any(String.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
List<HardApptDateOptionsVo> atEnd = service.listOptions(10L, end);
|
||||
|
||||
assertEquals(1, atEnd.size());
|
||||
assertTrue(atEnd.get(0).getApptList().get(0).getAvailable());
|
||||
assertTrue(service.listOptions(10L, end.plusNanos(1)).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
/** 未来尚未开放的目标继续展示;同一目标的旧窗口已结束也不能误删空档后的下一窗口。 */
|
||||
@Test
|
||||
void v3ListKeepsFutureWindowsIncludingGapsAfterClosedWindows() {
|
||||
LocalDateTime now = LocalDateTime.of(2026, 10, 23, 8, 0);
|
||||
LocalDate target = now.toLocalDate().plusDays(3);
|
||||
HardApptWindowEntity closed = wideWindow();
|
||||
closed.setStartTime(now.minusDays(4));
|
||||
closed.setEndTime(now.minusDays(3));
|
||||
HardApptWindowEntity next = wideWindow();
|
||||
next.setId(2L);
|
||||
next.setStartTime(now.plusDays(1));
|
||||
next.setEndTime(now.plusDays(2));
|
||||
HardApptReservationServiceImpl service = dynamicOptionsService(List.of(
|
||||
new WindowSource(closed, List.of(windowItem(1L, target, "1"))),
|
||||
new WindowSource(next, List.of(windowItem(2L, target, "1"), windowItem(2L, target.plusDays(1), "1")))));
|
||||
|
||||
try (var messages = mockStatic(MsgUtils.class)) {
|
||||
messages.when(() -> MsgUtils.getMessage(any(String.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
List<HardApptDateOptionsVo> result = service.listOptions(10L, now);
|
||||
|
||||
assertEquals(List.of(target, target.plusDays(1)), result.stream().map(HardApptDateOptionsVo::getApptDate).toList());
|
||||
assertTrue(result.stream().flatMap(day -> day.getApptList().stream()).allMatch(option ->
|
||||
Long.valueOf(2L).equals(option.getId()) && Boolean.FALSE.equals(option.getAvailable())
|
||||
&& ErrorCodes.HARD_APPT_NOT_OPEN.equals(option.getUnavailableReason())
|
||||
&& next.getStartTime().equals(option.getReserveStartDateTime())
|
||||
&& next.getEndTime().equals(option.getAutoRefundDeadline())));
|
||||
}
|
||||
}
|
||||
|
||||
/** 管理端仍需推演已截止配置及自然日空档,不跟随 App 列表隐藏过期窗口。 */
|
||||
@Test
|
||||
void dynamicPreviewKeepsClosedOptionsAndEmptyDates() {
|
||||
LocalDateTime now = LocalDateTime.of(2026, 10, 23, 8, 0);
|
||||
HardApptWindowEntity closed = wideWindow();
|
||||
closed.setStartTime(now.minusDays(4));
|
||||
closed.setEndTime(now.minusDays(3));
|
||||
LocalDate target = now.toLocalDate().plusDays(1);
|
||||
HardApptReservationServiceImpl service = dynamicOptionsService(List.of(
|
||||
new WindowSource(closed, List.of(windowItem(1L, target, "1")))));
|
||||
|
||||
try (var messages = mockStatic(MsgUtils.class)) {
|
||||
messages.when(() -> MsgUtils.getMessage(any(String.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
List<HardApptDateOptionsVo> result = service.previewOptions(10L, 3, now);
|
||||
|
||||
assertEquals(3, result.size());
|
||||
assertTrue(result.get(0).getApptList().isEmpty());
|
||||
assertTrue(result.get(2).getApptList().isEmpty());
|
||||
HardApptOptionVo option = result.get(1).getApptList().get(0);
|
||||
assertFalse(option.getAvailable());
|
||||
assertEquals(ErrorCodes.HARD_APPT_CLOSED, option.getUnavailableReason());
|
||||
assertEquals(closed.getEndTime(), option.getReserveEndDateTime());
|
||||
}
|
||||
}
|
||||
|
||||
/** 管理端推演限制在推演日期范围内,范围外日期不输出。 */
|
||||
@Test
|
||||
void dynamicPreviewBoundsOptionsToDateRange() {
|
||||
@@ -445,6 +547,15 @@ class HardApptReservationServiceImplTest {
|
||||
mock(HardApptCycleRuleService.class), merchantModeService(HardApptReserveModeEnum.TIME_WINDOW));
|
||||
}
|
||||
|
||||
/** 仅用于列表与推演的多窗口替身,直接提供商户全部启用窗口,避免引入下单校验的无关桩。 */
|
||||
private HardApptReservationServiceImpl dynamicOptionsService(List<WindowSource> sources) {
|
||||
HardApptService apptService = mock(HardApptService.class);
|
||||
when(apptService.listEnabledWindows(10L)).thenReturn(sources);
|
||||
return new HardApptReservationServiceImpl(apptService, mock(HardApptWindowMapper.class),
|
||||
mock(HardApptItemMapper.class), mock(HardApptCycleRuleService.class),
|
||||
merchantModeService(HardApptReserveModeEnum.TIME_WINDOW));
|
||||
}
|
||||
|
||||
/** 周期模式的预约服务替身。 */
|
||||
private HardApptReservationServiceImpl cycleService(List<HardApptCycleRuleVo> rules, Long mchId) {
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
|
||||
Reference in new issue
Block a user