|
@@ -12,9 +12,11 @@ import com.etotem.cfc.entity.AssessmentProduct;
|
|
|
import com.etotem.cfc.entity.Consignee;
|
|
import com.etotem.cfc.entity.Consignee;
|
|
|
import com.etotem.cfc.entity.UserAddress;
|
|
import com.etotem.cfc.entity.UserAddress;
|
|
|
import com.etotem.cfc.mapper.UserAddressMapper;
|
|
import com.etotem.cfc.mapper.UserAddressMapper;
|
|
|
|
|
+import com.etotem.cfc.pricing.LineItem;
|
|
|
import com.etotem.cfc.entity.Coupon;
|
|
import com.etotem.cfc.entity.Coupon;
|
|
|
import com.etotem.cfc.entity.FamilyCoupon;
|
|
import com.etotem.cfc.entity.FamilyCoupon;
|
|
|
import com.etotem.cfc.entity.Product;
|
|
import com.etotem.cfc.entity.Product;
|
|
|
|
|
+import com.etotem.cfc.entity.ProductBundleItem;
|
|
|
import com.etotem.cfc.entity.ProductGiftRule;
|
|
import com.etotem.cfc.entity.ProductGiftRule;
|
|
|
import com.etotem.cfc.entity.ProductOnlineSlot;
|
|
import com.etotem.cfc.entity.ProductOnlineSlot;
|
|
|
import com.etotem.cfc.entity.OrderItem;
|
|
import com.etotem.cfc.entity.OrderItem;
|
|
@@ -40,7 +42,10 @@ import com.etotem.cfc.mapper.FamilyCouponMapper;
|
|
|
import com.etotem.cfc.mapper.ProductMapper;
|
|
import com.etotem.cfc.mapper.ProductMapper;
|
|
|
import com.etotem.cfc.mapper.ProductOrderMapper;
|
|
import com.etotem.cfc.mapper.ProductOrderMapper;
|
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.entity.ProductBundleItem;
|
|
|
import com.etotem.cfc.entity.ProductSku;
|
|
import com.etotem.cfc.entity.ProductSku;
|
|
|
|
|
+import com.etotem.cfc.entity.ProductSkuComboItem;
|
|
|
|
|
+import com.etotem.cfc.mapper.ProductBundleItemMapper;
|
|
|
import com.etotem.cfc.service.ProductSkuService;
|
|
import com.etotem.cfc.service.ProductSkuService;
|
|
|
import com.etotem.cfc.service.AssessmentProductService;
|
|
import com.etotem.cfc.service.AssessmentProductService;
|
|
|
import com.etotem.cfc.service.AssessmentQuotaService;
|
|
import com.etotem.cfc.service.AssessmentQuotaService;
|
|
@@ -94,9 +99,15 @@ public class ProductOrderService {
|
|
|
private ProductSkuService productSkuService;
|
|
private ProductSkuService productSkuService;
|
|
|
@Resource
|
|
@Resource
|
|
|
private MemberPriceResolver memberPriceResolver;
|
|
private MemberPriceResolver memberPriceResolver;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private BundlePricingService bundlePricingService;
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private InventoryService inventoryService;
|
|
private InventoryService inventoryService;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private ProductBundleItemMapper bundleItemMapper;
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private CfCommissionService cfCommissionService;
|
|
private CfCommissionService cfCommissionService;
|
|
|
|
|
|
|
@@ -235,6 +246,13 @@ public class ProductOrderService {
|
|
|
&& product.getStock() < dto.getQuantity()) {
|
|
&& product.getStock() < dto.getQuantity()) {
|
|
|
return Result.error("库存不足");
|
|
return Result.error("库存不足");
|
|
|
}
|
|
}
|
|
|
|
|
+ // 套餐商品:检查子商品库存
|
|
|
|
|
+ if ("bundle".equals(product.getProductType())) {
|
|
|
|
|
+ Result<String> bundleCheck = inventoryService.checkBundleStock(product.getId(), dto.getQuantity());
|
|
|
|
|
+ if (bundleCheck.getCode() != 200) {
|
|
|
|
|
+ return Result.error(bundleCheck.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
// 校验最小购买数量
|
|
// 校验最小购买数量
|
|
|
if (product.getMinQuantity() != null && product.getMinQuantity() > 1
|
|
if (product.getMinQuantity() != null && product.getMinQuantity() > 1
|
|
|
&& dto.getQuantity() < product.getMinQuantity()) {
|
|
&& dto.getQuantity() < product.getMinQuantity()) {
|
|
@@ -258,7 +276,12 @@ public class ProductOrderService {
|
|
|
order.setPointsUsed(dto.getPointsUsed() != null ? dto.getPointsUsed() : 0);
|
|
order.setPointsUsed(dto.getPointsUsed() != null ? dto.getPointsUsed() : 0);
|
|
|
int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
|
|
int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
|
|
|
order.setPointsCost(pointsCost);
|
|
order.setPointsCost(pointsCost);
|
|
|
- int discountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0);
|
|
|
|
|
|
|
+ // 套餐自动定价优惠(服务端权威):按下单最终单价(含履约覆盖价)计算;前端 discountAmount 仅含规则优惠
|
|
|
|
|
+ int baseUnitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, buyerId);
|
|
|
|
|
+ int effectiveUnitPrice = fv.unitPrice != null ? fv.unitPrice : baseUnitPrice;
|
|
|
|
|
+ int bundleDiscount = bundlePricingService.compute(java.util.Collections.singletonList(
|
|
|
|
|
+ new LineItem(product.getId(), null, dto.getQuantity(), effectiveUnitPrice))).getDiscount();
|
|
|
|
|
+ int discountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0) + bundleDiscount;
|
|
|
// 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
|
|
// 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
|
|
|
Long appliedCouponId = null;
|
|
Long appliedCouponId = null;
|
|
|
if (dto.getCouponId() != null) {
|
|
if (dto.getCouponId() != null) {
|
|
@@ -390,6 +413,12 @@ public class ProductOrderService {
|
|
|
&& product.getStock() < quantity) {
|
|
&& product.getStock() < quantity) {
|
|
|
return Result.error("库存不足");
|
|
return Result.error("库存不足");
|
|
|
}
|
|
}
|
|
|
|
|
+ if ("bundle".equals(product.getProductType())) {
|
|
|
|
|
+ Result<String> bundleCheck = inventoryService.checkBundleStock(product.getId(), quantity);
|
|
|
|
|
+ if (bundleCheck.getCode() != 200) {
|
|
|
|
|
+ return Result.error(bundleCheck.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (product.getMinQuantity() != null && product.getMinQuantity() > 1
|
|
if (product.getMinQuantity() != null && product.getMinQuantity() > 1
|
|
|
&& quantity < product.getMinQuantity()) {
|
|
&& quantity < product.getMinQuantity()) {
|
|
|
return Result.error("该商品最少需购买" + product.getMinQuantity() + "件");
|
|
return Result.error("该商品最少需购买" + product.getMinQuantity() + "件");
|
|
@@ -727,9 +756,16 @@ public class ProductOrderService {
|
|
|
java.util.List<OrderItem> orderItemRecords = new java.util.ArrayList<>();
|
|
java.util.List<OrderItem> orderItemRecords = new java.util.ArrayList<>();
|
|
|
Product firstProduct = null;
|
|
Product firstProduct = null;
|
|
|
int firstUnitPrice = 0;
|
|
int firstUnitPrice = 0;
|
|
|
|
|
+ // 收集下单口径行项(解析后身份 + 结算单价),用于服务端权威的套餐自动定价
|
|
|
|
|
+ java.util.List<LineItem> bundleLines = new java.util.ArrayList<>();
|
|
|
|
|
+ Long comboSkuId = null; // 首个组合行 SKU(多商品订单组合可不在首行)
|
|
|
|
|
|
|
|
for (OrderItemVO item : dto.getItems()) {
|
|
for (OrderItemVO item : dto.getItems()) {
|
|
|
ProductSku itemSku = item.getSkuId() != null ? productSkuService.getById(item.getSkuId()) : null;
|
|
ProductSku itemSku = item.getSkuId() != null ? productSkuService.getById(item.getSkuId()) : null;
|
|
|
|
|
+ boolean comboItem = productSkuService.hasComboItems(itemSku);
|
|
|
|
|
+ if (comboItem && comboSkuId == null) {
|
|
|
|
|
+ comboSkuId = item.getSkuId();
|
|
|
|
|
+ }
|
|
|
Product product = productMapper.selectById(item.getProductId());
|
|
Product product = productMapper.selectById(item.getProductId());
|
|
|
if (product == null) {
|
|
if (product == null) {
|
|
|
return Result.error("商品不存在: " + item.getProductId());
|
|
return Result.error("商品不存在: " + item.getProductId());
|
|
@@ -737,21 +773,44 @@ public class ProductOrderService {
|
|
|
if (!"on_shelf".equals(product.getStatus())) {
|
|
if (!"on_shelf".equals(product.getStatus())) {
|
|
|
return Result.error("商品已下架: " + product.getName());
|
|
return Result.error("商品已下架: " + product.getName());
|
|
|
}
|
|
}
|
|
|
- // 实物商品校验库存;虚拟商品/优惠券跳过库存检查
|
|
|
|
|
- if ("physical".equals(product.getProductType())
|
|
|
|
|
|
|
+ // 实物商品校验库存;虚拟商品/优惠券跳过库存检查(组合商品按子商品校验)
|
|
|
|
|
+ if (!comboItem && "physical".equals(product.getProductType())
|
|
|
&& product.getStock() != null
|
|
&& product.getStock() != null
|
|
|
&& product.getStock() < item.getQuantity()) {
|
|
&& product.getStock() < item.getQuantity()) {
|
|
|
return Result.error("库存不足: " + product.getName());
|
|
return Result.error("库存不足: " + product.getName());
|
|
|
}
|
|
}
|
|
|
- // 套餐商品:校验套餐SKU库存(只扣套餐SKU,不扣子商品)
|
|
|
|
|
- if ("bundle".equals(product.getProductType())
|
|
|
|
|
- && itemSku != null
|
|
|
|
|
- && !productSkuService.hasStock(itemSku.getId(), item.getQuantity())) {
|
|
|
|
|
- return Result.error("套餐库存不足: " + product.getName());
|
|
|
|
|
|
|
+ // 套餐商品:检查子商品库存
|
|
|
|
|
+ if ("bundle".equals(product.getProductType())) {
|
|
|
|
|
+ Result<String> bundleCheck = inventoryService.checkBundleStock(product.getId(), item.getQuantity());
|
|
|
|
|
+ if (bundleCheck.getCode() != 200) {
|
|
|
|
|
+ return Result.error(bundleCheck.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 组合商品:校验各子商品库存(子 SKU / 子商品双通道)
|
|
|
|
|
+ if (comboItem) {
|
|
|
|
|
+ for (ProductSkuComboItem ci : itemSku.getComboItems()) {
|
|
|
|
|
+ int need = (ci.getQuantity() == null ? 1 : ci.getQuantity()) * item.getQuantity();
|
|
|
|
|
+ if (ci.getChildSkuId() != null) {
|
|
|
|
|
+ if (!productSkuService.hasStock(ci.getChildSkuId(), need)) {
|
|
|
|
|
+ return Result.error("组合商品库存不足: " + ci.getChildProductName());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Product cp = productMapper.selectById(ci.getChildProductId());
|
|
|
|
|
+ if (cp == null || cp.getStock() == null || cp.getStock() < need) {
|
|
|
|
|
+ return Result.error("组合商品库存不足: "
|
|
|
|
|
+ + (cp == null ? ci.getChildProductId() : cp.getName()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
int unitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, buyerId);
|
|
int unitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, buyerId);
|
|
|
if (itemSku != null) {
|
|
if (itemSku != null) {
|
|
|
- if (itemSku.getLinkedProductId() != null) {
|
|
|
|
|
|
|
+ if (comboItem) {
|
|
|
|
|
+ if (itemSku.getPrice() != null) unitPrice = itemSku.getPrice();
|
|
|
|
|
+ if (itemSku.getMinQuantity() != null && item.getQuantity() < itemSku.getMinQuantity()) {
|
|
|
|
|
+ return Result.error(product.getName() + " 该组合最低需购买" + itemSku.getMinQuantity() + "件");
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (itemSku.getLinkedProductId() != null) {
|
|
|
// Linked SKU: 转发到关联商品
|
|
// Linked SKU: 转发到关联商品
|
|
|
Product linkedProduct = productMapper.selectById(itemSku.getLinkedProductId());
|
|
Product linkedProduct = productMapper.selectById(itemSku.getLinkedProductId());
|
|
|
if (linkedProduct != null) {
|
|
if (linkedProduct != null) {
|
|
@@ -788,11 +847,14 @@ public class ProductOrderService {
|
|
|
itemRecord.setSkuId(item.getSkuId());
|
|
itemRecord.setSkuId(item.getSkuId());
|
|
|
itemRecord.setProductName(product.getName());
|
|
itemRecord.setProductName(product.getName());
|
|
|
itemRecord.setProductImage(product.getCoverImage());
|
|
itemRecord.setProductImage(product.getCoverImage());
|
|
|
- itemRecord.setSkuSpecs(item.getSpecOptionIds());
|
|
|
|
|
|
|
+ itemRecord.setSkuSpecs(comboItem
|
|
|
|
|
+ ? productSkuService.buildComboSummary(itemSku.getComboItems())
|
|
|
|
|
+ : item.getSpecOptionIds());
|
|
|
itemRecord.setPrice(java.math.BigDecimal.valueOf(unitPrice));
|
|
itemRecord.setPrice(java.math.BigDecimal.valueOf(unitPrice));
|
|
|
itemRecord.setQuantity(item.getQuantity());
|
|
itemRecord.setQuantity(item.getQuantity());
|
|
|
itemRecord.setSubtotal(java.math.BigDecimal.valueOf((long) unitPrice * item.getQuantity()));
|
|
itemRecord.setSubtotal(java.math.BigDecimal.valueOf((long) unitPrice * item.getQuantity()));
|
|
|
orderItemRecords.add(itemRecord);
|
|
orderItemRecords.add(itemRecord);
|
|
|
|
|
+ bundleLines.add(new LineItem(product.getId(), item.getSkuId(), item.getQuantity(), unitPrice));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
User buyer = userMapper.selectById(buyerId);
|
|
User buyer = userMapper.selectById(buyerId);
|
|
@@ -801,7 +863,7 @@ public class ProductOrderService {
|
|
|
order.setProductId(firstProduct.getId());
|
|
order.setProductId(firstProduct.getId());
|
|
|
order.setProductName(firstProduct.getName() + "等多件");
|
|
order.setProductName(firstProduct.getName() + "等多件");
|
|
|
order.setProductType(firstProduct.getProductType());
|
|
order.setProductType(firstProduct.getProductType());
|
|
|
- order.setSkuId(firstItem.getSkuId());
|
|
|
|
|
|
|
+ order.setSkuId(comboSkuId != null ? comboSkuId : firstItem.getSkuId());
|
|
|
order.setBuyerId(buyerId);
|
|
order.setBuyerId(buyerId);
|
|
|
order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
|
|
order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
|
|
|
order.setQuantity(totalQuantity);
|
|
order.setQuantity(totalQuantity);
|
|
@@ -812,7 +874,8 @@ public class ProductOrderService {
|
|
|
int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
|
|
int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
|
|
|
order.setPointsCost(pointsCost);
|
|
order.setPointsCost(pointsCost);
|
|
|
// 套餐自动定价优惠(服务端权威):前端 discountAmount 仅含规则优惠,套餐部分由服务端重算加入
|
|
// 套餐自动定价优惠(服务端权威):前端 discountAmount 仅含规则优惠,套餐部分由服务端重算加入
|
|
|
- int multiDiscountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0);
|
|
|
|
|
|
|
+ int bundleDiscount = bundlePricingService.compute(bundleLines).getDiscount();
|
|
|
|
|
+ int multiDiscountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0) + bundleDiscount;
|
|
|
// 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
|
|
// 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
|
|
|
Long appliedCouponId = null;
|
|
Long appliedCouponId = null;
|
|
|
if (dto.getCouponId() != null) {
|
|
if (dto.getCouponId() != null) {
|
|
@@ -1016,7 +1079,9 @@ public class ProductOrderService {
|
|
|
// 仅对实物商品扣减库存;虚拟商品/优惠券无库存概念
|
|
// 仅对实物商品扣减库存;虚拟商品/优惠券无库存概念
|
|
|
Product paidProduct = productMapper.selectById(order.getProductId());
|
|
Product paidProduct = productMapper.selectById(order.getProductId());
|
|
|
ProductSku paidSku = order.getSkuId() != null ? productSkuService.getById(order.getSkuId()) : null;
|
|
ProductSku paidSku = order.getSkuId() != null ? productSkuService.getById(order.getSkuId()) : null;
|
|
|
- if (paidProduct != null && "physical".equals(paidProduct.getProductType())) {
|
|
|
|
|
|
|
+ if (productSkuService.hasComboItems(paidSku)) {
|
|
|
|
|
+ deductComboStock(paidSku, order, orderNo);
|
|
|
|
|
+ } else if (paidProduct != null && "physical".equals(paidProduct.getProductType())) {
|
|
|
boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
|
|
boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
|
|
|
if (!stockOk) {
|
|
if (!stockOk) {
|
|
|
log.error("库存扣减失败: orderNo={}, productId={}, quantity={}",
|
|
log.error("库存扣减失败: orderNo={}, productId={}, quantity={}",
|
|
@@ -1029,19 +1094,29 @@ public class ProductOrderService {
|
|
|
// 记录出库流水
|
|
// 记录出库流水
|
|
|
inventoryService.recordOutbound(order.getProductId(), null, order.getQuantity(),
|
|
inventoryService.recordOutbound(order.getProductId(), null, order.getQuantity(),
|
|
|
"order", order.getId(), null, "订单支付:" + orderNo);
|
|
"order", order.getId(), null, "订单支付:" + orderNo);
|
|
|
- } else if (paidProduct != null && "bundle".equals(paidProduct.getProductType()) && paidSku != null) {
|
|
|
|
|
- // 套餐商品:只扣套餐SKU库存,不扣子商品库存
|
|
|
|
|
- boolean stockOk = productSkuService.decreaseStock(paidSku.getId(), order.getQuantity());
|
|
|
|
|
- if (!stockOk) {
|
|
|
|
|
- log.error("套餐库存扣减失败: orderNo={}, skuId={}, quantity={}",
|
|
|
|
|
- orderNo, paidSku.getId(), order.getQuantity());
|
|
|
|
|
- order.setRemark("套餐库存扣减失败");
|
|
|
|
|
- order.setUpdatedAt(new Date());
|
|
|
|
|
- orderMapper.updateById(order);
|
|
|
|
|
- throw new RuntimeException("套餐库存不足");
|
|
|
|
|
|
|
+ } else if (paidProduct != null && "bundle".equals(paidProduct.getProductType())) {
|
|
|
|
|
+ // 套餐商品:穿透到子商品扣减库存
|
|
|
|
|
+ List<ProductBundleItem> bundleItems = bundleItemMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<ProductBundleItem>()
|
|
|
|
|
+ .eq(ProductBundleItem::getBundleProductId, paidProduct.getId()));
|
|
|
|
|
+ for (ProductBundleItem bi : bundleItems) {
|
|
|
|
|
+ int need = bi.getQuantity() * order.getQuantity();
|
|
|
|
|
+ if (bi.getChildSkuId() != null) {
|
|
|
|
|
+ boolean skuOk = productSkuService.decreaseStock(bi.getChildSkuId(), need);
|
|
|
|
|
+ if (!skuOk) {
|
|
|
|
|
+ throw new RuntimeException("套餐子商品库存不足: bundleItemId=" + bi.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ inventoryService.recordOutbound(bi.getChildProductId(), bi.getChildSkuId(), need,
|
|
|
|
|
+ "order", order.getId(), null, "套餐出库:" + orderNo);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ boolean stockOk = productService.decreaseStock(bi.getChildProductId(), need);
|
|
|
|
|
+ if (!stockOk) {
|
|
|
|
|
+ throw new RuntimeException("套餐子商品库存不足: bundleItemId=" + bi.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ inventoryService.recordOutbound(bi.getChildProductId(), null, need,
|
|
|
|
|
+ "order", order.getId(), null, "套餐出库:" + orderNo);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- inventoryService.recordOutbound(order.getProductId(), paidSku.getId(), order.getQuantity(),
|
|
|
|
|
- "order", order.getId(), null, "订单支付:" + orderNo);
|
|
|
|
|
}
|
|
}
|
|
|
order.setStatus("paid");
|
|
order.setStatus("paid");
|
|
|
order.setTransactionId(transactionId);
|
|
order.setTransactionId(transactionId);
|
|
@@ -1215,6 +1290,73 @@ public class ProductOrderService {
|
|
|
return Result.success("支付成功");
|
|
return Result.success("支付成功");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组合 SKU 出库:按组合行子项(子 SKU / 子商品)扣减库存并记出库流水。
|
|
|
|
|
+ * 包级可见,供单测直接调用。任一子项库存不足抛 {@link RuntimeException} 由事务回滚。
|
|
|
|
|
+ */
|
|
|
|
|
+ void deductComboStock(ProductSku combo, ProductOrder order, String orderNo) {
|
|
|
|
|
+ for (ProductSkuComboItem ci : combo.getComboItems()) {
|
|
|
|
|
+ int unit = ci.getQuantity() == null ? 1 : ci.getQuantity();
|
|
|
|
|
+ int need = unit * comboLineQuantity(order);
|
|
|
|
|
+ if (ci.getChildSkuId() != null) {
|
|
|
|
|
+ if (!productSkuService.decreaseStock(ci.getChildSkuId(), need)) {
|
|
|
|
|
+ throw new RuntimeException("组合商品库存不足: skuId=" + ci.getChildSkuId());
|
|
|
|
|
+ }
|
|
|
|
|
+ inventoryService.recordOutbound(ci.getChildProductId(), ci.getChildSkuId(), need,
|
|
|
|
|
+ "order", order.getId(), null, "组合出库:" + orderNo);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (!productService.decreaseStock(ci.getChildProductId(), need)) {
|
|
|
|
|
+ throw new RuntimeException("组合商品库存不足: productId=" + ci.getChildProductId());
|
|
|
|
|
+ }
|
|
|
|
|
+ inventoryService.recordOutbound(ci.getChildProductId(), null, need,
|
|
|
|
|
+ "order", order.getId(), null, "组合出库:" + orderNo);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组合订单回补:若订单 SKU 为组合,回补各子项库存并记退款入库流水,返回 true;
|
|
|
|
|
+ * 非组合订单返回 false,由调用方回退到原有父商品回补逻辑。包级可见,供单测直接调用。
|
|
|
|
|
+ */
|
|
|
|
|
+ boolean restockComboOrder(ProductOrder order) {
|
|
|
|
|
+ ProductSku sku = order.getSkuId() != null ? productSkuService.getById(order.getSkuId()) : null;
|
|
|
|
|
+ if (!productSkuService.hasComboItems(sku)) return false;
|
|
|
|
|
+ for (ProductSkuComboItem ci : sku.getComboItems()) {
|
|
|
|
|
+ int unit = ci.getQuantity() == null ? 1 : ci.getQuantity();
|
|
|
|
|
+ int need = unit * comboLineQuantity(order);
|
|
|
|
|
+ if (ci.getChildSkuId() != null) {
|
|
|
|
|
+ productSkuService.increaseStock(ci.getChildSkuId(), need);
|
|
|
|
|
+ inventoryService.recordRefundInbound(ci.getChildProductId(), ci.getChildSkuId(), need,
|
|
|
|
|
+ "order", order.getId(), null, "组合退款入库:" + order.getOrderNo());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ productService.increaseStock(ci.getChildProductId(), need);
|
|
|
|
|
+ inventoryService.recordRefundInbound(ci.getChildProductId(), null, need,
|
|
|
|
|
+ "order", order.getId(), null, "组合退款入库:" + order.getOrderNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组合订单的组合行数量:查 order_items 中与订单 SKU 匹配行的数量合计。
|
|
|
|
|
+ * 混合订单(组合行 + 其他行)下 order.quantity 为订单总量,不能用于子项扣减/回补;
|
|
|
|
|
+ * 无匹配行(如旧数据/单测缺省)时回退 order.quantity。包级可见,供单测直接调用。
|
|
|
|
|
+ */
|
|
|
|
|
+ int comboLineQuantity(ProductOrder order) {
|
|
|
|
|
+ if (order.getId() == null || order.getSkuId() == null) {
|
|
|
|
|
+ return order.getQuantity() == null ? 1 : order.getQuantity();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<OrderItem> comboLines = orderItemMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<OrderItem>()
|
|
|
|
|
+ .eq(OrderItem::getOrderId, order.getId())
|
|
|
|
|
+ .eq(OrderItem::getSkuId, order.getSkuId()));
|
|
|
|
|
+ int q = 0;
|
|
|
|
|
+ for (OrderItem li : comboLines) {
|
|
|
|
|
+ q += li.getQuantity() == null ? 0 : li.getQuantity();
|
|
|
|
|
+ }
|
|
|
|
|
+ return q > 0 ? q : (order.getQuantity() == null ? 1 : order.getQuantity());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public Result<String> cancel(String orderNo, Long userId) {
|
|
public Result<String> cancel(String orderNo, Long userId) {
|
|
|
ProductOrder order = orderMapper.selectOne(
|
|
ProductOrder order = orderMapper.selectOne(
|
|
|
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
|
|
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
|
|
@@ -1234,15 +1376,13 @@ public class ProductOrderService {
|
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本,幂等 refId = 商品订单 ID)
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本,幂等 refId = 商品订单 ID)
|
|
|
refundOrderPoints(order, "取消订单退还CF值:" + orderNo);
|
|
refundOrderPoints(order, "取消订单退还CF值:" + orderNo);
|
|
|
- // Restore stock on cancel
|
|
|
|
|
- if ("bundle".equals(order.getProductType()) && order.getSkuId() != null) {
|
|
|
|
|
- productSkuService.increaseStock(order.getSkuId(), order.getQuantity());
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ // Restore stock on cancel(组合订单回补子商品,非组合回补父商品)
|
|
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
|
|
+ // 记录退款入库流水
|
|
|
|
|
+ inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
|
|
+ "order", order.getId(), null, "取消订单:" + orderNo);
|
|
|
}
|
|
}
|
|
|
- // 记录退款入库流水
|
|
|
|
|
- inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
|
|
- "order", order.getId(), null, "取消订单:" + orderNo);
|
|
|
|
|
// 特价订单回滚扣量(与恢复库存并列,失败仅记日志不阻断取消)
|
|
// 特价订单回滚扣量(与恢复库存并列,失败仅记日志不阻断取消)
|
|
|
try {
|
|
try {
|
|
|
specialPriceService.releaseQuota(order.getSpecialDealId(), order.getQuantity());
|
|
specialPriceService.releaseQuota(order.getSpecialDealId(), order.getQuantity());
|
|
@@ -1687,13 +1827,11 @@ public class ProductOrderService {
|
|
|
order.setStatus("refunded");
|
|
order.setStatus("refunded");
|
|
|
order.setUpdatedAt(new Date());
|
|
order.setUpdatedAt(new Date());
|
|
|
orderMapper.updateById(order);
|
|
orderMapper.updateById(order);
|
|
|
- if ("bundle".equals(order.getProductType()) && order.getSkuId() != null) {
|
|
|
|
|
- productSkuService.increaseStock(order.getSkuId(), order.getQuantity());
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
|
|
+ inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
|
|
+ "order", order.getId(), adminId, "退款:" + orderNo);
|
|
|
}
|
|
}
|
|
|
- inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
|
|
- "order", order.getId(), adminId, "退款:" + orderNo);
|
|
|
|
|
// 退款成功:归还优惠券;CF 兑换来的券同时退还 CF 值
|
|
// 退款成功:归还优惠券;CF 兑换来的券同时退还 CF 值
|
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本)
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本)
|
|
@@ -1909,9 +2047,7 @@ public class ProductOrderService {
|
|
|
return Result.error("该订单无法关闭");
|
|
return Result.error("该订单无法关闭");
|
|
|
}
|
|
}
|
|
|
if ("paid".equals(currentStatus) || "pending_receipt".equals(currentStatus)) {
|
|
if ("paid".equals(currentStatus) || "pending_receipt".equals(currentStatus)) {
|
|
|
- if ("bundle".equals(order.getProductType()) && order.getSkuId() != null) {
|
|
|
|
|
- productSkuService.increaseStock(order.getSkuId(), order.getQuantity());
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|