修复库存问题,修复cabinet_type问题
This commit is contained in:
1 parent
59b9946bca
commit
701be5a0c0
7 files changed
+241
-38
No files matched your search
+1
-1
@@ -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 = "")
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+12
-2
@@ -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<TradeOrderItemDO> list) {
|
||||
List<ProductSkuUpdateStockReqDTO.Item> items = CollectionUtils.convertList(list, item ->
|
||||
new ProductSkuUpdateStockReqDTO.Item().setId(item.getSkuId()).setIncrCount(item.getCount()));
|
||||
List<ProductSkuUpdateStockReqDTO.Item> 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);
|
||||
}
|
||||
|
||||
|
||||
+17
@@ -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> stockDeductionAllocation;
|
||||
|
||||
/**
|
||||
* 商品属性
|
||||
@@ -223,5 +231,14 @@ public class TradeOrderItemDO extends BaseDO {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 售货机订单库存扣减分配
|
||||
*/
|
||||
@Data
|
||||
public static class StockDeductionAllocation implements Serializable {
|
||||
private Long skuId;
|
||||
private Integer count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+87
-26
@@ -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<TradeOrderItemDO> 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<Long> spuIds = orderItems.stream().map(TradeOrderItemDO::getSpuId).distinct().collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(spuIds)) {
|
||||
log.info("[售货机出货]订单:{} 无商品明细,跳过", order.getNo());
|
||||
return;
|
||||
}
|
||||
List<ProductSpuRespDTO> 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<TradeOrderItemDO> orderItems,
|
||||
DeviceLogSaveReqVO deviceLogSaveReqVO) {
|
||||
log.info("[售货机出货]订单:{} handleDeviceOrderAfterPaid 开始", order.getNo());
|
||||
// 1. 提取 spuId 列表
|
||||
List<Long> 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<ProductSpuRespDTO> 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<Long, String> 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<ProductSkuRespDTO> 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<DispenseRequestBo> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// 创建 RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
try {
|
||||
// 发送请求并解析响应
|
||||
long startMs = System.currentTimeMillis();
|
||||
ResponseEntity<DispenseResponseBo> 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());
|
||||
|
||||
+66
-1
@@ -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} 实现类
|
||||
*
|
||||
* <p>售货机/自提柜订单(deliveryType=3):按 SPU 总库存扣减,从有库存的货道中分配
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Component
|
||||
@@ -25,7 +34,12 @@ public class TradeProductSkuOrderHandler implements TradeOrderHandler {
|
||||
|
||||
@Override
|
||||
public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> 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<TradeOrderItemDO> orderItems) {
|
||||
List<ProductSkuUpdateStockReqDTO.Item> deductItems = new ArrayList<>();
|
||||
List<Long> spuIds = orderItems.stream().map(TradeOrderItemDO::getSpuId).distinct().collect(Collectors.toList());
|
||||
List<ProductSkuRespDTO> allSkus = productSkuApi.getSkuListBySpuId(spuIds);
|
||||
Map<Long, List<ProductSkuRespDTO>> skuMapBySpu = allSkus.stream()
|
||||
.collect(Collectors.groupingBy(ProductSkuRespDTO::getSpuId));
|
||||
|
||||
for (TradeOrderItemDO item : orderItems) {
|
||||
List<ProductSkuRespDTO> skus = skuMapBySpu.get(item.getSpuId());
|
||||
if (CollUtil.isEmpty(skus)) {
|
||||
continue;
|
||||
}
|
||||
// 优先从库存多的货道扣,若有订单行的 skuId 且有库存则优先
|
||||
List<ProductSkuRespDTO> sorted = skus.stream()
|
||||
.filter(s -> s.getStock() != null && s.getStock() > 0)
|
||||
.sorted(Comparator.<ProductSkuRespDTO>comparingInt(s -> s.getId().equals(item.getSkuId()) ? 0 : 1)
|
||||
.thenComparing(Comparator.<ProductSkuRespDTO>comparingInt(s -> s.getStock() != null ? s.getStock() : 0).reversed()))
|
||||
.collect(Collectors.toList());
|
||||
int remain = item.getCount();
|
||||
List<TradeOrderItemDO.StockDeductionAllocation> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-7
@@ -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<ProductSkuRespDTO> skuList = checkSkuList(calculateReqBO);
|
||||
// 1.2 获得商品 SPU 数组
|
||||
List<ProductSpuRespDTO> spuList = checkSpuList(skuList);
|
||||
@@ -95,23 +97,69 @@ public class TradePriceServiceImpl implements TradePriceService {
|
||||
return calculateRespBO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 SKU 并返回列表。
|
||||
* <p>
|
||||
* 售货机/自提柜(deliveryType=3):按 SPU 下所有货道(SKU)的库存总和校验,不要求指定某个 SKU 有货;
|
||||
* 普通订单:按单个 SKU 库存校验。
|
||||
*/
|
||||
private List<ProductSkuRespDTO> checkSkuList(TradePriceCalculateReqBO reqBO) {
|
||||
// 获得商品 SKU 数组
|
||||
Map<Long, Integer> skuIdCountMap = convertMap(reqBO.getItems(),
|
||||
TradePriceCalculateReqBO.Item::getSkuId, TradePriceCalculateReqBO.Item::getCount);
|
||||
List<ProductSkuRespDTO> 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<Long, Integer> skuIdCountMap,
|
||||
List<ProductSkuRespDTO> requestSkus) {
|
||||
if (CollUtil.isEmpty(requestSkus)) {
|
||||
return;
|
||||
}
|
||||
// 按 SPU 分组,计算每个 SPU 需要的总数量
|
||||
Map<Long, Integer> 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<Long> spuIds = new ArrayList<>(spuIdCountMap.keySet());
|
||||
List<ProductSkuRespDTO> allSkusOfSpu = productSkuApi.getSkuListBySpuId(spuIds);
|
||||
Map<Long, Integer> spuTotalStock = allSkusOfSpu.stream()
|
||||
.filter(sku -> sku.getStock() != null && sku.getStock() > 0)
|
||||
.collect(Collectors.groupingBy(ProductSkuRespDTO::getSpuId,
|
||||
Collectors.summingInt(ProductSkuRespDTO::getStock)));
|
||||
for (Map.Entry<Long, Integer> 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<ProductSpuRespDTO> checkSpuList(List<ProductSkuRespDTO> skuList) {
|
||||
|
||||
Reference in new issue
Block a user