fix:核销bug修改

This commit is contained in:
dzc committed 2026-09-04 18:57:10 +08:00
1 parent a9a5a2f8d0
commit a7c505e9fc
5 files changed
+58 -5

No files matched your search

@@ -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<Boolean> 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));
}
@@ -24,7 +24,7 @@ public interface ReservationMenuQueryService {
* 在同一事务中批量核销同一识别用户的一组预约订单,并只创建一张兑换券打印任务。
*
* @param dto 批量订单主键参数
* @param hardId 当前食堂或超市终端 ID,可为空以兼容历史调用
* @param hardId 当前食堂或超市终端 ID,批量打印必须提供有效来源终端
* @return 整批订单均核销成功时返回 {@code true}
*/
Boolean cancelBatch(ReservationMenuBatchCancelDto dto, Long hardId);
@@ -142,6 +142,8 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
}
List<Long> 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<HardOrderEntity> 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("当前设备不能核销该预约订单");
}
}
/**
* 判断预约订单退款聚合状态是否允许继续履约,历史空值按未售后兼容,未知状态按不可核销处理。
*
@@ -3995,10 +3995,12 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
if (requestByOrderId.values().stream().anyMatch(dto -> !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<HardOrderEntity> canceledOrders = new ArrayList<>();
@@ -4075,8 +4077,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
if (ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())) {
validateCanteenReservationBeforeCancel(order);
} else if (hardCarService.getByOrderId(order.getId()).stream()
.noneMatch(car -> 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<HardOrderMapper, HardOrder
}
}
/**
* 对订单服务内部调用再次校验来源设备类型和商户,避免绕过预约菜单服务时使用其他硬件类型核销。
*
* @param order 批次首订单快照
* @param sourceHardId 来源业务终端 ID;为空表示历史无设备来源调用
*/
private void validateReservationVerificationSource(HardOrderEntity order, Long sourceHardId) {
if (sourceHardId == null) return;
HardManagerEntity source = hardManagerService.getById(sourceHardId);
HardTypeEnum sourceType = source == null ? null : HardTypeEnum.valueOfByType(source.getType());
if (source == null || (sourceType != HardTypeEnum.SUPERMARKET && sourceType != HardTypeEnum.CANTEEN)
|| !Objects.equals(source.getMerchantId(), order.getMerchantId())) {
throw new RuntimeException("当前设备不能核销该预约订单");
}
}
/**
* 判断预约订单是否仍有可履约商品,兼容历史商品查询返回空值的情况。
*
* @param orderId 待核销预约订单主键
* @return 至少有一份剩余商品时返回 {@code true}
*/
private boolean hasReservationFulfillableProduct(Long orderId) {
List<HardCarVo> cars = hardCarService.getByOrderId(orderId);
return cars != null && cars.stream().anyMatch(car -> resolveEffectiveProductCount(car) > 0);
}
/**
* 按用户、商户、日期、餐段和来源设备串行化预约核销批次,避免共享核销时间误合并并发批次。
*
@@ -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);
/**
* 验证食堂预约部分退款后数量和行金额按剩余份数投影,已全部退款商品不会返回消费机。