diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/ReservationMenuCancelDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/ReservationMenuCancelDto.java new file mode 100644 index 000000000..881750b87 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/ReservationMenuCancelDto.java @@ -0,0 +1,27 @@ +package com.bsd.pjcloud.hard.api.dto.hard; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 预约菜单单笔核销参数。 + * + * 核销目标只能由订单 ID 确定,禁止客户端附带日期、餐段、设备或识别身份参与订单选择。 + */ +@Data +public class ReservationMenuCancelDto { + + @NotNull(message = "预约订单ID不能为空") + @Schema(description = "待核销预约订单ID", requiredMode = Schema.RequiredMode.REQUIRED) + private Long orderId; + + /** + * Spring 默认可能忽略未知 JSON 字段,这里显式拒绝,保证请求体只能包含 orderId。 + */ + @JsonAnySetter + public void rejectUnknownField(String fieldName, Object value) { + throw new IllegalArgumentException("预约核销接口不支持参数:" + fieldName); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/android/ReservationMenuQueryVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/android/ReservationMenuQueryVo.java index 2d3941e4c..7ba52c603 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/android/ReservationMenuQueryVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/android/ReservationMenuQueryVo.java @@ -39,5 +39,8 @@ public class ReservationMenuQueryVo { private String mealTitle; private List items; + + /** 同一日期餐段可能合并多笔预约,终端按订单逐笔确认核销。 */ + private List orderIds; } } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java index c60b75cef..618678b23 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java @@ -178,6 +178,13 @@ public class HardApiController { return R.ok(reservationMenuQueryService.query(dto)); } + @PostMapping("/reservation-menu/cancel") + @Operation(summary = "按订单ID核销预约订单") + @Idempotent(expireTime = 1, key = "'/hard-api/ad/reservation-menu/cancel:' + #dto.getOrderId()", info = "正在核销中,请勿重复操作") + public R cancelReservationMenu(@Valid @RequestBody ReservationMenuCancelDto dto) { + return R.ok(reservationMenuQueryService.cancel(dto)); + } + @GetMapping("/chart-latitude") @Operation(summary = "统计每天、每周、每月、每年的交易人数和次数") public R chartLatitude(@RequestParam Long merchantId) { diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/ReservationMenuQueryService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/ReservationMenuQueryService.java index 842098e14..d2c2834b4 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/ReservationMenuQueryService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/ReservationMenuQueryService.java @@ -1,5 +1,6 @@ package com.bsd.pjcloud.hard.service; +import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto; import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto; import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo; @@ -9,4 +10,9 @@ import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo; public interface ReservationMenuQueryService { ReservationMenuQueryVo query(ReservationMenuQueryDto dto); + + /** + * 仅按订单 ID 核销一笔预约订单。 + */ + Boolean cancel(ReservationMenuCancelDto dto); } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java index 14d2b51de..4aa2feec3 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java @@ -13,9 +13,12 @@ import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum; import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum; +import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto; +import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto; import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity; +import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.vo.android.HardAdUserVo; import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo; import com.bsd.pjcloud.hard.api.vo.order.HardCarVo; @@ -73,6 +76,33 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ return aggregate(recognizedUser, terminalMerchant, orders); } + @Override + public Boolean cancel(ReservationMenuCancelDto dto) { + HardOrderEntity order = hardOrderService.getById(dto.getOrderId()); + if (order == null) throw new RuntimeException("预约订单不存在"); + if (!Objects.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType()) || !Objects.equals(order.getOrderStatus(), HardOrderStatusEnum.PAY.getType())) { + throw new RuntimeException("该预约订单当前不可核销"); + } + if (!isCancelableRefundStatus(order.getRefundStatus())) { + throw new RuntimeException("该预约订单正在退款或已退款,不能核销"); + } + if (order.getApptUseDate() == null || order.getApptUseDate().isBefore(LocalDate.now(BUSINESS_ZONE))) { + throw new RuntimeException("该预约订单已失效"); + } + + HandCancelCodeDto cancelDto = new HandCancelCodeDto(); + cancelDto.setOrderId(order.getId()); + // 当前接口契约只接收订单 ID;-1 沿用商户端和后台手动核销的非终端设备标识。 + cancelDto.setHardwareId(-1L); + return hardOrderService.handCancelCode(cancelDto); + } + + private static boolean isCancelableRefundStatus(String refundStatus) { + return StrUtil.isBlank(refundStatus) + || Objects.equals(refundStatus, HardRefundStatusEnum.NONE.getStatus()) + || Objects.equals(refundStatus, HardRefundStatusEnum.REJECT.getStatus()); + } + private TerminalMerchant resolveTerminalMerchant(Long hardId) { HardManagerEntity hard = hardManagerService.getById(hardId); if (hard == null) { @@ -126,6 +156,7 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ String hourType = resolveHourType(order); LinkedHashMap meals = grouped.computeIfAbsent(order.getApptUseDate(), ignored -> new LinkedHashMap<>()); MealAccumulator meal = meals.computeIfAbsent(hourType, ignored -> new MealAccumulator(hourType, resolveMealTitle(order, hourType))); + if (order.getId() != null) meal.orderIds().add(order.getId()); if (order.getCarList() == null) continue; for (HardCarVo car : order.getCarList()) { if (car != null && StrUtil.isNotBlank(car.getProductName())) { @@ -160,6 +191,7 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ meal.setHourType(source.hourType()); meal.setMealTitle(source.mealTitle()); meal.setItems(new ArrayList<>(source.items())); + meal.setOrderIds(new ArrayList<>(source.orderIds())); return meal; } @@ -199,10 +231,11 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ record TerminalMerchant(MchManageVo merchant) { } - private record MealAccumulator(String hourType, String mealTitle, LinkedHashSet items) { + private record MealAccumulator(String hourType, String mealTitle, LinkedHashSet items, + LinkedHashSet orderIds) { private MealAccumulator(String hourType, String mealTitle) { - this(hourType, mealTitle, new LinkedHashSet<>()); + this(hourType, mealTitle, new LinkedHashSet<>(), new LinkedHashSet<>()); } } } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index 33107a757..0b3ba2fb1 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -5,6 +5,8 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; +import com.baomidou.lock.LockInfo; +import com.baomidou.lock.LockTemplate; import com.baomidou.lock.annotation.Lock4j; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -150,6 +152,8 @@ import java.util.stream.Collectors; public class HardOrderServiceImpl extends ServiceImpl implements HardOrderService { static private final String TAG = "订单"; + private static final long RESERVATION_SLOT_LOCK_EXPIRE_MILLIS = 60_000L; + private static final long RESERVATION_SLOT_LOCK_WAIT_MILLIS = 3_000L; private final HardManagerService hardManagerService; private final HardCarService hardCarService; @@ -189,6 +193,7 @@ public class HardOrderServiceImpl extends ServiceImpl inventorySnapshot = validateReserveBatchInventory(sourceParentCar.getMerchantId(), sourceCarList, dinerUserIds.size()); String subject = buildReserveOrderSubject(dto.getCarParentId(), sourceCarList); + // 在所有远程预检完成后再占用餐段锁,缩短并发请求等待时间;锁仍覆盖查询、落单与事务提交。 + validateSingleReservationOnlyPerMeal(merchant, dinerUserIds, reservation); List createdOrders = new ArrayList<>(); try { @@ -1067,6 +1074,83 @@ public class HardOrderServiceImpl extends ServiceImpl dinerUserIds, + HardApptReservationService.ReservationValidation reservation) { + MchManageEntity.TableReservationConf conf = merchant.getTableReservationConf(); + if (conf == null || !Boolean.TRUE.equals(conf.getSingleReservationOnlyPerMeal())) { + return; + } + List locks = acquireReservationSlotLocks(merchant.getId(), dinerUserIds, reservation); + try { + registerReservationSlotLockRelease(locks); + } catch (RuntimeException exception) { + releaseReservationSlotLocks(locks); + throw exception; + } + long existingCount = baseMapper.selectCount(new LambdaQueryWrapper() + .eq(HardOrderEntity::getMerchantId, merchant.getId()) + .in(HardOrderEntity::getUserId, dinerUserIds) + .eq(HardOrderEntity::getSysType, HardTypeEnum.CANTEEN.getType()) + .eq(HardOrderEntity::getOrderType, HardOrderTypeEnum.RESERVE.getType()) + .eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType()) + .eq(HardOrderEntity::getApptUseDate, reservation.apptDate()) + .eq(HardOrderEntity::getHourType, reservation.hourType())); + if (existingCount > 0) { + throw new RuntimeException("同一用餐人同一天同一餐段只能预约一次,请勿重复预约"); + } + } + + private List acquireReservationSlotLocks(Long merchantId, List dinerUserIds, + HardApptReservationService.ReservationValidation reservation) { + List locks = new ArrayList<>(); + try { + for (Long dinerUserId : dinerUserIds.stream().sorted().toList()) { + String lockKey = String.format("hard:reserve:meal-slot:%s:%s:%s:%s", + merchantId, dinerUserId, reservation.apptDate(), reservation.hourType()); + LockInfo lock = lockTemplate.lock(lockKey, RESERVATION_SLOT_LOCK_EXPIRE_MILLIS, + RESERVATION_SLOT_LOCK_WAIT_MILLIS); + if (lock == null) { + throw new RuntimeException("预约正在处理中,请勿重复提交"); + } + locks.add(lock); + } + return locks; + } catch (RuntimeException exception) { + releaseReservationSlotLocks(locks); + throw exception; + } + } + + private void registerReservationSlotLockRelease(List locks) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + throw new RuntimeException("预约事务状态异常,请稍后重试"); + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + releaseReservationSlotLocks(locks); + } + }); + } + + private void releaseReservationSlotLocks(List locks) { + ListIterator iterator = locks.listIterator(locks.size()); + while (iterator.hasPrevious()) { + LockInfo lock = iterator.previous(); + try { + if (!lockTemplate.releaseLock(lock)) { + log.warn("|{}|预约餐段锁释放失败,lockKey:|{}|", TAG, lock.getLockKey()); + } + } catch (Exception exception) { + log.error("|{}|预约餐段锁释放异常,lockKey:|{}|", TAG, lock.getLockKey(), exception); + } + } + } + /** * 多人复制购物车前先拒绝缺少商品、数量或金额的异常明细,避免在库存汇总阶段出现半途失败。 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/support/HardApptCycleRuleCalculator.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/support/HardApptCycleRuleCalculator.java index 5b7aa5f28..2bee42842 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/support/HardApptCycleRuleCalculator.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/support/HardApptCycleRuleCalculator.java @@ -14,7 +14,8 @@ public final class HardApptCycleRuleCalculator { public static final int MIN_WEEKDAY = DayOfWeek.MONDAY.getValue(); public static final int MAX_WEEKDAY = DayOfWeek.SUNDAY.getValue(); - public static final int MIN_TARGET_OFFSET_DAYS = 1; + // 第0天表示截止日期当天,周期规则允许从本批截止日开始生成用餐日期。 + public static final int MIN_TARGET_OFFSET_DAYS = 0; public static final int MAX_TARGET_OFFSET_DAYS = 31; public static final int MIN_TARGET_DAY_COUNT = 1; public static final int MAX_TARGET_DAY_COUNT = 31; @@ -39,7 +40,7 @@ public final class HardApptCycleRuleCalculator { if (rule.getTargetStartOffsetDays() == null || rule.getTargetStartOffsetDays() < MIN_TARGET_OFFSET_DAYS || rule.getTargetStartOffsetDays() > MAX_TARGET_OFFSET_DAYS) { - throw new RuntimeException("目标起始天数必须在1至31天之间"); + throw new RuntimeException("目标起始天数必须在0至31天之间"); } if (rule.getTargetDayCount() == null || rule.getTargetDayCount() < MIN_TARGET_DAY_COUNT diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImplTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImplTest.java index 5e0fd958d..5386cf610 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImplTest.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImplTest.java @@ -90,6 +90,18 @@ class HardApptReservationServiceImplTest { LocalDate.of(2026, 8, 4), null, fridayOpen)); } + @Test + void configurableCycleSupportsTargetDateOnCloseDay() { + HardApptCycleRuleVo rule = cycleRule(100L, 10L, 5, LocalTime.of(17, 0), + 7, LocalTime.of(22, 0), 0, 1, "1"); + HardApptReservationServiceImpl service = reservationService(weeklyAppt(1L, 10L), 0, List.of(rule)); + LocalDateTime fridayOpen = LocalDateTime.of(2026, 7, 24, 18, 0); + + assertEquals(LocalDate.of(2026, 7, 26), + service.validateReservation(10L, null, 100L, "1", + LocalDate.of(2026, 7, 26), null, fridayOpen).apptDate()); + } + @Test void arbitraryWeekdaysAndCrossWeekWindowsAreCalculatedFromConfiguration() { HardApptCycleRuleVo mondayRule = cycleRule(100L, 10L, 1, LocalTime.of(8, 0), @@ -131,6 +143,12 @@ class HardApptReservationServiceImplTest { assertEquals(LocalDate.of(2026, 7, 21), batch.targetStartDate()); rule.setTargetStartOffsetDays(0); + assertDoesNotThrow(() -> HardApptCycleRuleCalculator.validate(rule)); + HardApptCycleRuleCalculator.CycleBatch sameDayBatch = HardApptCycleRuleCalculator.resolveBatch( + rule, LocalDateTime.of(2026, 7, 20, 8, 30)); + assertEquals(LocalDate.of(2026, 7, 20), sameDayBatch.targetStartDate()); + + rule.setTargetStartOffsetDays(-1); assertThrows(RuntimeException.class, () -> HardApptCycleRuleCalculator.validate(rule)); rule.setTargetStartOffsetDays(1); rule.setTargetDayCount(32); diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImplTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImplTest.java index 673e28a9c..29ffeffac 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImplTest.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImplTest.java @@ -10,9 +10,13 @@ import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.hard.api.constant.ReservationRecognitionTypeEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum; +import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum; +import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto; +import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto; import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity; +import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.vo.android.HardAdUserVo; import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo; import com.bsd.pjcloud.hard.api.vo.order.HardCarVo; @@ -22,6 +26,8 @@ import com.bsd.pjcloud.hard.service.HardManagerService; import com.bsd.pjcloud.hard.service.HardOrderService; import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -109,6 +115,7 @@ class ReservationMenuQueryServiceImplTest { assertEquals("2", result.getDays().get(0).getMeals().get(0).getHourType()); assertEquals("午餐", result.getDays().get(0).getMeals().get(0).getMealTitle()); assertEquals(List.of("米饭", "青菜", "红烧肉"), result.getDays().get(0).getMeals().get(0).getItems()); + assertEquals(List.of(1L, 3L), result.getDays().get(0).getMeals().get(0).getOrderIds()); } @Test @@ -121,6 +128,69 @@ class ReservationMenuQueryServiceImplTest { verifyNoMoreInteractions(hardApiService, hardOrderService); } + @Test + void cancelRejectsNonReservationOrder() { + HardOrderEntity order = HardOrderEntity.builder() + .id(1L) + .orderType(HardOrderTypeEnum.BELOW.getType()) + .orderStatus(HardOrderStatusEnum.PAY.getType()) + .apptUseDate(LocalDate.now()) + .build(); + when(hardOrderService.getById(1L)).thenReturn(order); + ReservationMenuCancelDto cancel = cancel(1L); + + assertThrows(RuntimeException.class, () -> service.cancel(cancel)); + verify(hardOrderService).getById(1L); + } + + @Test + void cancelUsesOnlyOrderIdAndDelegatesExistingCancellationFlow() { + HardOrderEntity order = HardOrderEntity.builder() + .id(1L) + .userId(300L) + .merchantId(200L) + .orderType(HardOrderTypeEnum.RESERVE.getType()) + .orderStatus(HardOrderStatusEnum.PAY.getType()) + .refundStatus(HardRefundStatusEnum.NONE.getStatus()) + .apptUseDate(LocalDate.now().plusDays(1)) + .build(); + when(hardOrderService.getById(1L)).thenReturn(order); + when(hardOrderService.handCancelCode(any())).thenReturn(true); + + assertTrue(service.cancel(cancel(1L))); + ArgumentCaptor cancelCaptor = ArgumentCaptor.forClass(HandCancelCodeDto.class); + verify(hardOrderService).handCancelCode(cancelCaptor.capture()); + assertEquals(1L, cancelCaptor.getValue().getOrderId()); + assertEquals(-1L, cancelCaptor.getValue().getHardwareId()); + } + + @Test + void cancelRejectsOrderDuringRefund() { + HardOrderEntity order = HardOrderEntity.builder() + .id(1L) + .userId(300L) + .merchantId(200L) + .orderType(HardOrderTypeEnum.RESERVE.getType()) + .orderStatus(HardOrderStatusEnum.PAY.getType()) + .refundStatus(HardRefundStatusEnum.APPLY.getStatus()) + .apptUseDate(LocalDate.now()) + .build(); + when(hardOrderService.getById(1L)).thenReturn(order); + + assertThrows(RuntimeException.class, () -> service.cancel(cancel(1L))); + verify(hardOrderService).getById(1L); + } + + @Test + void cancelRequestRejectsFieldsOtherThanOrderId() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + ReservationMenuCancelDto cancel = objectMapper.readValue("{\"orderId\":1}", ReservationMenuCancelDto.class); + assertEquals(1L, cancel.getOrderId()); + assertThrows(JsonMappingException.class, + () -> objectMapper.readValue("{\"orderId\":1,\"hardId\":2}", ReservationMenuCancelDto.class)); + } + private void stubTerminal(Long hardId, Long merchantId, String merchantName) { HardManagerEntity hard = new HardManagerEntity(); hard.setId(hardId); @@ -166,6 +236,12 @@ class ReservationMenuQueryServiceImplTest { return query; } + private static ReservationMenuCancelDto cancel(Long orderId) { + ReservationMenuCancelDto cancel = new ReservationMenuCancelDto(); + cancel.setOrderId(orderId); + return cancel; + } + private static HardOrderVo order(Long id, LocalDate date, String hourType, String title, LocalDateTime createTime, String... items) { HardOrderVo order = new HardOrderVo(); order.setId(id); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserFaceController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserFaceController.java index 8703b80cd..4c687f58d 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserFaceController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserFaceController.java @@ -177,6 +177,16 @@ public class SysUserFaceController { dto.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode()); return R.ok(sysUserFaceService.pageJoin(page, dto).getRecords()); } + + @Operation(summary = "android查询人脸数据库", description = "android查询人脸数据库") + @GetMapping("/ad/v2/find-face") + public R adV2FinFace(@ParameterObject Page page, @RequestParam(required = false) Long userId, @RequestParam(required = false) Long merchantDeptId) { + SysUserFaceDto dto = new SysUserFaceDto(); + dto.setUserId(userId); + dto.setMerchantDeptIdOrGlobal(merchantDeptId); + dto.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode()); + return R.ok(sysUserFaceService.allPageJoin(page, dto).getRecords()); + } /* ************************************* start android start ************************************* */ @Operation(summary = "设置为全局人脸", description = "设置为全局人脸") diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysUserFaceMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysUserFaceMapper.java index 550986a8a..3be25fb29 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysUserFaceMapper.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysUserFaceMapper.java @@ -84,4 +84,6 @@ public interface SysUserFaceMapper extends PjcloudBaseMapper } return selectJoinPage(page, SysUserFaceVo.class, wrapper); } + + Page allPageJoin(Page page, @Param("pa") SysUserFaceDto pa); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserFaceService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserFaceService.java index 41212f675..a8ef60b09 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserFaceService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserFaceService.java @@ -54,6 +54,8 @@ public interface SysUserFaceService extends IService { Page pageJoin(Page page, SysUserFaceDto dto); + Page allPageJoin(Page page, SysUserFaceDto dto); + SysUserFaceVo join(Long id); /** diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFaceServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFaceServiceImpl.java index 6c16f60c5..29cd04add 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFaceServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFaceServiceImpl.java @@ -413,6 +413,19 @@ public class SysUserFaceServiceImpl extends ServiceImpl allPageJoin(Page page, SysUserFaceDto dto) { + if (ObjectUtil.isNotEmpty(dto.getMerchantDeptIdOrGlobal())) { + List deptList = sysDeptService.listDescendant(dto.getMerchantDeptIdOrGlobal()); + if (ObjectUtil.isNotEmpty(deptList)) { + List deptIds = deptList.stream().map(SysDept::getDeptId).toList(); + dto.setDeptIds(deptIds); + } + } + return baseMapper.allPageJoin(page, dto); + } + + @Override public SysUserFaceVo join(Long id) { return baseMapper.join(id); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserFaceMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserFaceMapper.xml index 2f2d7ffe9..b5de04eb8 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserFaceMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserFaceMapper.xml @@ -95,4 +95,50 @@ GROUP BY a.user_id + + diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java index e7ffdcebb..8be347e64 100644 --- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java +++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java @@ -241,6 +241,11 @@ public class MchManageEntity extends Model { */ private Boolean singleProductOnlyInCart = false; + /** + * 同一用餐人在同一商户、同一天、同一餐段是否只允许保留一笔未核销预约。 + */ + private Boolean singleReservationOnlyPerMeal = false; + /** * 允许代订餐的用户关系编码,对应 sys_user_relation_type 字典和 UserRelationTypeEnum。 */ diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java index f69ba59e8..49f417a12 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java @@ -198,6 +198,8 @@ public class MchManageServiceImpl extends ServiceImpl