修复库存问题,修复cabinet_type问题

This commit is contained in:
zjf committed 2026-02-23 20:25:06 +08:00
1 parent 59b9946bca
commit 701be5a0c0
7 files changed
+241 -38

No files matched your search

@@ -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 = "")
@@ -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);
@@ -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);
}
@@ -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;
}
}
@@ -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());
@@ -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));
}
}
}
@@ -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) {