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 79661460..acf001ab 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 @@ -475,7 +475,7 @@ public class HardApiController { @Operation(summary = "批量核销预约订单并合并打印兑换券") @Idempotent(expireTime = 1, key = "'/hard-api/ad/reservation-menu/cancel-batch:' + #dto.getOrderIds()", info = "正在核销中,请勿重复操作") public R cancelReservationMenuBatch(@Valid @RequestBody ReservationMenuBatchCancelDto dto, - @RequestHeader(value = "DEVICE-ID", required = false) Long hardId) { + @RequestHeader("DEVICE-ID") Long hardId) { return R.ok(reservationMenuQueryService.cancelBatch(dto, hardId)); } 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 543eb0a8..69d09707 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 @@ -24,7 +24,7 @@ public interface ReservationMenuQueryService { * 在同一事务中批量核销同一识别用户的一组预约订单,并只创建一张兑换券打印任务。 * * @param dto 批量订单主键参数 - * @param hardId 当前食堂或超市终端 ID,可为空以兼容历史调用 + * @param hardId 当前食堂或超市终端 ID,批量打印必须提供有效来源终端 * @return 整批订单均核销成功时返回 {@code true} */ Boolean cancelBatch(ReservationMenuBatchCancelDto dto, Long hardId); 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 8b746dbc..a8c480d3 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 @@ -142,6 +142,8 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ } List orderIds = new LinkedHashSet<>(dto.getOrderIds()).stream().toList(); if (orderIds.stream().anyMatch(Objects::isNull)) throw new RuntimeException("预约订单ID不能为空"); + if (orderIds.size() > 100) throw new RuntimeException("一次最多核销100笔预约订单"); + if (hardId == null) throw new RuntimeException("DEVICE-ID不能为空"); List orders = orderIds.stream().map(hardOrderService::getById).toList(); HardOrderEntity first = orders.stream().findFirst().orElseThrow(() -> new RuntimeException("预约订单不存在")); @@ -183,6 +185,9 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ throw new RuntimeException("该订单设备类型不支持预约核销"); } resolveVerificationHardId(order, hardId); + if (first == null) { + validateBatchDevice(order, hardId); + } if (first == null) return; if (!Objects.equals(first.getUserId(), order.getUserId()) || !Objects.equals(first.getMerchantId(), order.getMerchantId()) @@ -220,6 +225,22 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ return hard.getId(); } + /** + * 批量核销专用的来源设备白名单校验;单笔旧接口保留原有商户归属兼容语义。 + * + * @param order 批次订单 + * @param hardId 请求头来源设备 ID + */ + private void validateBatchDevice(HardOrderEntity order, Long hardId) { + if (hardId == null) return; + HardManagerEntity hard = hardManagerService.getById(hardId); + HardTypeEnum hardType = hard == null ? null : HardTypeEnum.valueOfByType(hard.getType()); + if (hard == null || (hardType != HardTypeEnum.SUPERMARKET && hardType != HardTypeEnum.CANTEEN) + || !Objects.equals(hard.getMerchantId(), order.getMerchantId())) { + throw new RuntimeException("当前设备不能核销该预约订单"); + } + } + /** * 判断预约订单退款聚合状态是否允许继续履约,历史空值按未售后兼容,未知状态按不可核销处理。 * 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 ae11213a..2c07fb8b 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 @@ -3995,10 +3995,12 @@ public class HardOrderServiceImpl extends ServiceImpl !Objects.equals(sourceHardId, dto.getHardwareId()))) { throw new RuntimeException("批量核销必须使用同一来源设备"); } + if (sourceHardId == null) throw new RuntimeException("批量核销来源设备不能为空"); Long firstOrderId = requestByOrderId.keySet().stream().min(Long::compareTo).orElseThrow(); HardOrderEntity firstSnapshot = baseMapper.selectById(firstOrderId); if (firstSnapshot == null) throw new I18nBizException(ErrorCodes.COMMON_ORDER_NOT_FOUND); + validateReservationVerificationSource(firstSnapshot, sourceHardId); LockInfo verificationLock = acquireReservationVerificationLock(firstSnapshot, sourceHardId); boolean releaseByTransaction = false; List canceledOrders = new ArrayList<>(); @@ -4075,8 +4077,7 @@ public class HardOrderServiceImpl extends ServiceImpl resolveEffectiveProductCount(car) > 0)) { + } else if (!hasReservationFulfillableProduct(order.getId())) { throw new I18nBizException(ErrorCodes.HARD_RESERVE_FULL_REFUND_NO_VERIFY); } if (sourceHardId != null && !Objects.equals(sourceHardId, order.getHardId()) @@ -4093,6 +4094,33 @@ public class HardOrderServiceImpl extends ServiceImpl cars = hardCarService.getByOrderId(orderId); + return cars != null && cars.stream().anyMatch(car -> resolveEffectiveProductCount(car) > 0); + } + /** * 按用户、商户、日期、餐段和来源设备串行化预约核销批次,避免共享核销时间误合并并发批次。 * diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/controller/hard/HardApiControllerTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/controller/hard/HardApiControllerTest.java index 003300ac..354b321f 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/controller/hard/HardApiControllerTest.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/controller/hard/HardApiControllerTest.java @@ -16,6 +16,7 @@ import com.bsd.pjcloud.hard.service.HardOrderTotalService; import com.bsd.pjcloud.hard.service.HardPrinterService; import com.bsd.pjcloud.hard.service.HardTerminalStatusService; import com.bsd.pjcloud.hard.service.HardTerminalFaceService; +import com.bsd.pjcloud.hard.service.HardTerminalPrintService; import com.bsd.pjcloud.hard.service.ReservationMenuQueryService; import com.bsd.pjcloud.hard.service.VerificationRecordService; import org.junit.jupiter.api.Test; @@ -69,11 +70,14 @@ class HardApiControllerTest { /** 终端人脸删除服务,本组购物车测试不调用。 */ private final HardTerminalFaceService hardTerminalFaceService = mock(HardTerminalFaceService.class); + /** 终端打印凭证服务,本组购物车测试不调用。 */ + private final HardTerminalPrintService hardTerminalPrintService = mock(HardTerminalPrintService.class); + /** 被测 Android 硬件接口控制器。 */ private final HardApiController controller = new HardApiController(hardManagerService, hardInvoiceService, hardPrinterService, hardCarService, hardOrderService, hardOrderSyncService, hardApiService, hardOrderTotalService, reservationMenuQueryService, verificationRecordService, hardTerminalFaceService, - hardTerminalStatusService); + hardTerminalPrintService, hardTerminalStatusService); /** * 验证食堂预约部分退款后数量和行金额按剩余份数投影,已全部退款商品不会返回消费机。