Przeglądaj źródła

feat(backend): 组合SKU购物车/下单/扣库/回补与详情展示

Xiaogang Liao 1 tydzień temu
rodzic
commit
f7ae859a29

+ 1 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java

@@ -120,7 +120,7 @@ public class ProductController {
             int groupId = 1;
 
             for (ProductSku sku : skus) {
-                // 组合 SKU: 返回 isCombo=true 与 comboItems 摘要
+                // 组合 SKU:单独成组(groupName=商品组合),每个选项附带子项列表;不进入 isLinked / legacy 分支
                 if (sku.getComboItems() != null && !sku.getComboItems().isEmpty()) {
                     String optionName = (sku.getLabel() != null && !sku.getLabel().trim().isEmpty())
                             ? sku.getLabel() : "商品组合";
@@ -161,7 +161,6 @@ public class ProductController {
                     }
                     continue;
                 }
-
                 boolean isLinked = sku.getLinkedProductId() != null;
 
                 if (isLinked) {

+ 2 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java

@@ -358,14 +358,11 @@ public class CartService {
         if (cart.getSkuId() != null) {
             ProductSku sku = productSkuService.getById(cart.getSkuId());
             if (sku != null) {
-                // 组合 SKU: 不替换商品,显示父商品名 + 组合摘要,价格取手填售价
+                // 组合 SKU:展示组合摘要,不转发、不解析 legacy specs
                 if (sku.getComboItems() != null && !sku.getComboItems().isEmpty()) {
-                    if (sku.getPrice() != null) {
-                        dto.setUnitPrice(sku.getPrice());
-                    }
+                    if (sku.getPrice() != null) dto.setUnitPrice(sku.getPrice());
                     dto.setSpecDesc(productSkuService.buildComboSummary(sku.getComboItems()));
                 } else if (sku.getLinkedProductId() != null) {
-                    // Linked SKU: 转发到关联商品
                     Product linkedProduct = productMapper.selectById(sku.getLinkedProductId());
                     if (linkedProduct != null) {
                         dto.setProductId(linkedProduct.getId());

+ 14 - 12
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -758,8 +758,14 @@ public class ProductOrderService {
         int firstUnitPrice = 0;
         // 收集下单口径行项(解析后身份 + 结算单价),用于服务端权威的套餐自动定价
         java.util.List<LineItem> bundleLines = new java.util.ArrayList<>();
+        Long comboSkuId = null; // 首个组合行 SKU(多商品订单组合可不在首行)
 
         for (OrderItemVO item : dto.getItems()) {
+            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());
             if (product == null) {
                 return Result.error("商品不存在: " + item.getProductId());
@@ -767,9 +773,7 @@ public class ProductOrderService {
             if (!"on_shelf".equals(product.getStatus())) {
                 return Result.error("商品已下架: " + product.getName());
             }
-            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()) {
@@ -782,7 +786,7 @@ public class ProductOrderService {
                     return Result.error(bundleCheck.getMessage());
                 }
             }
-            // 组合 SKU:校验各组成商品/SKU 库存
+            // 组合商品:校验各子商品库存(子 SKU / 子商品双通道)
             if (comboItem) {
                 for (ProductSkuComboItem ci : itemSku.getComboItems()) {
                     int need = (ci.getQuantity() == null ? 1 : ci.getQuantity()) * item.getQuantity();
@@ -859,7 +863,7 @@ public class ProductOrderService {
         order.setProductId(firstProduct.getId());
         order.setProductName(firstProduct.getName() + "等多件");
         order.setProductType(firstProduct.getProductType());
-        order.setSkuId(firstItem.getSkuId());
+        order.setSkuId(comboSkuId != null ? comboSkuId : firstItem.getSkuId());
         order.setBuyerId(buyerId);
         order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
         order.setQuantity(totalQuantity);
@@ -1287,8 +1291,8 @@ public class ProductOrderService {
     }
 
     /**
-     * 组合订单支付成功:按组合明细逐项扣减库存(不扣父商品库存)。
-     * 任一组成 SKU/商品库存不足抛异常,由外层事务整体回滚。
+     * 组合 SKU 出库:按组合行子项(子 SKU / 子商品)扣减库存并记出库流水。
+     * 包级可见,供单测直接调用。任一子项库存不足抛 {@link RuntimeException} 由事务回滚。
      */
     void deductComboStock(ProductSku combo, ProductOrder order, String orderNo) {
         for (ProductSkuComboItem ci : combo.getComboItems()) {
@@ -1311,9 +1315,8 @@ public class ProductOrderService {
     }
 
     /**
-     * 组合订单取消/退款:反向回补各组成商品/SKU 库存(不回补父商品)。
-     *
-     * @return true 如果该订单是组合订单且已完成回补;false 走原回补逻辑
+     * 组合订单回补:若订单 SKU 为组合,回补各子项库存并记退款入库流水,返回 true;
+     * 非组合订单返回 false,由调用方回退到原有父商品回补逻辑。包级可见,供单测直接调用。
      */
     boolean restockComboOrder(ProductOrder order) {
         ProductSku sku = order.getSkuId() != null ? productSkuService.getById(order.getSkuId()) : null;
@@ -1353,7 +1356,7 @@ public class ProductOrderService {
         releaseCouponForCancelledOrder(order.getCouponId());
         // F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本,幂等 refId = 商品订单 ID)
         refundOrderPoints(order, "取消订单退还CF值:" + orderNo);
-        // Restore stock on cancel(组合订单走组合回补,不回补父商品)
+        // Restore stock on cancel(组合订单回补子商品,非组合回补父商品)
         if (!restockComboOrder(order)) {
             productService.increaseStock(order.getProductId(), order.getQuantity());
             // 记录退款入库流水
@@ -2024,7 +2027,6 @@ public class ProductOrderService {
             return Result.error("该订单无法关闭");
         }
         if ("paid".equals(currentStatus) || "pending_receipt".equals(currentStatus)) {
-            // 组合订单走组合回补,不回补父商品
             if (!restockComboOrder(order)) {
                 productService.increaseStock(order.getProductId(), order.getQuantity());
             }

+ 7 - 7
cfc-backend/src/test/java/com/etotem/cfc/service/ProductOrderComboStockTest.java

@@ -17,10 +17,6 @@ import java.util.Collections;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
-/**
- * 组合 SKU 支付扣减 / 退款回补单测(TDD)。
- * 关键断言:子 SKU 与子商品分别扣减/回补,父商品库存不参与扣减与回补。
- */
 class ProductOrderComboStockTest {
 
     private ProductOrderService orderService;
@@ -41,9 +37,13 @@ class ProductOrderComboStockTest {
 
     private ProductSku combo() {
         ProductSkuComboItem a = new ProductSkuComboItem();
-        a.setChildProductId(10L); a.setChildSkuId(100L); a.setQuantity(2);
+        a.setChildProductId(10L);
+        a.setChildSkuId(100L);
+        a.setQuantity(2);
         ProductSkuComboItem b = new ProductSkuComboItem();
-        b.setChildProductId(11L); b.setChildSkuId(null); b.setQuantity(1);
+        b.setChildProductId(11L);
+        b.setChildSkuId(null);
+        b.setQuantity(1);
         ProductSku sku = new ProductSku();
         sku.setId(9L);
         sku.setComboItems(Arrays.asList(a, b));
@@ -102,4 +102,4 @@ class ProductOrderComboStockTest {
         // 父商品(order.productId)不得回补:本用例未设置 productId,仍断言未调用
         verify(productService, never()).increaseStock(null, 3);
     }
-}
+}