|
|
@@ -44,6 +44,7 @@ import com.etotem.cfc.mapper.ProductOrderMapper;
|
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
|
import com.etotem.cfc.entity.ProductBundleItem;
|
|
|
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.AssessmentProductService;
|
|
|
@@ -766,8 +767,10 @@ public class ProductOrderService {
|
|
|
if (!"on_shelf".equals(product.getStatus())) {
|
|
|
return Result.error("商品已下架: " + product.getName());
|
|
|
}
|
|
|
- // 实物商品校验库存;虚拟商品/优惠券跳过库存检查
|
|
|
- if ("physical".equals(product.getProductType())
|
|
|
+ ProductSku itemSku = item.getSkuId() != null ? productSkuService.getById(item.getSkuId()) : null;
|
|
|
+ boolean comboItem = productSkuService.hasComboItems(itemSku);
|
|
|
+ // 实物商品校验库存;虚拟商品/优惠券跳过库存检查(组合行由组成商品库存校验,不拦截父商品)
|
|
|
+ if (!comboItem && "physical".equals(product.getProductType())
|
|
|
&& product.getStock() != null
|
|
|
&& product.getStock() < item.getQuantity()) {
|
|
|
return Result.error("库存不足: " + product.getName());
|
|
|
@@ -779,28 +782,47 @@ public class ProductOrderService {
|
|
|
return Result.error(bundleCheck.getMessage());
|
|
|
}
|
|
|
}
|
|
|
- int unitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, buyerId);
|
|
|
- if (item.getSkuId() != null) {
|
|
|
- ProductSku sku = productSkuService.getById(item.getSkuId());
|
|
|
- if (sku != null) {
|
|
|
- // Linked SKU: 转发到关联商品
|
|
|
- if (sku.getLinkedProductId() != null) {
|
|
|
- Product linkedProduct = productMapper.selectById(sku.getLinkedProductId());
|
|
|
- if (linkedProduct != null) {
|
|
|
- product = linkedProduct;
|
|
|
- unitPrice = linkedProduct.getPrice() != null ? linkedProduct.getPrice() : 0;
|
|
|
+ // 组合 SKU:校验各组成商品/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 {
|
|
|
- // Legacy SKU: 用 SKU 价格
|
|
|
- if (sku.getMinQuantity() != null && item.getQuantity() < sku.getMinQuantity()) {
|
|
|
- return Result.error(product.getName() + " 该规格最低需购买" + sku.getMinQuantity() + "件");
|
|
|
- }
|
|
|
- if (sku.getPrice() != null) {
|
|
|
- unitPrice = sku.getPrice();
|
|
|
+ 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);
|
|
|
+ if (itemSku != 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: 转发到关联商品
|
|
|
+ Product linkedProduct = productMapper.selectById(itemSku.getLinkedProductId());
|
|
|
+ if (linkedProduct != null) {
|
|
|
+ product = linkedProduct;
|
|
|
+ unitPrice = linkedProduct.getPrice() != null ? linkedProduct.getPrice() : 0;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Legacy SKU: 用 SKU 价格
|
|
|
+ if (itemSku.getMinQuantity() != null && item.getQuantity() < itemSku.getMinQuantity()) {
|
|
|
+ return Result.error(product.getName() + " 该规格最低需购买" + itemSku.getMinQuantity() + "件");
|
|
|
+ }
|
|
|
+ if (itemSku.getPrice() != null) {
|
|
|
+ unitPrice = itemSku.getPrice();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
// 产品级最小购买数量检查(当没有SKU或SKU不限制时)
|
|
|
if (item.getSkuId() == null && product.getMinQuantity() != null && item.getQuantity() < product.getMinQuantity()) {
|
|
|
return Result.error(product.getName() + " 最低需购买" + product.getMinQuantity() + "件");
|
|
|
@@ -821,7 +843,9 @@ public class ProductOrderService {
|
|
|
itemRecord.setSkuId(item.getSkuId());
|
|
|
itemRecord.setProductName(product.getName());
|
|
|
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.setQuantity(item.getQuantity());
|
|
|
itemRecord.setSubtotal(java.math.BigDecimal.valueOf((long) unitPrice * item.getQuantity()));
|
|
|
@@ -835,6 +859,7 @@ public class ProductOrderService {
|
|
|
order.setProductId(firstProduct.getId());
|
|
|
order.setProductName(firstProduct.getName() + "等多件");
|
|
|
order.setProductType(firstProduct.getProductType());
|
|
|
+ order.setSkuId(firstItem.getSkuId());
|
|
|
order.setBuyerId(buyerId);
|
|
|
order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
|
|
|
order.setQuantity(totalQuantity);
|
|
|
@@ -1049,7 +1074,10 @@ public class ProductOrderService {
|
|
|
}
|
|
|
// 仅对实物商品扣减库存;虚拟商品/优惠券无库存概念
|
|
|
Product paidProduct = productMapper.selectById(order.getProductId());
|
|
|
- if (paidProduct != null && "physical".equals(paidProduct.getProductType())) {
|
|
|
+ ProductSku paidSku = order.getSkuId() != null ? productSkuService.getById(order.getSkuId()) : null;
|
|
|
+ if (productSkuService.hasComboItems(paidSku)) {
|
|
|
+ deductComboStock(paidSku, order, orderNo);
|
|
|
+ } else if (paidProduct != null && "physical".equals(paidProduct.getProductType())) {
|
|
|
boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
|
|
|
if (!stockOk) {
|
|
|
log.error("库存扣减失败: orderNo={}, productId={}, quantity={}",
|
|
|
@@ -1258,6 +1286,54 @@ public class ProductOrderService {
|
|
|
return Result.success("支付成功");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 组合订单支付成功:按组合明细逐项扣减库存(不扣父商品库存)。
|
|
|
+ * 任一组成 SKU/商品库存不足抛异常,由外层事务整体回滚。
|
|
|
+ */
|
|
|
+ void deductComboStock(ProductSku combo, ProductOrder order, String orderNo) {
|
|
|
+ for (ProductSkuComboItem ci : combo.getComboItems()) {
|
|
|
+ int unit = ci.getQuantity() == null ? 1 : ci.getQuantity();
|
|
|
+ int need = unit * order.getQuantity();
|
|
|
+ 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 库存(不回补父商品)。
|
|
|
+ *
|
|
|
+ * @return 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 * order.getQuantity();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
public Result<String> cancel(String orderNo, Long userId) {
|
|
|
ProductOrder order = orderMapper.selectOne(
|
|
|
new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
|
|
|
@@ -1277,11 +1353,13 @@ public class ProductOrderService {
|
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本,幂等 refId = 商品订单 ID)
|
|
|
refundOrderPoints(order, "取消订单退还CF值:" + orderNo);
|
|
|
- // Restore stock on cancel
|
|
|
- productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
- // 记录退款入库流水
|
|
|
- inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
- "order", order.getId(), null, "取消订单:" + orderNo);
|
|
|
+ // Restore stock on cancel(组合订单走组合回补,不回补父商品)
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
+ productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
+ // 记录退款入库流水
|
|
|
+ inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
+ "order", order.getId(), null, "取消订单:" + orderNo);
|
|
|
+ }
|
|
|
// 特价订单回滚扣量(与恢复库存并列,失败仅记日志不阻断取消)
|
|
|
try {
|
|
|
specialPriceService.releaseQuota(order.getSpecialDealId(), order.getQuantity());
|
|
|
@@ -1726,9 +1804,11 @@ public class ProductOrderService {
|
|
|
order.setStatus("refunded");
|
|
|
order.setUpdatedAt(new Date());
|
|
|
orderMapper.updateById(order);
|
|
|
- productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
- inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
- "order", order.getId(), adminId, "退款:" + orderNo);
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
+ productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
+ inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
|
|
|
+ "order", order.getId(), adminId, "退款:" + orderNo);
|
|
|
+ }
|
|
|
// 退款成功:归还优惠券;CF 兑换来的券同时退还 CF 值
|
|
|
releaseCouponForCancelledOrder(order.getCouponId());
|
|
|
// F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本)
|
|
|
@@ -1944,7 +2024,10 @@ public class ProductOrderService {
|
|
|
return Result.error("该订单无法关闭");
|
|
|
}
|
|
|
if ("paid".equals(currentStatus) || "pending_receipt".equals(currentStatus)) {
|
|
|
- productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
+ // 组合订单走组合回补,不回补父商品
|
|
|
+ if (!restockComboOrder(order)) {
|
|
|
+ productService.increaseStock(order.getProductId(), order.getQuantity());
|
|
|
+ }
|
|
|
}
|
|
|
order.setStatus("closed");
|
|
|
order.setUpdatedAt(new Date());
|