From 701be5a0c046839f9ecde7f6c6fddf9c2324db0e Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Mon, 23 Feb 2026 20:25:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BA=93=E5=AD=98=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E4=BF=AE=E5=A4=8Dcabinet=5Ftype=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/vo/DeviceProductSkuSaveReqVO.java | 2 +- .../service/spu/ProductSpuServiceImpl.java | 4 +- .../convert/order/TradeOrderConvert.java | 14 ++- .../dataobject/order/TradeOrderItemDO.java | 17 +++ .../handler/TradeDeviceOrderHandler.java | 113 ++++++++++++++---- .../handler/TradeProductSkuOrderHandler.java | 67 ++++++++++- .../service/price/TradePriceServiceImpl.java | 62 ++++++++-- 7 files changed, 241 insertions(+), 38 deletions(-) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java index 6285691..9bbb69e 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/device/vo/DeviceProductSkuSaveReqVO.java @@ -58,7 +58,7 @@ public class DeviceProductSkuSaveReqVO { @NotEmpty(message = "货道或柜门编号不能为空") private String cabinetChannel; - @Schema(description = "通道类型", example = "") + @Schema(description = "通道类型,不填默认1。1=货道 2=货道带调货检测 48=柜门 49=柜门带反馈", example = "1") private String cabinetType; @Schema(description = "限购类型", example = "") diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java index 09b89e8..9ed1382 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.product.service.spu; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; @@ -289,7 +290,8 @@ public class ProductSpuServiceImpl implements ProductSpuService { sku.setFirstBrokeragePrice(deviceSku.getFirstBrokeragePrice() != null ? deviceSku.getFirstBrokeragePrice() : 0); sku.setSecondBrokeragePrice(deviceSku.getSecondBrokeragePrice() != null ? deviceSku.getSecondBrokeragePrice() : 0); sku.setCabinetChannel(deviceSku.getCabinetChannel()); - sku.setCabinetType(deviceSku.getCabinetType()); + // cabinetType 为空时默认 "1"(货道),否则出货时会因无通道类型而失败 + sku.setCabinetType(StrUtil.isNotBlank(deviceSku.getCabinetType()) ? deviceSku.getCabinetType() : "1"); sku.setPurchaseBizType(deviceSku.getPurchaseBizType()); // properties 会在 validateSkuList 中自动处理(单规格会添加默认属性) skus.add(sku); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index fe94bc6..10cf8ec 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.trade.convert.order; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -119,8 +120,17 @@ public interface TradeOrderConvert { TradeOrderItemDO convert(TradePriceCalculateSummaryRespBO.OrderItem item); default ProductSkuUpdateStockReqDTO convert(List list) { - List items = CollectionUtils.convertList(list, item -> - new ProductSkuUpdateStockReqDTO.Item().setId(item.getSkuId()).setIncrCount(item.getCount())); + List items = new ArrayList<>(); + for (TradeOrderItemDO item : list) { + if (CollUtil.isNotEmpty(item.getStockDeductionAllocation())) { + // 售货机订单:按实际扣减的 allocation 回滚 + for (TradeOrderItemDO.StockDeductionAllocation alloc : item.getStockDeductionAllocation()) { + items.add(new ProductSkuUpdateStockReqDTO.Item().setId(alloc.getSkuId()).setIncrCount(alloc.getCount())); + } + } else { + items.add(new ProductSkuUpdateStockReqDTO.Item().setId(item.getSkuId()).setIncrCount(item.getCount())); + } + } return new ProductSkuUpdateStockReqDTO(items); } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java index 1ca1d47..2f8c25c 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/dataobject/order/TradeOrderItemDO.java @@ -188,6 +188,14 @@ public class TradeOrderItemDO extends BaseDO { * 枚举 {@link TradeOrderItemAfterSaleStatusEnum} */ private Integer afterSaleStatus; + /** + * 售货机订单实际扣减库存的 SKU 分配,JSON 格式 + * + * 格式:[{"skuId":1,"count":2},{"skuId":2,"count":1}] + * 用于订单取消时正确回滚库存到原货道 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List stockDeductionAllocation; /** * 商品属性 @@ -223,5 +231,14 @@ public class TradeOrderItemDO extends BaseDO { } + /** + * 售货机订单库存扣减分配 + */ + @Data + public static class StockDeductionAllocation implements Serializable { + private Long skuId; + private Integer count; + } + } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java index 09fee4a..b0bae22 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeDeviceOrderHandler.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.order.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.product.api.device.DeviceApi; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; @@ -26,6 +27,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.http.*; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; @@ -89,24 +93,38 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { @Override public void afterPayOrder(TradeOrderDO order, List orderItems) { - // 商城与售货机共用同一支付回调,仅售货机订单才走设备出货流程 - if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) { - return; - } + log.info("[售货机出货]订单:{} 进入 afterPayOrder, orderType={}, orderStatus={}, itemCount={}", + order.getNo(), order.getType(), order.getStatus(), orderItems != null ? orderItems.size() : 0); - // 这里不要抛异常,否则会影响“支付成功后”的主流程 - // 所有错误通过日志 + 设备出货日志记录 - - // 构建基础的设备出货日志 VO(后续各种分支都会复用) + // 构建基础的设备出货日志 VO(所有分支都会记录,便于排查“支付成功但无出货日志”问题) DeviceLogSaveReqVO deviceLogSaveReqVO = new DeviceLogSaveReqVO(); deviceLogSaveReqVO.setUserId(order.getUserId()); deviceLogSaveReqVO.setOrderId(order.getId()); deviceLogSaveReqVO.setOrderNo(order.getNo()); + + // 快速跳过:订单类型不是售货机时,按 SPU 类型二次判断,防止 type 未正确设置时漏掉售货机订单 + if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) { + log.info("[售货机出货]订单:{} 订单类型非MACHINE(type={}),二次检查SPU是否包含售货机商品", order.getNo(), order.getType()); + List spuIds = orderItems.stream().map(TradeOrderItemDO::getSpuId).distinct().collect(Collectors.toList()); + if (CollUtil.isEmpty(spuIds)) { + log.info("[售货机出货]订单:{} 无商品明细,跳过", order.getNo()); + return; + } + List checkSpuList = productSpuApi.getSpuList(spuIds); + boolean hasDeviceSpu = checkSpuList != null && checkSpuList.stream() + .anyMatch(spu -> Objects.equals(ProductSpuTypesEnum.ENABLE.getTypes(), spu.getTypes())); + if (!hasDeviceSpu) { + log.info("[售货机出货]订单:{} 确认无售货机商品(spuIds={}),跳过", order.getNo(), spuIds); + return; // 确认无售货机商品,不记录设备日志 + } + log.info("[售货机出货]订单:{} 含售货机商品但type非MACHINE,继续走出货流程", order.getNo()); + } + try { handleDeviceOrderAfterPaid(order, orderItems, deviceLogSaveReqVO); } catch (Throwable ex) { - // 兜底保护:这里不再向上抛异常,仅记录日志,避免影响支付回调流程 - log.error("订单--{}:[设备出货处理异常]:原因: {}", order.getNo(), ex.getMessage(), ex); + log.error("[售货机出货]订单:{} 设备出货处理异常: {} | 异常类型: {} | 堆栈: ", + order.getNo(), ex.getMessage(), ex.getClass().getName(), ex); deviceLogSaveReqVO.setDeviceStatus(99); deviceLogSaveReqVO.setContent("[出货异常]原因:" + ex.getMessage()); deviceLogService.createDeviceLog(deviceLogSaveReqVO); @@ -123,18 +141,25 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { private void handleDeviceOrderAfterPaid(TradeOrderDO order, List orderItems, DeviceLogSaveReqVO deviceLogSaveReqVO) { + log.info("[售货机出货]订单:{} handleDeviceOrderAfterPaid 开始", order.getNo()); // 1. 提取 spuId 列表 List spuIds = orderItems.stream() .map(TradeOrderItemDO::getSpuId) .distinct() .collect(Collectors.toList()); + log.info("[售货机出货]订单:{} step1 spuIds={}", order.getNo(), spuIds); if (CollUtil.isEmpty(spuIds)) { log.warn("订单:{} 无有效商品明细,跳过设备出货处理", order.getNo()); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]原因:订单无有效商品明细"); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); return; } // 2. 查询 spu 信息,并校验是否为“提货柜商品” + “同一设备” + log.info("[售货机出货]订单:{} step2 调用 productSpuApi.getSpuList", order.getNo()); List spuList = productSpuApi.getSpuList(spuIds); + log.info("[售货机出货]订单:{} step2 spuList.size={}", order.getNo(), spuList != null ? spuList.size() : 0); if (CollUtil.isEmpty(spuList)) { log.error("订单:{}, 提货柜订单spu列表为空", order.getNo()); deviceLogSaveReqVO.setDeviceStatus(99); @@ -144,20 +169,24 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { } if (hasNonDeviceSpu(spuList)) { - // 订单中包含非提货柜商品,只打日志,不抛异常 log.error("订单:{}, 中包含非提货柜商品,跳过设备出货处理", order.getNo()); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]原因:订单中包含非提货柜商品"); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); return; } Map spuNameMap = spuList.stream() .collect(Collectors.toMap(ProductSpuRespDTO::getId, ProductSpuRespDTO::getName, (a, b) -> a)); + log.info("[售货机出货]订单:{} step3 调用 findUniqueDeviceId", order.getNo()); String deviceId = findUniqueDeviceId(order, spuList, deviceLogSaveReqVO); if (deviceId == null) { - // 已在方法内部记录日志 return; } + log.info("[售货机出货]订单:{} step3 deviceId={}", order.getNo(), deviceId); // 3. 按 SPU 查询该商品下所有货道(SKU),并校验单一通道类型 + log.info("[售货机出货]订单:{} step4 调用 productSkuApi.getSkuListBySpuId", order.getNo()); List skuListBySpu = productSkuApi.getSkuListBySpuId(spuIds); if (CollUtil.isEmpty(skuListBySpu)) { log.warn("订单:{} 商品下无货道(SKU),跳过设备出货处理", order.getNo()); @@ -168,13 +197,21 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { return; } + log.info("[售货机出货]订单:{} step5 调用 findUniqueCabinetType", order.getNo()); String cabinetType = findUniqueCabinetType(order, skuListBySpu, spuNameMap, deviceId, deviceLogSaveReqVO); - if (cabinetType == null) { - // 已在方法内部记录日志 + if (StrUtil.isBlank(cabinetType)) { + log.error("[售货机出货]订单:{} step5 无法获取有效 cabinetType,spuIds={} 下所有货道(SKU)的 cabinetType 均为空,请检查 product_sku 表 cabinet_type 字段", + order.getNo(), spuIds); + deviceLogSaveReqVO.setDeviceId(deviceId); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]原因:货道(SKU)的通道类型(cabinetType)未配置,请检查商品货道设置"); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); return; } + log.info("[售货机出货]订单:{} step5 cabinetType={}", order.getNo(), cabinetType); // 4. 组装出货请求:每条订单行在该商品下有库存的货道中随机选一个,同货道数量合并 + log.info("[售货机出货]订单:{} step6 调用 buildDispenseRequest", order.getNo()); DispenseRequestBo request = buildDispenseRequest(order, orderItems, deviceId, cabinetType); if (request == null) { deviceLogSaveReqVO.setDeviceId(deviceId); @@ -185,6 +222,8 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { } // 5. 调用设备出货接口,并记录出货日志 + log.info("[售货机出货]订单:{} 校验通过,准备调用出货API deviceId={} message={}", + order.getNo(), request.getDeviceId(), request.getMessage()); sendDispenseRequest(order, request, deviceLogSaveReqVO); // 6. 无论出货成功/失败,尝试将订单状态更新为已完成(保持原有语义) @@ -326,29 +365,31 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { private void sendDispenseRequest(TradeOrderDO order, DispenseRequestBo request, DeviceLogSaveReqVO deviceLogSaveReqVO) { - // 设置请求头 + String requestJson = JsonUtils.toJsonString(request); + log.info("[售货机出货]订单:{} 即将请求出货API url={} requestBody={}", order.getNo(), api_url, requestJson); + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", token); - - // 创建请求实体 HttpEntity requestEntity = new HttpEntity<>(request, headers); - - // 创建 RestTemplate RestTemplate restTemplate = new RestTemplate(); try { - // 发送请求并解析响应 + long startMs = System.currentTimeMillis(); ResponseEntity response = restTemplate.exchange( api_url, HttpMethod.POST, requestEntity, DispenseResponseBo.class ); + long costMs = System.currentTimeMillis() - startMs; + + log.info("[售货机出货]订单:{} API请求完成 statusCode={} costMs={}", order.getNo(), + response.getStatusCodeValue(), costMs); DispenseResponseBo body = response.getBody(); if (body == null) { - log.error("订单--{}:[出货失败]:响应体为空", order.getNo()); + log.error("[售货机出货]订单:{} 出货失败: 响应体为空 statusCode={}", order.getNo(), response.getStatusCodeValue()); deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); deviceLogSaveReqVO.setDeviceStatus(99); deviceLogSaveReqVO.setContent("[出货失败]原因:响应为空"); @@ -358,20 +399,40 @@ public class TradeDeviceOrderHandler implements TradeOrderHandler { Integer respCode = body.getCode() != null ? body.getCode() : -1; String respData = body.getData() != null ? body.getData() : ""; - log.info("订单--{}:[设备出货响应]: code={}, msg={}, data={}", order.getNo(), - respCode, body.getMsg(), respData); + log.info("[售货机出货]订单:{} API业务响应 code={} msg={} data={}", order.getNo(), respCode, body.getMsg(), respData); deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); deviceLogSaveReqVO.setDeviceStatus(respCode); String content = buildDispenseLogContent(respCode, body.getMsg(), respData); deviceLogSaveReqVO.setContent(content); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); if (respCode != 0) { - log.error("订单--{}:[出货失败]: {}", order.getNo(), content); + log.warn("[售货机出货]订单:{} 出货业务失败: {}", order.getNo(), content); } + } catch (HttpClientErrorException e) { + String responseBody = e.getResponseBodyAsString(); + log.error("[售货机出货]订单:{} API返回4xx statusCode={} responseBody={} | 异常: {}", + order.getNo(), e.getStatusCode().value(), responseBody, e.getMessage(), e); + deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]API返回" + e.getStatusCode().value() + ": " + StrUtil.sub(e.getMessage(), 0, 200)); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); + } catch (HttpServerErrorException e) { + String responseBody = e.getResponseBodyAsString(); + log.error("[售货机出货]订单:{} API返回5xx statusCode={} responseBody={} | 异常: {}", + order.getNo(), e.getStatusCode().value(), responseBody, e.getMessage(), e); + deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]API返回" + e.getStatusCode().value() + ": " + StrUtil.sub(e.getMessage(), 0, 200)); + deviceLogService.createDeviceLog(deviceLogSaveReqVO); + } catch (ResourceAccessException e) { + log.error("[售货机出货]订单:{} API连接异常(超时/网络不可达) url={} | 异常: {}", order.getNo(), api_url, e.getMessage(), e); + deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); + deviceLogSaveReqVO.setDeviceStatus(99); + deviceLogSaveReqVO.setContent("[出货异常]连接失败: " + e.getMessage()); deviceLogService.createDeviceLog(deviceLogSaveReqVO); } catch (Exception e) { - // HTTP 请求异常 - log.error("订单--{}:[出货异常]:原因: {}", order.getNo(), e.getMessage(), e); + log.error("[售货机出货]订单:{} API请求异常 异常类型={} | 异常: {}", order.getNo(), e.getClass().getName(), e.getMessage(), e); deviceLogSaveReqVO.setDeviceId(request.getDeviceId()); deviceLogSaveReqVO.setDeviceStatus(99); deviceLogSaveReqVO.setContent("[出货异常]原因:" + e.getMessage()); diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeProductSkuOrderHandler.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeProductSkuOrderHandler.java index d28a643..6ca182e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeProductSkuOrderHandler.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeProductSkuOrderHandler.java @@ -2,19 +2,28 @@ package cn.iocoder.yudao.module.trade.service.order.handler; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; +import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO; import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static java.util.Collections.singletonList; /** * 商品 SKU 库存的 {@link TradeOrderHandler} 实现类 * + *

售货机/自提柜订单(deliveryType=3):按 SPU 总库存扣减,从有库存的货道中分配 + * * @author 芋道源码 */ @Component @@ -25,7 +34,12 @@ public class TradeProductSkuOrderHandler implements TradeOrderHandler { @Override public void beforeOrderCreate(TradeOrderDO order, List orderItems) { - productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems)); + if (isDeviceOrder(order)) { + // 售货机订单:按 SPU 分配库存,从有库存的货道扣减 + deductStockBySpuAllocation(orderItems); + } else { + productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convertNegative(orderItems)); + } } @Override @@ -43,4 +57,55 @@ public class TradeProductSkuOrderHandler implements TradeOrderHandler { productSkuApi.updateSkuStock(TradeOrderConvert.INSTANCE.convert(singletonList(orderItem))); } + private boolean isDeviceOrder(TradeOrderDO order) { + return order != null && java.util.Objects.equals(order.getDeliveryType(), DeliveryTypeEnum.DOOR.getType()); + } + + /** + * 售货机订单:按 SPU 分配库存并扣减。从每个 SPU 下有库存的货道(SKU)中贪婪分配。 + */ + private void deductStockBySpuAllocation(List orderItems) { + List deductItems = new ArrayList<>(); + List spuIds = orderItems.stream().map(TradeOrderItemDO::getSpuId).distinct().collect(Collectors.toList()); + List allSkus = productSkuApi.getSkuListBySpuId(spuIds); + Map> skuMapBySpu = allSkus.stream() + .collect(Collectors.groupingBy(ProductSkuRespDTO::getSpuId)); + + for (TradeOrderItemDO item : orderItems) { + List skus = skuMapBySpu.get(item.getSpuId()); + if (CollUtil.isEmpty(skus)) { + continue; + } + // 优先从库存多的货道扣,若有订单行的 skuId 且有库存则优先 + List sorted = skus.stream() + .filter(s -> s.getStock() != null && s.getStock() > 0) + .sorted(Comparator.comparingInt(s -> s.getId().equals(item.getSkuId()) ? 0 : 1) + .thenComparing(Comparator.comparingInt(s -> s.getStock() != null ? s.getStock() : 0).reversed())) + .collect(Collectors.toList()); + int remain = item.getCount(); + List allocation = new ArrayList<>(); + for (ProductSkuRespDTO sku : sorted) { + if (remain <= 0) { + break; + } + int stock = sku.getStock() != null ? sku.getStock() : 0; + int deduct = Math.min(remain, stock); + if (deduct > 0) { + deductItems.add(new ProductSkuUpdateStockReqDTO.Item().setId(sku.getId()).setIncrCount(-deduct)); + allocation.add(new TradeOrderItemDO.StockDeductionAllocation().setSkuId(sku.getId()).setCount(deduct)); + remain -= deduct; + // 更新已用库存,避免同事务内重复分配(后续分配会重新查库,这里只影响本 SPU 多 item 场景) + sku.setStock(stock - deduct); + } + } + if (remain > 0) { + throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception( + cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH); + } + item.setStockDeductionAllocation(allocation); + } + if (!deductItems.isEmpty()) { + productSkuApi.updateSkuStock(new ProductSkuUpdateStockReqDTO(deductItems)); + } + } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java index 7083a4c..3b4b00e 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/TradePriceServiceImpl.java @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi; import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum; import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO; +import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO; import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateSummaryReqBO; @@ -27,6 +28,7 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; +import java.util.*; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -68,7 +70,7 @@ public class TradePriceServiceImpl implements TradePriceService { @Override public TradePriceCalculateRespBO calculateOrderPrice(TradePriceCalculateReqBO calculateReqBO) { - // 1.1 获得商品 SKU 数组 + // 1.1 获得商品 SKU 数组(售货机/自提柜按 SPU 总库存校验,其他按单 SKU 校验) List skuList = checkSkuList(calculateReqBO); // 1.2 获得商品 SPU 数组 List spuList = checkSpuList(skuList); @@ -95,23 +97,69 @@ public class TradePriceServiceImpl implements TradePriceService { return calculateRespBO; } + /** + * 校验 SKU 并返回列表。 + *

+ * 售货机/自提柜(deliveryType=3):按 SPU 下所有货道(SKU)的库存总和校验,不要求指定某个 SKU 有货; + * 普通订单:按单个 SKU 库存校验。 + */ private List checkSkuList(TradePriceCalculateReqBO reqBO) { - // 获得商品 SKU 数组 Map skuIdCountMap = convertMap(reqBO.getItems(), TradePriceCalculateReqBO.Item::getSkuId, TradePriceCalculateReqBO.Item::getCount); List skus = productSkuApi.getSkuList(skuIdCountMap.keySet()); - // 校验商品 SKU - skus.forEach(sku -> { + // 售货机/自提柜:按 SPU 总库存校验 + boolean isDeviceOrder = Objects.equals(reqBO.getDeliveryType(), DeliveryTypeEnum.DOOR.getType()); + if (isDeviceOrder) { + checkSkuStockBySpu(reqBO, skuIdCountMap, skus); + } else { + // 普通订单:按单 SKU 库存校验 + skus.forEach(sku -> { + Integer count = skuIdCountMap.get(sku.getId()); + if (count == null) { + throw exception(SKU_NOT_EXISTS); + } + if (count > (sku.getStock() != null ? sku.getStock() : 0)) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + }); + } + return skus; + } + + /** + * 售货机订单:按 SPU 下所有货道(SKU)的库存总和校验 + */ + private void checkSkuStockBySpu(TradePriceCalculateReqBO reqBO, + Map skuIdCountMap, + List requestSkus) { + if (CollUtil.isEmpty(requestSkus)) { + return; + } + // 按 SPU 分组,计算每个 SPU 需要的总数量 + Map spuIdCountMap = new HashMap<>(); + for (ProductSkuRespDTO sku : requestSkus) { Integer count = skuIdCountMap.get(sku.getId()); if (count == null) { throw exception(SKU_NOT_EXISTS); } - if (count > sku.getStock()) { + spuIdCountMap.merge(sku.getSpuId(), count, Integer::sum); + } + // 查询每个 SPU 下所有 SKU 的库存 + List spuIds = new ArrayList<>(spuIdCountMap.keySet()); + List allSkusOfSpu = productSkuApi.getSkuListBySpuId(spuIds); + Map spuTotalStock = allSkusOfSpu.stream() + .filter(sku -> sku.getStock() != null && sku.getStock() > 0) + .collect(Collectors.groupingBy(ProductSkuRespDTO::getSpuId, + Collectors.summingInt(ProductSkuRespDTO::getStock))); + for (Map.Entry entry : spuIdCountMap.entrySet()) { + Long spuId = entry.getKey(); + Integer needCount = entry.getValue(); + int totalStock = spuTotalStock.getOrDefault(spuId, 0); + if (needCount > totalStock) { throw exception(SKU_STOCK_NOT_ENOUGH); } - }); - return skus; + } } private List checkSpuList(List skuList) {