feat:加入预约时间模式
This commit is contained in:
1 parent
6b2a95cc65
commit
61bb43e5c5
14 files changed
+500
-100
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ import lombok.Getter;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 订餐预约限制模式。
|
||||
* 订餐预约限制模式
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
|
||||
+2
-11
@@ -8,11 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.HardApptEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptDateOptionsVo;
|
||||
@@ -51,14 +49,7 @@ public class HardApptController {
|
||||
@ApiCryptoAes
|
||||
@GetMapping("/app/list")
|
||||
public R<List<HardApptEntity>> appList(@ParameterObject HardApptDto dto) {
|
||||
List<HardApptEntity> list = hardApptService.list(new LambdaQueryWrapper<HardApptEntity>()
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getMchId()), HardApptEntity::getMchId, dto.getMchId())
|
||||
.eq(HardApptEntity::getStatus, YesNoEnum.YES.getCode())
|
||||
.eq(HardApptEntity::getReserveMode, HardApptReserveModeEnum.TIME_WINDOW.getType())
|
||||
.orderByAsc(HardApptEntity::getHourType)
|
||||
.orderByDesc(HardApptEntity::getSort)
|
||||
);
|
||||
return R.ok(list);
|
||||
return R.ok(hardApptReservationService.listLegacyFlatOptions(dto.getMchId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "app预约时间列表v2", description = "app预约时间列表v2")
|
||||
@@ -68,7 +59,7 @@ public class HardApptController {
|
||||
return R.ok(hardApptReservationService.listLegacyOptions(dto.getMchId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "app预约时间列表v3", description = "按实际用餐日期返回动态时间窗与按周集中预约餐段")
|
||||
@Operation(summary = "app预约时间列表v3", description = "按商户当前预约模式返回实际用餐日期和餐段")
|
||||
@ApiCryptoAes
|
||||
@GetMapping("/app/v3/list")
|
||||
public R<List<HardApptDateOptionsVo>> appV3List(@ParameterObject HardApptDto dto) {
|
||||
|
||||
+12
@@ -7,7 +7,9 @@ import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptCycleRuleDto;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptCycleRuleVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptDateOptionsVo;
|
||||
import com.bsd.pjcloud.hard.service.HardApptCycleRuleService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptReservationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
@@ -34,6 +37,7 @@ import java.util.List;
|
||||
public class HardApptCycleRuleController {
|
||||
|
||||
private final HardApptCycleRuleService cycleRuleService;
|
||||
private final HardApptReservationService reservationService;
|
||||
|
||||
@Operation(summary = "周期预约规则分页")
|
||||
@GetMapping("/page")
|
||||
@@ -43,6 +47,14 @@ public class HardApptCycleRuleController {
|
||||
return R.ok(cycleRuleService.scopePage(page, dto, DataScope.of()));
|
||||
}
|
||||
|
||||
@Operation(summary = "推演商户用户预约视图")
|
||||
@GetMapping("/preview")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardAppt_view')")
|
||||
public R<List<HardApptDateOptionsVo>> preview(@RequestParam Long mchId,
|
||||
@RequestParam(defaultValue = "7") Integer days) {
|
||||
return R.ok(reservationService.previewOptions(mchId, days));
|
||||
}
|
||||
|
||||
@Operation(summary = "周期预约规则详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('hard_hardAppt_view')")
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bsd.pjcloud.hard.service;
|
||||
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
|
||||
/**
|
||||
* 统一解析商户当前生效的订餐预约模式。
|
||||
*/
|
||||
public interface HardApptMerchantModeService {
|
||||
|
||||
/**
|
||||
* 获取商户及其当前预约模式,历史空配置按动态时间窗处理。
|
||||
*
|
||||
* @param mchId 商户ID
|
||||
* @return 商户预约配置上下文
|
||||
*/
|
||||
MerchantModeContext resolve(Long mchId);
|
||||
|
||||
record MerchantModeContext(MchManageVo merchant, HardApptReserveModeEnum reserveMode) {
|
||||
}
|
||||
}
|
||||
+11
@@ -12,10 +12,21 @@ import java.util.List;
|
||||
*/
|
||||
public interface HardApptReservationService {
|
||||
|
||||
List<HardApptEntity> listLegacyFlatOptions(Long mchId);
|
||||
|
||||
List<HardApptTimeCascadeVo> listLegacyOptions(Long mchId);
|
||||
|
||||
List<HardApptDateOptionsVo> listOptions(Long mchId);
|
||||
|
||||
/**
|
||||
* 按今天起连续自然日推演商户最终预约视图,供管理端核对多条规则叠加结果。
|
||||
*
|
||||
* @param mchId 商户ID
|
||||
* @param days 推演自然日数量,包含今天
|
||||
* @return 每个自然日及其最终餐段配置
|
||||
*/
|
||||
List<HardApptDateOptionsVo> previewOptions(Long mchId, Integer days);
|
||||
|
||||
ReservationValidation validateReservation(Long mchId, Long apptId, Long apptRuleId, String hourType,
|
||||
LocalDate apptDate, String legacyApptTimeType);
|
||||
|
||||
|
||||
+7
-5
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptCycleRuleDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptCycleRuleItemDto;
|
||||
@@ -18,6 +19,7 @@ import com.bsd.pjcloud.hard.api.vo.HardApptCycleRuleVo;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptCycleRuleItemMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptCycleRuleMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardApptCycleRuleService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.hard.service.support.HardApptCycleRuleCalculator;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
@@ -40,6 +42,7 @@ public class HardApptCycleRuleServiceImpl
|
||||
|
||||
private final HardApptCycleRuleItemMapper itemMapper;
|
||||
private final RemoteMchService remoteMchService;
|
||||
private final HardApptMerchantModeService merchantModeService;
|
||||
|
||||
@Override
|
||||
public Page<HardApptCycleRuleVo> scopePage(Page page, HardApptCycleRuleDto dto, DataScope dataScope) {
|
||||
@@ -147,12 +150,11 @@ public class HardApptCycleRuleServiceImpl
|
||||
}
|
||||
|
||||
List<HardApptCycleRuleItemDto> items = normalizeItems(dto.getItemList(), dto.getTargetDayCount());
|
||||
R<MchManageVo> merchantResult = remoteMchService.getById(dto.getMchId());
|
||||
MchManageVo merchant = merchantResult == null || !merchantResult.isOk() ? null : merchantResult.getData();
|
||||
if (merchant == null) {
|
||||
throw new RuntimeException("商户不存在");
|
||||
HardApptMerchantModeService.MerchantModeContext merchantContext = merchantModeService.resolve(dto.getMchId());
|
||||
if (merchantContext.reserveMode() != HardApptReserveModeEnum.WEEKLY_CYCLE) {
|
||||
throw new RuntimeException("当前商户使用动态时间窗,不能维护周期预约规则");
|
||||
}
|
||||
dto.setDeptId(merchant.getDeptId());
|
||||
dto.setDeptId(merchantContext.merchant().getDeptId());
|
||||
|
||||
if (YesNoEnum.YES.getCode().equals(dto.getStatus())) {
|
||||
validateConflict(dto, items, excludeId);
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.bsd.pjcloud.hard.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.wallet.api.entity.mch.MchManageEntity;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 商户预约模式解析实现。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HardApptMerchantModeServiceImpl implements HardApptMerchantModeService {
|
||||
|
||||
private final RemoteMchService remoteMchService;
|
||||
|
||||
@Override
|
||||
public MerchantModeContext resolve(Long mchId) {
|
||||
if (ObjectUtil.isEmpty(mchId)) {
|
||||
throw new RuntimeException("商户ID不能为空");
|
||||
}
|
||||
|
||||
R<MchManageVo> result;
|
||||
try {
|
||||
result = remoteMchService.getById(mchId);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new RuntimeException("获取商户预约配置失败", exception);
|
||||
}
|
||||
if (result == null || !result.isOk()) {
|
||||
String message = result == null || ObjectUtil.isEmpty(result.getMsg())
|
||||
? "获取商户预约配置失败" : result.getMsg();
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
MchManageVo merchant = result.getData();
|
||||
if (merchant == null) {
|
||||
throw new RuntimeException("商户不存在");
|
||||
}
|
||||
|
||||
MchManageEntity.TableReservationConf conf = merchant.getTableReservationConf();
|
||||
String modeValue = conf == null ? null : conf.getReserveMode();
|
||||
HardApptReserveModeEnum reserveMode = HardApptReserveModeEnum.valueOfByType(
|
||||
modeValue == null ? null : modeValue.trim());
|
||||
if (reserveMode == null) {
|
||||
throw new RuntimeException("商户预约时间模式无效");
|
||||
}
|
||||
return new MerchantModeContext(merchant, reserveMode);
|
||||
}
|
||||
}
|
||||
+206
-51
@@ -15,6 +15,7 @@ import com.bsd.pjcloud.hard.api.vo.HardApptDateOptionsVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptOptionVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptTimeCascadeVo;
|
||||
import com.bsd.pjcloud.hard.service.HardApptCycleRuleService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptReservationService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptTimeService;
|
||||
@@ -24,10 +25,12 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -46,17 +49,30 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
|
||||
static final ZoneId RESERVATION_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||
private static final int DEFAULT_PREVIEW_DAYS = 7;
|
||||
private static final int MAX_PREVIEW_DAYS = 31;
|
||||
|
||||
private final HardApptService hardApptService;
|
||||
private final HardApptTimeService hardApptTimeService;
|
||||
private final HardApptCycleRuleService cycleRuleService;
|
||||
private final HardApptMerchantModeService merchantModeService;
|
||||
|
||||
@Override
|
||||
public List<HardApptEntity> listLegacyFlatOptions(Long mchId) {
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
if (merchantMode != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
return List.of();
|
||||
}
|
||||
return findEnabledAppts(mchId, HardApptReserveModeEnum.TIME_WINDOW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HardApptTimeCascadeVo> listLegacyOptions(Long mchId) {
|
||||
validateMerchantId(mchId);
|
||||
List<HardApptEntity> dynamicAppts = findEnabledAppts(mchId).stream()
|
||||
.filter(appt -> resolveMode(appt) == HardApptReserveModeEnum.TIME_WINDOW)
|
||||
.toList();
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
if (merchantMode != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
return List.of();
|
||||
}
|
||||
List<HardApptEntity> dynamicAppts = findEnabledAppts(mchId, merchantMode);
|
||||
if (dynamicAppts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -88,28 +104,26 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
}
|
||||
|
||||
List<HardApptDateOptionsVo> listOptions(Long mchId, LocalDateTime now) {
|
||||
validateMerchantId(mchId);
|
||||
List<HardApptEntity> allAppts = findEnabledAppts(mchId);
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
LocalDate today = now.toLocalDate();
|
||||
Map<LocalDate, List<HardApptOptionVo>> optionsByDate = new TreeMap<>();
|
||||
|
||||
List<HardApptEntity> dynamicAppts = allAppts.stream()
|
||||
.filter(appt -> resolveMode(appt) == HardApptReserveModeEnum.TIME_WINDOW)
|
||||
.toList();
|
||||
Map<String, Set<Long>> apptIdsByDateType = groupMappingsByDateType(mchId, dynamicAppts);
|
||||
for (HardApptTimeTypeEnum dateType : HardApptTimeTypeEnum.values()) {
|
||||
if (!HardApptTimeTypeEnum.isReservationDateType(dateType.getType())) {
|
||||
continue;
|
||||
if (merchantMode == HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
List<HardApptEntity> dynamicAppts = findEnabledAppts(mchId, merchantMode);
|
||||
Map<String, Set<Long>> apptIdsByDateType = groupMappingsByDateType(mchId, dynamicAppts);
|
||||
for (HardApptTimeTypeEnum dateType : HardApptTimeTypeEnum.values()) {
|
||||
if (!HardApptTimeTypeEnum.isReservationDateType(dateType.getType())) {
|
||||
continue;
|
||||
}
|
||||
Set<Long> mappedIds = apptIdsByDateType.getOrDefault(dateType.getType(), Set.of());
|
||||
LocalDate targetDate = today.plusDays(dateType.getDay());
|
||||
dynamicAppts.stream()
|
||||
.filter(appt -> mappedIds.contains(appt.getId()))
|
||||
.map(appt -> buildOption(appt, evaluateAvailability(appt, targetDate, now)))
|
||||
.forEach(option -> optionsByDate.computeIfAbsent(targetDate, key -> new ArrayList<>()).add(option));
|
||||
}
|
||||
Set<Long> mappedIds = apptIdsByDateType.getOrDefault(dateType.getType(), Set.of());
|
||||
LocalDate targetDate = today.plusDays(dateType.getDay());
|
||||
dynamicAppts.stream()
|
||||
.filter(appt -> mappedIds.contains(appt.getId()))
|
||||
.map(appt -> buildOption(appt, evaluateAvailability(appt, targetDate, now)))
|
||||
.forEach(option -> optionsByDate.computeIfAbsent(targetDate, key -> new ArrayList<>()).add(option));
|
||||
}
|
||||
|
||||
for (HardApptCycleRuleVo rule : cycleRuleService.listEnabledConfigurations(mchId)) {
|
||||
} else {
|
||||
for (HardApptCycleRuleVo rule : cycleRuleService.listEnabledConfigurations(mchId)) {
|
||||
HardApptCycleRuleCalculator.CycleBatch batch;
|
||||
try {
|
||||
HardApptCycleRuleCalculator.validate(rule);
|
||||
@@ -131,6 +145,7 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
option.setApptRuleId(rule.getId());
|
||||
optionsByDate.computeIfAbsent(targetDate, key -> new ArrayList<>()).add(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return optionsByDate.entrySet().stream().map(entry -> {
|
||||
@@ -144,6 +159,148 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端推演会覆盖多个未来周批次,并保留没有配置餐段的自然日,便于管理员核对规则空档。
|
||||
*/
|
||||
@Override
|
||||
public List<HardApptDateOptionsVo> previewOptions(Long mchId, Integer days) {
|
||||
return previewOptions(mchId, days, nowInShanghai());
|
||||
}
|
||||
|
||||
List<HardApptDateOptionsVo> previewOptions(Long mchId, Integer days, LocalDateTime now) {
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
int previewDays = days == null ? DEFAULT_PREVIEW_DAYS : days;
|
||||
if (previewDays < 1 || previewDays > MAX_PREVIEW_DAYS) {
|
||||
throw new RuntimeException("推演天数必须在1至31天之间");
|
||||
}
|
||||
|
||||
LocalDate startDate = now.toLocalDate();
|
||||
LocalDate endDate = startDate.plusDays(previewDays - 1L);
|
||||
Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate = new TreeMap<>();
|
||||
for (int day = 0; day < previewDays; day++) {
|
||||
optionsByDate.put(startDate.plusDays(day), new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
if (merchantMode == HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
addDynamicPreviewOptions(mchId, now, startDate, endDate, optionsByDate);
|
||||
} else {
|
||||
addCyclePreviewOptions(mchId, now, startDate, endDate, optionsByDate);
|
||||
}
|
||||
return optionsByDate.entrySet().stream().map(entry -> {
|
||||
HardApptDateOptionsVo dateOptions = new HardApptDateOptionsVo();
|
||||
dateOptions.setApptDate(entry.getKey());
|
||||
dateOptions.setApptList(new ArrayList<>(entry.getValue().values()));
|
||||
return dateOptions;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态模式仍只按已有今天、明天、后天映射展示,不把相对日期规则错误扩展到整个推演范围。
|
||||
*/
|
||||
private void addDynamicPreviewOptions(Long mchId, LocalDateTime now, LocalDate startDate, LocalDate endDate,
|
||||
Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate) {
|
||||
List<HardApptEntity> dynamicAppts = findEnabledAppts(mchId, HardApptReserveModeEnum.TIME_WINDOW);
|
||||
Map<String, Set<Long>> apptIdsByDateType = groupMappingsByDateType(mchId, dynamicAppts);
|
||||
for (HardApptTimeTypeEnum dateType : HardApptTimeTypeEnum.values()) {
|
||||
if (!HardApptTimeTypeEnum.isReservationDateType(dateType.getType())) {
|
||||
continue;
|
||||
}
|
||||
LocalDate targetDate = startDate.plusDays(dateType.getDay());
|
||||
if (targetDate.isAfter(endDate)) {
|
||||
continue;
|
||||
}
|
||||
Set<Long> mappedIds = apptIdsByDateType.getOrDefault(dateType.getType(), Set.of());
|
||||
dynamicAppts.stream()
|
||||
.filter(appt -> mappedIds.contains(appt.getId()))
|
||||
.forEach(appt -> optionsByDate.get(targetDate).put("appt:" + appt.getId(),
|
||||
buildOption(appt, evaluateAvailability(appt, targetDate, now))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 周期模式按每个实际周窗口逐批推演,避免20天预览只展示当前或下一批次。
|
||||
*/
|
||||
private void addCyclePreviewOptions(Long mchId, LocalDateTime now, LocalDate startDate, LocalDate endDate,
|
||||
Map<LocalDate, LinkedHashMap<String, HardApptOptionVo>> optionsByDate) {
|
||||
for (HardApptCycleRuleVo rule : cycleRuleService.listEnabledConfigurations(mchId)) {
|
||||
try {
|
||||
HardApptCycleRuleCalculator.validate(rule);
|
||||
} catch (RuntimeException ignored) {
|
||||
continue;
|
||||
}
|
||||
if (rule.getItemList() == null || rule.getItemList().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int lookBehindDays = rule.getTargetStartOffsetDays() + rule.getTargetDayCount() + 14;
|
||||
LocalDate monday = startDate.minusDays(lookBehindDays)
|
||||
.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||
LocalDate lastMonday = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
|
||||
while (!monday.isAfter(lastMonday)) {
|
||||
HardApptCycleRuleCalculator.CycleWindow window =
|
||||
HardApptCycleRuleCalculator.windowForMonday(rule, monday);
|
||||
LocalDate targetStart = window.endDateTime().toLocalDate()
|
||||
.plusDays(rule.getTargetStartOffsetDays());
|
||||
Availability availability = cyclePreviewAvailability(rule, window, now);
|
||||
for (HardApptCycleRuleItemVo item : rule.getItemList()) {
|
||||
if (item.getTargetDayOffset() == null || item.getTargetDayOffset() < 0
|
||||
|| item.getTargetDayOffset() >= rule.getTargetDayCount()) {
|
||||
continue;
|
||||
}
|
||||
HardHourTypeEnum hourType = HardHourTypeEnum.valueOfByType(item.getHourType());
|
||||
LocalDate targetDate = targetStart.plusDays(item.getTargetDayOffset());
|
||||
if (hourType == null || targetDate.isBefore(startDate) || targetDate.isAfter(endDate)) {
|
||||
continue;
|
||||
}
|
||||
HardApptOptionVo option = buildCycleOption(hourType, availability);
|
||||
option.setApptRuleId(rule.getId());
|
||||
putPreviewOption(optionsByDate.get(targetDate),
|
||||
"cycle:" + rule.getId() + ":" + hourType.getType(), option, now);
|
||||
}
|
||||
monday = monday.plusWeeks(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一规则覆盖超过7天时,相邻周批次可能落到同一日期;优先展示当前可订,其次展示最近将开放的批次。
|
||||
*/
|
||||
private static void putPreviewOption(Map<String, HardApptOptionVo> options, String key,
|
||||
HardApptOptionVo candidate, LocalDateTime now) {
|
||||
HardApptOptionVo existing = options.get(key);
|
||||
if (existing == null || previewPriority(candidate, now) > previewPriority(existing, now)
|
||||
|| previewPriority(candidate, now) == previewPriority(existing, now)
|
||||
&& candidate.getReserveStartDateTime() != null && existing.getReserveStartDateTime() != null
|
||||
&& candidate.getReserveStartDateTime().isBefore(existing.getReserveStartDateTime())) {
|
||||
options.put(key, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
private static int previewPriority(HardApptOptionVo option, LocalDateTime now) {
|
||||
if (Boolean.TRUE.equals(option.getAvailable())) {
|
||||
return 3;
|
||||
}
|
||||
if (option.getReserveStartDateTime() != null && option.getReserveStartDateTime().isAfter(now)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static Availability cyclePreviewAvailability(HardApptCycleRuleVo rule,
|
||||
HardApptCycleRuleCalculator.CycleWindow window,
|
||||
LocalDateTime now) {
|
||||
LocalDateTime start = window.startDateTime();
|
||||
LocalDateTime end = window.endDateTime();
|
||||
if (!now.isBefore(start) && !now.isAfter(end)) {
|
||||
return Availability.available(start, end);
|
||||
}
|
||||
if (now.isBefore(start)) {
|
||||
String reason = "周" + weekdayLabel(rule.getOpenWeekday())
|
||||
+ rule.getOpenTime().format(TIME_FORMATTER) + "开放";
|
||||
return Availability.unavailable(reason, start, end);
|
||||
}
|
||||
return Availability.unavailable("本期预约已截止", start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReservationValidation validateReservation(Long mchId, Long apptId, Long apptRuleId, String hourType,
|
||||
LocalDate apptDate, String legacyApptTimeType) {
|
||||
@@ -152,11 +309,15 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
|
||||
ReservationValidation validateReservation(Long mchId, Long apptId, Long apptRuleId, String hourType, LocalDate apptDate,
|
||||
String legacyApptTimeType, LocalDateTime now) {
|
||||
if (ObjectUtil.isEmpty(mchId)) {
|
||||
throw new RuntimeException("预约信息不完整");
|
||||
HardApptReserveModeEnum merchantMode = resolveMerchantMode(mchId);
|
||||
if (merchantMode == HardApptReserveModeEnum.WEEKLY_CYCLE) {
|
||||
if (apptRuleId == null) {
|
||||
throw new RuntimeException("周期预约规则不能为空");
|
||||
}
|
||||
return validateCycleReservation(mchId, apptRuleId, hourType, apptDate, now);
|
||||
}
|
||||
if (apptRuleId != null) {
|
||||
return validateCycleReservation(mchId, apptRuleId, hourType, apptDate, now);
|
||||
throw new RuntimeException("当前商户使用动态时间窗,不能提交周期预约规则");
|
||||
}
|
||||
if (ObjectUtil.isEmpty(apptId)) {
|
||||
throw new RuntimeException("动态预约餐段不能为空");
|
||||
@@ -174,27 +335,22 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
}
|
||||
|
||||
HardApptReserveModeEnum reserveMode = resolveMode(appt);
|
||||
if (reserveMode == null) {
|
||||
throw new RuntimeException("预约规则模式无效");
|
||||
if (reserveMode != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
throw new RuntimeException("预约餐段与商户当前模式不匹配");
|
||||
}
|
||||
|
||||
LocalDate targetDate;
|
||||
if (reserveMode != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
throw new RuntimeException("周期预约请升级客户端后重试");
|
||||
} else {
|
||||
targetDate = resolveLegacyApptDate(apptDate, legacyApptTimeType, now.toLocalDate());
|
||||
long dayOffset = ChronoUnit.DAYS.between(now.toLocalDate(), targetDate);
|
||||
HardApptTimeTypeEnum dateType = HardApptTimeTypeEnum.valueOfByDay((int) dayOffset);
|
||||
if (dateType == null || !HardApptTimeTypeEnum.isReservationDateType(dateType.getType())) {
|
||||
throw new RuntimeException("该预约日期不在可选范围内");
|
||||
}
|
||||
long mappingCount = hardApptTimeService.count(new LambdaQueryWrapper<HardApptTimeEntity>()
|
||||
.eq(HardApptTimeEntity::getMchId, mchId)
|
||||
.eq(HardApptTimeEntity::getApptId, apptId)
|
||||
.eq(HardApptTimeEntity::getType, dateType.getType()));
|
||||
if (mappingCount == 0) {
|
||||
throw new RuntimeException("该日期未配置此预约餐段");
|
||||
}
|
||||
LocalDate targetDate = resolveLegacyApptDate(apptDate, legacyApptTimeType, now.toLocalDate());
|
||||
long dayOffset = ChronoUnit.DAYS.between(now.toLocalDate(), targetDate);
|
||||
HardApptTimeTypeEnum dateType = HardApptTimeTypeEnum.valueOfByDay((int) dayOffset);
|
||||
if (dateType == null || !HardApptTimeTypeEnum.isReservationDateType(dateType.getType())) {
|
||||
throw new RuntimeException("该预约日期不在可选范围内");
|
||||
}
|
||||
long mappingCount = hardApptTimeService.count(new LambdaQueryWrapper<HardApptTimeEntity>()
|
||||
.eq(HardApptTimeEntity::getMchId, mchId)
|
||||
.eq(HardApptTimeEntity::getApptId, apptId)
|
||||
.eq(HardApptTimeEntity::getType, dateType.getType()));
|
||||
if (mappingCount == 0) {
|
||||
throw new RuntimeException("该日期未配置此预约餐段");
|
||||
}
|
||||
|
||||
Availability availability = evaluateAvailability(appt, targetDate, now);
|
||||
@@ -320,14 +476,19 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
return Availability.unavailable(reason, start, end);
|
||||
}
|
||||
|
||||
private List<HardApptEntity> findEnabledAppts(Long mchId) {
|
||||
private List<HardApptEntity> findEnabledAppts(Long mchId, HardApptReserveModeEnum reserveMode) {
|
||||
return hardApptService.list(new LambdaQueryWrapper<HardApptEntity>()
|
||||
.eq(HardApptEntity::getMchId, mchId)
|
||||
.eq(HardApptEntity::getStatus, YesNoEnum.YES.getCode())
|
||||
.eq(HardApptEntity::getReserveMode, reserveMode.getType())
|
||||
.orderByAsc(HardApptEntity::getHourType)
|
||||
.orderByDesc(HardApptEntity::getSort));
|
||||
}
|
||||
|
||||
private HardApptReserveModeEnum resolveMerchantMode(Long mchId) {
|
||||
return merchantModeService.resolve(mchId).reserveMode();
|
||||
}
|
||||
|
||||
private Map<String, Set<Long>> groupMappingsByDateType(Long mchId, List<HardApptEntity> appts) {
|
||||
if (apptListIsEmpty(appts)) {
|
||||
return Map.of();
|
||||
@@ -359,12 +520,6 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
|
||||
return HardApptReserveModeEnum.valueOfByType(appt.getReserveMode());
|
||||
}
|
||||
|
||||
private static void validateMerchantId(Long mchId) {
|
||||
if (ObjectUtil.isEmpty(mchId)) {
|
||||
throw new RuntimeException("商户ID不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDateTime nowInShanghai() {
|
||||
return ZonedDateTime.now(RESERVATION_ZONE).toLocalDateTime();
|
||||
}
|
||||
|
||||
+33
-14
@@ -14,9 +14,8 @@ import com.bsd.pjcloud.hard.api.entity.HardApptTimeEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptVo;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptTimeMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -38,7 +37,7 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
|
||||
private final HardApptTimeMapper hardApptTimeMapper;
|
||||
|
||||
private final RemoteMchService remoteMchService;
|
||||
private final HardApptMerchantModeService merchantModeService;
|
||||
|
||||
/**
|
||||
* 新增
|
||||
@@ -46,16 +45,18 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(HardApptDto dto) {
|
||||
HardApptMerchantModeService.MerchantModeContext merchantContext = requireTimeWindowMerchant(dto.getMchId());
|
||||
if (ObjectUtil.isNotEmpty(dto.getReserveMode())
|
||||
&& HardApptReserveModeEnum.valueOfByType(dto.getReserveMode()) != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
throw new RuntimeException("动态餐段只能使用动态时间窗模式");
|
||||
}
|
||||
dto.setReserveMode(HardApptReserveModeEnum.TIME_WINDOW.getType());
|
||||
normalizeAndValidateRule(dto, HardApptReserveModeEnum.TIME_WINDOW.getType());
|
||||
// title不能重复
|
||||
if (existTitle(dto.getTitle(), dto.getMchId(), null)) {
|
||||
throw new RuntimeException("标题不能重复");
|
||||
}
|
||||
// 查询商户id
|
||||
MchManageVo mchDeatil = remoteMchService.getById(dto.getMchId()).getData();
|
||||
if (ObjectUtil.isNotEmpty(mchDeatil)) {
|
||||
dto.setDeptId(mchDeatil.getDeptId());
|
||||
}
|
||||
dto.setDeptId(merchantContext.merchant().getDeptId());
|
||||
baseMapper.insert(dto);
|
||||
createApptTime(dto, dto.getReserveTimeList());
|
||||
return dto.getId();
|
||||
@@ -71,16 +72,19 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("时间预约不存在");
|
||||
}
|
||||
normalizeAndValidateRule(dto, existing.getReserveMode());
|
||||
if (ObjectUtil.notEqual(existing.getMchId(), dto.getMchId())) {
|
||||
throw new RuntimeException("预约餐段不允许更换商户");
|
||||
}
|
||||
if (HardApptReserveModeEnum.valueOfByType(existing.getReserveMode()) != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
throw new RuntimeException("周期预约餐段由周期预约规则维护");
|
||||
}
|
||||
HardApptMerchantModeService.MerchantModeContext merchantContext = requireTimeWindowMerchant(dto.getMchId());
|
||||
normalizeAndValidateRule(dto, HardApptReserveModeEnum.TIME_WINDOW.getType());
|
||||
// title不能重复
|
||||
if (existTitle(dto.getTitle(), dto.getMchId(), dto.getId())) {
|
||||
throw new RuntimeException("标题不能重复");
|
||||
}
|
||||
// 查询商户id
|
||||
MchManageVo mchDeatil = remoteMchService.getById(dto.getMchId()).getData();
|
||||
if (ObjectUtil.isNotEmpty(mchDeatil)) {
|
||||
dto.setDeptId(mchDeatil.getDeptId());
|
||||
}
|
||||
dto.setDeptId(merchantContext.merchant().getDeptId());
|
||||
baseMapper.updateById(dto);
|
||||
createApptTime(dto, dto.getReserveTimeList());
|
||||
return dto.getId();
|
||||
@@ -104,7 +108,9 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
@Override
|
||||
public Page<HardApptVo> scopePage(Page page, HardApptDto dto, DataScope of) {
|
||||
LambdaQueryWrapper<HardApptEntity> wrapper = new LambdaQueryWrapper<HardApptEntity>()
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getMchId()), HardApptEntity::getMchId, dto.getMchId())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getDeptId()), HardApptEntity::getDeptId, dto.getDeptId())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getReserveMode()), HardApptEntity::getReserveMode, dto.getReserveMode())
|
||||
.like(ObjectUtil.isNotEmpty(dto.getTitle()), HardApptEntity::getTitle, dto.getTitle())
|
||||
.orderByAsc(HardApptEntity::getHourType);
|
||||
Page pageList = baseMapper.selectPageByScope(page, wrapper, of);
|
||||
@@ -121,7 +127,9 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
@Override
|
||||
public List<HardApptVo> scopeList(HardApptDto dto, DataScope of) {
|
||||
LambdaQueryWrapper<HardApptEntity> wrapper = new LambdaQueryWrapper<HardApptEntity>()
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getMchId()), HardApptEntity::getMchId, dto.getMchId())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getDeptId()), HardApptEntity::getDeptId, dto.getDeptId())
|
||||
.eq(ObjectUtil.isNotEmpty(dto.getReserveMode()), HardApptEntity::getReserveMode, dto.getReserveMode())
|
||||
.like(ObjectUtil.isNotEmpty(dto.getTitle()), HardApptEntity::getTitle, dto.getTitle())
|
||||
.orderByAsc(HardApptEntity::getHourType);
|
||||
List<HardApptEntity> list = baseMapper.selectListByScope(wrapper, of);
|
||||
@@ -220,6 +228,17 @@ public class HardApptServiceImpl extends ServiceImpl<HardApptMapper, HardApptEnt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 餐段时间窗只在商户当前选择动态模式时允许维护,切换模式不会删除原有数据。
|
||||
*/
|
||||
private HardApptMerchantModeService.MerchantModeContext requireTimeWindowMerchant(Long mchId) {
|
||||
HardApptMerchantModeService.MerchantModeContext context = merchantModeService.resolve(mchId);
|
||||
if (context.reserveMode() != HardApptReserveModeEnum.TIME_WINDOW) {
|
||||
throw new RuntimeException("当前商户使用按周集中预约,请在周期预约规则中配置");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private boolean existTitle(String title, Long mchId, Long id) {
|
||||
Long count = baseMapper.selectCount(new LambdaQueryWrapper<HardApptEntity>()
|
||||
.eq(ObjectUtil.isNotEmpty(title), HardApptEntity::getTitle, title)
|
||||
|
||||
+22
-5
@@ -1,13 +1,14 @@
|
||||
package com.bsd.pjcloud.hard.service.impl;
|
||||
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptCycleRuleDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardApptCycleRuleItemDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.HardApptCycleRuleEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.HardApptCycleRuleItemEntity;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptCycleRuleItemMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.HardApptCycleRuleMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -53,18 +54,33 @@ class HardApptCycleRuleServiceImplTest {
|
||||
assertTrue(exception.getMessage().contains("状态无效"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期配置允许稀疏保存,未提交的偏移表示该日不开放任何餐段。
|
||||
*/
|
||||
@Test
|
||||
void sparseTargetDayOffsetsAreAccepted() {
|
||||
Fixture fixture = fixture(YesNoEnum.NO.getCode());
|
||||
fixture.candidate().getItemList().get(0).setTargetDayOffset(3);
|
||||
|
||||
assertDoesNotThrow(() -> fixture.service().create(fixture.candidate()));
|
||||
}
|
||||
|
||||
private Fixture fixture(String candidateStatus) {
|
||||
HardApptCycleRuleMapper ruleMapper = mock(HardApptCycleRuleMapper.class);
|
||||
HardApptCycleRuleItemMapper itemMapper = mock(HardApptCycleRuleItemMapper.class);
|
||||
RemoteMchService remoteMchService = mock(RemoteMchService.class);
|
||||
TestableCycleRuleService service = new TestableCycleRuleService(itemMapper, remoteMchService);
|
||||
HardApptMerchantModeService merchantModeService = mock(HardApptMerchantModeService.class);
|
||||
TestableCycleRuleService service = new TestableCycleRuleService(
|
||||
itemMapper, remoteMchService, merchantModeService);
|
||||
service.useMapper(ruleMapper);
|
||||
|
||||
when(ruleMapper.selectCount(any())).thenReturn(0L);
|
||||
|
||||
MchManageVo merchant = new MchManageVo();
|
||||
merchant.setDeptId(20L);
|
||||
when(remoteMchService.getById(10L)).thenReturn(R.ok(merchant));
|
||||
when(merchantModeService.resolve(10L)).thenReturn(
|
||||
new HardApptMerchantModeService.MerchantModeContext(
|
||||
merchant, HardApptReserveModeEnum.WEEKLY_CYCLE));
|
||||
|
||||
HardApptCycleRuleEntity existingRule = new HardApptCycleRuleEntity();
|
||||
existingRule.setId(200L);
|
||||
@@ -105,8 +121,9 @@ class HardApptCycleRuleServiceImplTest {
|
||||
private static final class TestableCycleRuleService extends HardApptCycleRuleServiceImpl {
|
||||
|
||||
private TestableCycleRuleService(HardApptCycleRuleItemMapper itemMapper,
|
||||
RemoteMchService remoteMchService) {
|
||||
super(itemMapper, remoteMchService);
|
||||
RemoteMchService remoteMchService,
|
||||
HardApptMerchantModeService merchantModeService) {
|
||||
super(itemMapper, remoteMchService, merchantModeService);
|
||||
}
|
||||
|
||||
private void useMapper(HardApptCycleRuleMapper mapper) {
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.bsd.pjcloud.hard.service.impl;
|
||||
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardApptReserveModeEnum;
|
||||
import com.bsd.pjcloud.wallet.api.entity.mch.MchManageEntity;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class HardApptMerchantModeServiceImplTest {
|
||||
|
||||
@Test
|
||||
void missingNullAndBlankModesDefaultToTimeWindow() {
|
||||
RemoteMchService remoteMchService = mock(RemoteMchService.class);
|
||||
HardApptMerchantModeServiceImpl service = new HardApptMerchantModeServiceImpl(remoteMchService);
|
||||
MchManageVo merchant = new MchManageVo();
|
||||
when(remoteMchService.getById(10L)).thenReturn(R.ok(merchant));
|
||||
|
||||
assertEquals(HardApptReserveModeEnum.TIME_WINDOW, service.resolve(10L).reserveMode());
|
||||
|
||||
MchManageEntity.TableReservationConf conf = new MchManageEntity.TableReservationConf();
|
||||
conf.setReserveMode(null);
|
||||
merchant.setTableReservationConf(conf);
|
||||
assertEquals(HardApptReserveModeEnum.TIME_WINDOW, service.resolve(10L).reserveMode());
|
||||
|
||||
conf.setReserveMode(" ");
|
||||
assertEquals(HardApptReserveModeEnum.TIME_WINDOW, service.resolve(10L).reserveMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitWeeklyModeIsResolved() {
|
||||
RemoteMchService remoteMchService = mock(RemoteMchService.class);
|
||||
HardApptMerchantModeServiceImpl service = new HardApptMerchantModeServiceImpl(remoteMchService);
|
||||
MchManageVo merchant = new MchManageVo();
|
||||
MchManageEntity.TableReservationConf conf = new MchManageEntity.TableReservationConf();
|
||||
conf.setReserveMode(HardApptReserveModeEnum.WEEKLY_CYCLE.getType());
|
||||
merchant.setTableReservationConf(conf);
|
||||
when(remoteMchService.getById(10L)).thenReturn(R.ok(merchant));
|
||||
|
||||
assertEquals(HardApptReserveModeEnum.WEEKLY_CYCLE, service.resolve(10L).reserveMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidModeAndMerchantFailuresAreRejected() {
|
||||
RemoteMchService remoteMchService = mock(RemoteMchService.class);
|
||||
HardApptMerchantModeServiceImpl service = new HardApptMerchantModeServiceImpl(remoteMchService);
|
||||
MchManageVo merchant = new MchManageVo();
|
||||
MchManageEntity.TableReservationConf conf = new MchManageEntity.TableReservationConf();
|
||||
conf.setReserveMode("UNKNOWN");
|
||||
merchant.setTableReservationConf(conf);
|
||||
when(remoteMchService.getById(10L)).thenReturn(R.ok(merchant));
|
||||
when(remoteMchService.getById(20L)).thenReturn(R.ok(null));
|
||||
when(remoteMchService.getById(30L)).thenReturn(R.failed("钱包服务不可用"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.resolve(10L));
|
||||
assertThrows(RuntimeException.class, () -> service.resolve(20L));
|
||||
assertThrows(RuntimeException.class, () -> service.resolve(30L));
|
||||
}
|
||||
}
|
||||
+40
-12
@@ -9,11 +9,13 @@ import com.bsd.pjcloud.hard.api.vo.HardApptCycleRuleVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptDateOptionsVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.HardApptTimeCascadeVo;
|
||||
import com.bsd.pjcloud.hard.service.HardApptCycleRuleService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptMerchantModeService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptService;
|
||||
import com.bsd.pjcloud.hard.service.HardApptTimeService;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.bsd.pjcloud.hard.service.support.HardApptCycleRuleCalculator;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -136,7 +138,7 @@ class HardApptReservationServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void v3ListCanContainDynamicAndWeeklyModesTogether() {
|
||||
void v3ListOnlyReturnsDynamicRulesForTimeWindowMerchant() {
|
||||
HardApptService apptService = mock(HardApptService.class);
|
||||
HardApptTimeService timeService = mock(HardApptTimeService.class);
|
||||
HardApptEntity dynamic = timeWindowAppt();
|
||||
@@ -156,15 +158,15 @@ class HardApptReservationServiceImplTest {
|
||||
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
when(cycleRuleService.listEnabledConfigurations(10L)).thenReturn(List.of(cycleRule));
|
||||
List<HardApptDateOptionsVo> result = new HardApptReservationServiceImpl(apptService, timeService, cycleRuleService)
|
||||
List<HardApptDateOptionsVo> result = new HardApptReservationServiceImpl(
|
||||
apptService, timeService, cycleRuleService,
|
||||
merchantModeService(HardApptReserveModeEnum.TIME_WINDOW))
|
||||
.listOptions(10L, LocalDateTime.of(2026, 7, 24, 17, 0));
|
||||
|
||||
assertEquals(8, result.size());
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1, result.get(0).getApptList().size());
|
||||
assertEquals(7, result.stream()
|
||||
.filter(day -> day.getApptList().stream()
|
||||
.anyMatch(option -> HardApptReserveModeEnum.WEEKLY_CYCLE.getType().equals(option.getReserveMode())))
|
||||
.count());
|
||||
assertTrue(result.stream().flatMap(day -> day.getApptList().stream())
|
||||
.allMatch(option -> HardApptReserveModeEnum.TIME_WINDOW.getType().equals(option.getReserveMode())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -179,7 +181,9 @@ class HardApptReservationServiceImplTest {
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
when(cycleRuleService.listEnabledConfigurations(10L)).thenReturn(List.of(rule));
|
||||
|
||||
List<HardApptDateOptionsVo> result = new HardApptReservationServiceImpl(apptService, timeService, cycleRuleService)
|
||||
List<HardApptDateOptionsVo> result = new HardApptReservationServiceImpl(
|
||||
apptService, timeService, cycleRuleService,
|
||||
merchantModeService(HardApptReserveModeEnum.WEEKLY_CYCLE))
|
||||
.listOptions(10L, LocalDateTime.of(2026, 7, 24, 18, 0));
|
||||
|
||||
assertEquals(List.of(LocalDate.of(2026, 7, 28), LocalDate.of(2026, 7, 30)),
|
||||
@@ -204,19 +208,34 @@ class HardApptReservationServiceImplTest {
|
||||
mapping.setApptId(1L);
|
||||
mapping.setType(HardApptTimeTypeEnum.TODAY.getType());
|
||||
when(apptService.list(org.mockito.ArgumentMatchers.<Wrapper<HardApptEntity>>any()))
|
||||
.thenReturn(List.of(dynamic, weekly));
|
||||
.thenReturn(List.of(dynamic));
|
||||
when(timeService.list(org.mockito.ArgumentMatchers.<Wrapper<HardApptTimeEntity>>any()))
|
||||
.thenReturn(List.of(mapping));
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
|
||||
List<HardApptTimeCascadeVo> result =
|
||||
new HardApptReservationServiceImpl(apptService, timeService, cycleRuleService)
|
||||
new HardApptReservationServiceImpl(
|
||||
apptService, timeService, cycleRuleService,
|
||||
merchantModeService(HardApptReserveModeEnum.TIME_WINDOW))
|
||||
.listLegacyOptions(10L);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(List.of(1L), result.get(0).getApptList().stream().map(HardApptEntity::getId).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyListsAreEmptyForWeeklyMerchant() {
|
||||
HardApptService apptService = mock(HardApptService.class);
|
||||
HardApptTimeService timeService = mock(HardApptTimeService.class);
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
HardApptReservationServiceImpl service = new HardApptReservationServiceImpl(
|
||||
apptService, timeService, cycleRuleService,
|
||||
merchantModeService(HardApptReserveModeEnum.WEEKLY_CYCLE));
|
||||
|
||||
assertTrue(service.listLegacyFlatOptions(10L).isEmpty());
|
||||
assertTrue(service.listLegacyOptions(10L).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationRejectsWrongMerchantDisabledRuleTamperedDateAndMissingWeeklyDate() {
|
||||
HardApptEntity dynamic = timeWindowAppt();
|
||||
@@ -283,7 +302,7 @@ class HardApptReservationServiceImplTest {
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.validateReservation(10L, null, null, "1", targetDate, null, now));
|
||||
assertTrue(exception.getMessage().contains("动态预约餐段不能为空"));
|
||||
assertTrue(exception.getMessage().contains("周期预约规则不能为空"));
|
||||
assertEquals(100L, service.validateReservation(10L, null, 100L, "1", targetDate, null, now).apptRuleId());
|
||||
}
|
||||
|
||||
@@ -311,10 +330,19 @@ class HardApptReservationServiceImplTest {
|
||||
HardApptService apptService = mock(HardApptService.class);
|
||||
HardApptTimeService timeService = mock(HardApptTimeService.class);
|
||||
HardApptCycleRuleService cycleRuleService = mock(HardApptCycleRuleService.class);
|
||||
HardApptMerchantModeService merchantModeService = merchantModeService(
|
||||
HardApptReserveModeEnum.valueOfByType(appt.getReserveMode()));
|
||||
when(apptService.getById(appt.getId())).thenReturn(appt);
|
||||
when(timeService.count(any())).thenReturn(mappingCount);
|
||||
when(cycleRuleService.listEnabledConfigurations(appt.getMchId())).thenReturn(rules);
|
||||
return new HardApptReservationServiceImpl(apptService, timeService, cycleRuleService);
|
||||
return new HardApptReservationServiceImpl(apptService, timeService, cycleRuleService, merchantModeService);
|
||||
}
|
||||
|
||||
private HardApptMerchantModeService merchantModeService(HardApptReserveModeEnum reserveMode) {
|
||||
HardApptMerchantModeService service = mock(HardApptMerchantModeService.class);
|
||||
when(service.resolve(any())).thenReturn(
|
||||
new HardApptMerchantModeService.MerchantModeContext(new MchManageVo(), reserveMode));
|
||||
return service;
|
||||
}
|
||||
|
||||
private HardApptCycleRuleVo cycleRule(Long id, Long mchId,
|
||||
|
||||
+5
@@ -231,6 +231,11 @@ public class MchManageEntity extends Model<MchManageEntity> {
|
||||
@Data
|
||||
public static class TableReservationConf {
|
||||
|
||||
/**
|
||||
* 商户当前使用的预约模式。历史 JSON 缺少该属性时由字段默认值兼容为动态时间窗。
|
||||
*/
|
||||
private String reserveMode = "TIME_WINDOW";
|
||||
|
||||
/**
|
||||
* 订餐购物车是否只允许有一个商品
|
||||
*/
|
||||
|
||||
+23
-1
@@ -107,6 +107,7 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(MchManageEntity dto) {
|
||||
normalizeTableReservationConf(dto);
|
||||
if (ObjectUtil.isEmpty(dto.getParentId())) dto.setParentId(-1L);
|
||||
if (existName(dto.getName(), dto.getTypes(), null)) {
|
||||
throw new RuntimeException("商户名不能重复");
|
||||
@@ -138,6 +139,7 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
beforeInvocation = true
|
||||
)
|
||||
public void edit(MchManageEntity dto) {
|
||||
normalizeTableReservationConf(dto);
|
||||
if (existName(dto.getName(), dto.getTypes(), dto.getId())) {
|
||||
throw new RuntimeException("商户名不能重复");
|
||||
}
|
||||
@@ -176,6 +178,26 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
// walletThirdZyService.syncAllMch(dto.getId(), Boolean.FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只规范化本次明确提交的 JSON 配置,避免其他局部更新接口覆盖数据库中的原配置。
|
||||
*/
|
||||
private void normalizeTableReservationConf(MchManageEntity dto) {
|
||||
MchManageEntity.TableReservationConf conf = dto.getTableReservationConf();
|
||||
if (conf == null) {
|
||||
return;
|
||||
}
|
||||
String reserveMode = conf.getReserveMode();
|
||||
if (reserveMode == null || reserveMode.trim().isEmpty()) {
|
||||
conf.setReserveMode("TIME_WINDOW");
|
||||
return;
|
||||
}
|
||||
reserveMode = reserveMode.trim();
|
||||
if (!"TIME_WINDOW".equals(reserveMode) && !"WEEKLY_CYCLE".equals(reserveMode)) {
|
||||
throw new RuntimeException("预约时间模式无效");
|
||||
}
|
||||
conf.setReserveMode(reserveMode);
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
value = CacheConstants.WALLET_VIEWS_CANTEEN_MCH,
|
||||
allEntries = true,
|
||||
@@ -628,4 +650,4 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
private MchManageServiceImpl getSelf() {
|
||||
return SpringUtil.getBean(getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user