Просмотр исходного кода

refactor(backend): 移除SKU级组合机制(迁移438删表+库存/订单/编辑/详情引用清理)

Xiaogang Liao 1 неделя назад
Родитель
Сommit
f49ead1123

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3409,6 +3409,8 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移440: 套餐内容明细表(套餐=独立商品,内容=子商品SKU×数量,挂套餐SKU级)
         migrateProductComboItems();
 
+        // 迁移441: 删除旧 SKU 组合明细表(新模型:套餐内容在 product_combo_items,0数据安全删)
+        dropTableSkuCombo();
     }
 
     /**
@@ -3638,6 +3640,19 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         }
     }
 
+    /**
+     * 迁移438:删除旧 SKU 商品组合明细表 product_sku_combo_items。
+     * <p>新模型:组合机制移除,套餐=独立商品,内容在 product_combo_items(迁移437)。线上该表 0 数据,安全 DROP。
+     */
+    private void dropTableSkuCombo() {
+        try {
+            jdbcTemplate.execute("DROP TABLE IF EXISTS product_sku_combo_items");
+            log.info("迁移438: 已删除 product_sku_combo_items 表");
+        } catch (Exception e) {
+            log.warn("迁移438: 删除 product_sku_combo_items 表失败: {}", e.getMessage());
+        }
+    }
+
     private void migrateHealthDimensionsTable() {
         try {
             if (tableExists("health_dimension_score")) {

+ 0 - 15
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductPickerController.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.entity.ProductSkuComboItem;
 import com.etotem.cfc.mapper.ProductMapper;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -26,9 +25,6 @@ public class AdminProductPickerController {
 
     @Resource
     private com.etotem.cfc.mapper.ProductSkuMapper productSkuMapper;
-    @Resource
-    private com.etotem.cfc.mapper.ProductSkuComboItemMapper comboItemMapper;
-
     @PostMapping("/linked/list")
     public Result<Map<String, Object>> linkedList(@RequestBody Map<String, Object> params) {
         Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
@@ -70,19 +66,8 @@ public class AdminProductPickerController {
                         .filter(s -> s.getEnabled() != null && s.getEnabled() == 1)
                         .collect(java.util.stream.Collectors.toList());
                 m.put("hasSkus", !allSkus.isEmpty());
-                java.util.Set<Long> comboSkuIds = java.util.Collections.emptySet();
-                if (!allSkus.isEmpty()) {
-                    java.util.List<Long> ids = allSkus.stream().map(ProductSku::getId)
-                            .collect(java.util.stream.Collectors.toList());
-                    comboSkuIds = comboItemMapper.selectList(
-                                    new LambdaQueryWrapper<ProductSkuComboItem>()
-                                            .in(ProductSkuComboItem::getSkuId, ids)).stream()
-                            .map(ProductSkuComboItem::getSkuId)
-                            .collect(java.util.stream.Collectors.toSet());
-                }
                 java.util.List<java.util.Map<String, Object>> skuList = new java.util.ArrayList<>();
                 for (ProductSku s : allSkus) {
-                    if (comboSkuIds.contains(s.getId())) continue;
                     java.util.Map<String, Object> sm = new java.util.LinkedHashMap<>();
                     sm.put("id", s.getId());
                     sm.put("specs", s.getSpecs());

+ 0 - 42
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java

@@ -5,7 +5,6 @@ import com.etotem.cfc.dto.ProductDTO;
 import com.etotem.cfc.dto.ProductListQueryDTO;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.entity.ProductSkuComboItem;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.PpointService;
@@ -120,47 +119,6 @@ public class ProductController {
             int groupId = 1;
 
             for (ProductSku sku : skus) {
-                // 组合 SKU:单独成组(groupName=商品组合),每个选项附带子项列表;不进入 isLinked / legacy 分支
-                if (sku.getComboItems() != null && !sku.getComboItems().isEmpty()) {
-                    String optionName = (sku.getLabel() != null && !sku.getLabel().trim().isEmpty())
-                            ? sku.getLabel() : "商品组合";
-                    String groupName = "商品组合";
-                    if (!nameToGroupId.containsKey(groupName)) {
-                        nameToGroupId.put(groupName, groupId++);
-                        Map<String, Object> grp = new HashMap<>();
-                        grp.put("groupId", nameToGroupId.get(groupName));
-                        grp.put("groupName", groupName);
-                        grp.put("options", new ArrayList<Map<String, Object>>());
-                        groups.add(grp);
-                    }
-                    List<Map<String, Object>> comboList = new ArrayList<>();
-                    for (ProductSkuComboItem ci : sku.getComboItems()) {
-                        Map<String, Object> cm = new HashMap<>();
-                        cm.put("productId", ci.getChildProductId());
-                        cm.put("skuId", ci.getChildSkuId());
-                        cm.put("name", ci.getChildProductName());
-                        cm.put("image", ci.getChildProductImage());
-                        cm.put("price", ci.getChildProductPrice());
-                        cm.put("quantity", ci.getQuantity());
-                        comboList.add(cm);
-                    }
-                    Map<String, Object> option = new HashMap<>();
-                    option.put("id", sku.getId());
-                    option.put("groupId", nameToGroupId.get(groupName));
-                    option.put("name", optionName);
-                    option.put("skuId", sku.getId());
-                    option.put("isCombo", true);
-                    option.put("price", sku.getPrice());
-                    option.put("comboItems", comboList);
-                    for (Map<String, Object> g : groups) {
-                        if (g.get("groupId").equals(nameToGroupId.get(groupName))) {
-                            @SuppressWarnings("unchecked")
-                            List<Map<String, Object>> opts = (List<Map<String, Object>>) g.get("options");
-                            opts.add(option);
-                        }
-                    }
-                    continue;
-                }
                 boolean isLinked = sku.getLinkedProductId() != null;
 
                 if (isLinked) {

+ 0 - 2
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductSkuDTO.java

@@ -22,7 +22,6 @@ public class ProductSkuDTO {
     private Integer linkedProductPrice;
     private String linkedProductImage;
     private Integer linkedProductStock;
-    private java.util.List<com.etotem.cfc.entity.ProductSkuComboItem> comboItems;
 
     public static ProductSkuDTO from(ProductSku sku) {
         if (sku == null) return null;
@@ -76,7 +75,6 @@ public class ProductSkuDTO {
         dto.setLinkedProductPrice(sku.getLinkedProductPrice());
         dto.setLinkedProductImage(sku.getLinkedProductImage());
         dto.setLinkedProductStock(sku.getLinkedProductStock());
-        dto.setComboItems(sku.getComboItems());
         return dto;
     }
 }

+ 0 - 3
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java

@@ -31,9 +31,6 @@ public class ProductSku implements Serializable {
     private Date createTime;
     private Date updateTime;
 
-    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
-    private java.util.List<ProductSkuComboItem> comboItems;
-
     @com.baomidou.mybatisplus.annotation.TableField(exist = false)
     private String linkedProductName;
     @com.baomidou.mybatisplus.annotation.TableField(exist = false)

+ 0 - 33
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSkuComboItem.java

@@ -1,33 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.Date;
-
-@Data
-@TableName("product_sku_combo_items")
-public class ProductSkuComboItem implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    private Long skuId;
-    private Long childProductId;
-    private Long childSkuId;
-    private Integer quantity;
-    private Date createdAt;
-
-    @TableField(exist = false)
-    private String childProductName;
-    @TableField(exist = false)
-    private String childProductImage;
-    @TableField(exist = false)
-    private Integer childProductPrice;
-    @TableField(exist = false)
-    private String childSkuSpecs;
-}

+ 0 - 7
cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductSkuComboItemMapper.java

@@ -1,7 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.ProductSkuComboItem;
-
-public interface ProductSkuComboItemMapper extends BaseMapper<ProductSkuComboItem> {
-}

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

@@ -358,11 +358,7 @@ public class CartService {
         if (cart.getSkuId() != null) {
             ProductSku sku = productSkuService.getById(cart.getSkuId());
             if (sku != null) {
-                // 组合 SKU:展示组合摘要,不转发、不解析 legacy specs
-                if (sku.getComboItems() != null && !sku.getComboItems().isEmpty()) {
-                    if (sku.getPrice() != null) dto.setUnitPrice(sku.getPrice());
-                    dto.setSpecDesc(productSkuService.buildComboSummary(sku.getComboItems()));
-                } else if (sku.getLinkedProductId() != null) {
+                if (sku.getLinkedProductId() != null) {
                     Product linkedProduct = productMapper.selectById(sku.getLinkedProductId());
                     if (linkedProduct != null) {
                         dto.setProductId(linkedProduct.getId());

+ 15 - 120
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -44,7 +44,6 @@ 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;
@@ -758,14 +757,9 @@ 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());
@@ -773,8 +767,8 @@ public class ProductOrderService {
             if (!"on_shelf".equals(product.getStatus())) {
                 return Result.error("商品已下架: " + product.getName());
             }
-            // 实物商品校验库存;虚拟商品/优惠券跳过库存检查(组合商品按子商品校验)
-            if (!comboItem && "physical".equals(product.getProductType())
+            // 实物商品校验库存;虚拟商品/优惠券跳过库存检查
+            if ("physical".equals(product.getProductType())
                     && product.getStock() != null
                     && product.getStock() < item.getQuantity()) {
                 return Result.error("库存不足: " + product.getName());
@@ -786,31 +780,9 @@ public class ProductOrderService {
                     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);
             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) {
+                if (itemSku.getLinkedProductId() != null) {
                     // Linked SKU: 转发到关联商品
                     Product linkedProduct = productMapper.selectById(itemSku.getLinkedProductId());
                     if (linkedProduct != null) {
@@ -847,9 +819,7 @@ public class ProductOrderService {
             itemRecord.setSkuId(item.getSkuId());
             itemRecord.setProductName(product.getName());
             itemRecord.setProductImage(product.getCoverImage());
-            itemRecord.setSkuSpecs(comboItem
-                    ? productSkuService.buildComboSummary(itemSku.getComboItems())
-                    : item.getSpecOptionIds());
+            itemRecord.setSkuSpecs(item.getSpecOptionIds());
             itemRecord.setPrice(java.math.BigDecimal.valueOf(unitPrice));
             itemRecord.setQuantity(item.getQuantity());
             itemRecord.setSubtotal(java.math.BigDecimal.valueOf((long) unitPrice * item.getQuantity()));
@@ -863,7 +833,7 @@ public class ProductOrderService {
         order.setProductId(firstProduct.getId());
         order.setProductName(firstProduct.getName() + "等多件");
         order.setProductType(firstProduct.getProductType());
-        order.setSkuId(comboSkuId != null ? comboSkuId : firstItem.getSkuId());
+        order.setSkuId(firstItem.getSkuId());
         order.setBuyerId(buyerId);
         order.setFamilyId(buyer != null ? buyer.getFamilyId() : null);
         order.setQuantity(totalQuantity);
@@ -1079,9 +1049,7 @@ public class ProductOrderService {
         // 仅对实物商品扣减库存;虚拟商品/优惠券无库存概念
         Product paidProduct = productMapper.selectById(order.getProductId());
         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())) {
+        if (paidProduct != null && "physical".equals(paidProduct.getProductType())) {
             boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity());
             if (!stockOk) {
                 log.error("库存扣减失败: orderNo={}, productId={}, quantity={}",
@@ -1290,73 +1258,6 @@ public class ProductOrderService {
         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) {
         ProductOrder order = orderMapper.selectOne(
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
@@ -1376,13 +1277,11 @@ public class ProductOrderService {
         releaseCouponForCancelledOrder(order.getCouponId());
         // F-5:退还下单时抵扣的 CF 值(原路返还到购买人 member 账本,幂等 refId = 商品订单 ID)
         refundOrderPoints(order, "取消订单退还CF值:" + 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);
-        }
+        // Restore stock on cancel
+        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());
@@ -1827,11 +1726,9 @@ public class ProductOrderService {
         order.setStatus("refunded");
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
-        if (!restockComboOrder(order)) {
-            productService.increaseStock(order.getProductId(), order.getQuantity());
-            inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(),
-                    "order", order.getId(), adminId, "退款:" + orderNo);
-        }
+        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 账本)
@@ -2047,9 +1944,7 @@ public class ProductOrderService {
             return Result.error("该订单无法关闭");
         }
         if ("paid".equals(currentStatus) || "pending_receipt".equals(currentStatus)) {
-            if (!restockComboOrder(order)) {
-                productService.increaseStock(order.getProductId(), order.getQuantity());
-            }
+            productService.increaseStock(order.getProductId(), order.getQuantity());
         }
         order.setStatus("closed");
         order.setUpdatedAt(new Date());

+ 0 - 106
cfc-backend/src/main/java/com/etotem/cfc/service/ProductSkuService.java

@@ -1,11 +1,8 @@
 package com.etotem.cfc.service;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.entity.ProductSkuComboItem;
 import com.etotem.cfc.mapper.ProductMapper;
-import com.etotem.cfc.mapper.ProductSkuComboItemMapper;
 import com.etotem.cfc.mapper.ProductSkuMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -29,26 +26,18 @@ public class ProductSkuService {
     @Resource
     private ProductMapper productMapper;
 
-    @Resource
-    private ProductSkuComboItemMapper comboItemMapper;
-
     public List<ProductSku> listByProductId(Long productId) {
         List<ProductSku> list = skuMapper.findByProductId(productId);
-        fillComboItems(list);
         return list;
     }
 
     public List<ProductSku> listByProductIdAll(Long productId) {
         List<ProductSku> list = skuMapper.findByProductIdAll(productId);
-        fillComboItems(list);
         return list;
     }
 
     public ProductSku getById(Long id) {
         ProductSku sku = skuMapper.findById(id);
-        if (sku != null) {
-            fillComboItems(java.util.Collections.singletonList(sku));
-        }
         return sku;
     }
 
@@ -69,18 +58,12 @@ public class ProductSkuService {
         if (sku.getSkuCode() == null || sku.getSkuCode().isEmpty()) {
             sku.setSkuCode("SKU-" + sku.getProductId() + "-" + System.currentTimeMillis());
         }
-        validateComboItems(sku.getComboItems());
         skuMapper.insert(sku);
-        saveComboItems(sku.getId(), sku.getComboItems());
         return sku;
     }
 
     @Transactional
     public void update(ProductSku sku) {
-        validateComboItems(sku.getComboItems());
-        if (sku.getComboItems() != null && !sku.getComboItems().isEmpty()) {
-            sku.setLinkedProductId(null);
-        }
         if (sku.getLinkedProductId() == null) {
             // Legacy SKU: 需要 specs
             if (sku.getSpecs() == null || sku.getSpecs().trim().isEmpty()) {
@@ -89,7 +72,6 @@ public class ProductSkuService {
         }
         sku.setUpdateTime(new Date());
         skuMapper.updateById(sku);
-        saveComboItems(sku.getId(), sku.getComboItems());
         if (sku.getLinkedProductId() == null) {
             skuMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProductSku>()
                     .eq(ProductSku::getId, sku.getId())
@@ -99,8 +81,6 @@ public class ProductSkuService {
 
     @Transactional
     public void delete(Long id) {
-        comboItemMapper.delete(new LambdaQueryWrapper<ProductSkuComboItem>()
-                .eq(ProductSkuComboItem::getSkuId, id));
         skuMapper.deleteById(id);
     }
 
@@ -236,90 +216,4 @@ public class ProductSkuService {
                 .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
     }
 
-    private void saveComboItems(Long skuId, List<ProductSkuComboItem> items) {
-        if (items == null) return;
-        comboItemMapper.delete(new LambdaQueryWrapper<ProductSkuComboItem>()
-                .eq(ProductSkuComboItem::getSkuId, skuId));
-        if (items.isEmpty()) return;
-        for (ProductSkuComboItem item : items) {
-            item.setId(null);
-            item.setSkuId(skuId);
-            if (item.getQuantity() == null || item.getQuantity() < 1) item.setQuantity(1);
-            comboItemMapper.insert(item);
-        }
-    }
-
-    private void validateComboItems(List<ProductSkuComboItem> items) {
-        if (items == null || items.isEmpty()) return;
-        for (ProductSkuComboItem item : items) {
-            if (item.getChildProductId() == null) throw new IllegalArgumentException("组合商品不能为空");
-            List<ProductSku> childSkus = skuMapper.findByProductId(item.getChildProductId());
-            boolean hasEnabledSku = childSkus != null
-                    && childSkus.stream().anyMatch(s -> s.getEnabled() != null && s.getEnabled() == 1);
-            if (hasEnabledSku && item.getChildSkuId() == null) {
-                throw new IllegalArgumentException("组合商品存在 SKU,必须选择具体 SKU");
-            }
-        }
-    }
-
-    public void fillComboItems(List<ProductSku> skus) {
-        if (skus == null || skus.isEmpty()) return;
-        List<Long> skuIds = skus.stream().map(ProductSku::getId)
-                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toList());
-        if (skuIds.isEmpty()) return;
-        List<ProductSkuComboItem> all = comboItemMapper.selectList(
-                new LambdaQueryWrapper<ProductSkuComboItem>().in(ProductSkuComboItem::getSkuId, skuIds));
-        if (all.isEmpty()) {
-            skus.forEach(s -> s.setComboItems(java.util.Collections.emptyList()));
-            return;
-        }
-        java.util.Set<Long> productIds = all.stream().map(ProductSkuComboItem::getChildProductId)
-                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toSet());
-        java.util.Map<Long, Product> productMap = productIds.isEmpty() ? java.util.Collections.emptyMap()
-                : productMapper.selectBatchIds(productIds).stream()
-                    .collect(java.util.stream.Collectors.toMap(Product::getId, p -> p, (a, b) -> a));
-        java.util.Set<Long> childSkuIds = all.stream().map(ProductSkuComboItem::getChildSkuId)
-                .filter(java.util.Objects::nonNull).collect(java.util.stream.Collectors.toSet());
-        java.util.Map<Long, ProductSku> childSkuMap = childSkuIds.isEmpty() ? java.util.Collections.emptyMap()
-                : skuMapper.selectBatchIds(childSkuIds).stream()
-                    .collect(java.util.stream.Collectors.toMap(ProductSku::getId, s -> s, (a, b) -> a));
-        for (ProductSkuComboItem item : all) {
-            Product p = productMap.get(item.getChildProductId());
-            if (p != null) {
-                item.setChildProductName(p.getName());
-                item.setChildProductImage(p.getCoverImage());
-                item.setChildProductPrice(p.getPrice());
-            }
-            ProductSku cs = item.getChildSkuId() == null ? null : childSkuMap.get(item.getChildSkuId());
-            if (cs != null) item.setChildSkuSpecs(cs.getSpecs());
-        }
-        java.util.Map<Long, List<ProductSkuComboItem>> grouped = all.stream()
-                .collect(java.util.stream.Collectors.groupingBy(ProductSkuComboItem::getSkuId));
-        for (ProductSku sku : skus) {
-            sku.setComboItems(grouped.getOrDefault(sku.getId(), java.util.Collections.emptyList()));
-        }
-    }
-
-    public boolean hasComboItems(ProductSku sku) {
-        return sku != null && sku.getComboItems() != null && !sku.getComboItems().isEmpty();
-    }
-
-    public List<ProductSkuComboItem> listComboItems(Long skuId) {
-        ProductSku tmp = new ProductSku();
-        tmp.setId(skuId);
-        fillComboItems(java.util.Collections.singletonList(tmp));
-        return tmp.getComboItems() == null ? java.util.Collections.emptyList() : tmp.getComboItems();
-    }
-
-    public String buildComboSummary(List<ProductSkuComboItem> items) {
-        if (items == null || items.isEmpty()) return "";
-        StringBuilder sb = new StringBuilder();
-        for (ProductSkuComboItem item : items) {
-            if (sb.length() > 0) sb.append(";");
-            String name = item.getChildProductName() != null ? item.getChildProductName()
-                    : ("商品#" + item.getChildProductId());
-            sb.append(name).append("×").append(item.getQuantity() == null ? 1 : item.getQuantity());
-        }
-        return sb.toString();
-    }
 }

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

@@ -1,129 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.etotem.cfc.entity.OrderItem;
-import com.etotem.cfc.entity.ProductOrder;
-import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.entity.ProductSkuComboItem;
-import com.etotem.cfc.mapper.OrderItemMapper;
-import com.etotem.cfc.service.InventoryService;
-import com.etotem.cfc.service.ProductOrderService;
-import com.etotem.cfc.service.ProductService;
-import com.etotem.cfc.service.ProductSkuService;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-class ProductOrderComboStockTest {
-
-    private ProductOrderService orderService;
-    private ProductSkuService skuService;
-    private ProductService productService;
-    private InventoryService inventoryService;
-    private OrderItemMapper orderItemMapper;
-
-    @BeforeEach
-    void setUp() {
-        orderService = new ProductOrderService();
-        skuService = mock(ProductSkuService.class);
-        productService = mock(ProductService.class);
-        inventoryService = mock(InventoryService.class);
-        orderItemMapper = mock(OrderItemMapper.class);
-        ReflectionTestUtils.setField(orderService, "productSkuService", skuService);
-        ReflectionTestUtils.setField(orderService, "productService", productService);
-        ReflectionTestUtils.setField(orderService, "inventoryService", inventoryService);
-        ReflectionTestUtils.setField(orderService, "orderItemMapper", orderItemMapper);
-    }
-
-    private ProductSku combo() {
-        ProductSkuComboItem a = new ProductSkuComboItem();
-        a.setChildProductId(10L);
-        a.setChildSkuId(100L);
-        a.setQuantity(2);
-        ProductSkuComboItem b = new ProductSkuComboItem();
-        b.setChildProductId(11L);
-        b.setChildSkuId(null);
-        b.setQuantity(1);
-        ProductSku sku = new ProductSku();
-        sku.setId(9L);
-        sku.setComboItems(Arrays.asList(a, b));
-        return sku;
-    }
-
-    private ProductOrder order() {
-        ProductOrder o = new ProductOrder();
-        o.setId(77L);
-        o.setOrderNo("PO1");
-        o.setSkuId(9L);
-        o.setQuantity(3);
-        return o;
-    }
-
-    @Test
-    void deductComboStock_childSku_decreasesSkuAndRecordsOutbound() {
-        when(skuService.decreaseStock(100L, 6)).thenReturn(true);
-        when(productService.decreaseStock(11L, 3)).thenReturn(true);
-
-        orderService.deductComboStock(combo(), order(), "PO1");
-
-        verify(skuService).decreaseStock(100L, 6);
-        verify(inventoryService).recordOutbound(10L, 100L, 6, "order", 77L, null, "组合出库:PO1");
-        verify(productService).decreaseStock(11L, 3);
-        verify(inventoryService).recordOutbound(11L, null, 3, "order", 77L, null, "组合出库:PO1");
-    }
-
-    @Test
-    void deductComboStock_insufficient_throws() {
-        when(skuService.decreaseStock(100L, 6)).thenReturn(false);
-
-        assertThrows(RuntimeException.class, () -> orderService.deductComboStock(combo(), order(), "PO1"));
-    }
-
-    @Test
-    void restockComboOrder_nonCombo_returnsFalse() {
-        when(skuService.getById(9L)).thenReturn(new ProductSku());
-        when(skuService.hasComboItems(any())).thenReturn(false);
-
-        assertFalse(orderService.restockComboOrder(order()));
-        verify(productService, never()).increaseStock(anyLong(), anyInt());
-    }
-
-    @Test
-    void restockComboOrder_combo_restocksChildrenOnly() {
-        ProductSku sku = combo();
-        when(skuService.getById(9L)).thenReturn(sku);
-        when(skuService.hasComboItems(sku)).thenReturn(true);
-
-        assertTrue(orderService.restockComboOrder(order()));
-
-        verify(skuService).increaseStock(100L, 6);
-        verify(inventoryService).recordRefundInbound(10L, 100L, 6, "order", 77L, null, "组合退款入库:PO1");
-        verify(productService).increaseStock(11L, 3);
-        // 父商品(order.productId)不得回补:本用例未设置 productId,仍断言未调用
-        verify(productService, never()).increaseStock(null, 3);
-    }
-
-    @Test
-    void deductComboStock_mixedOrder_usesComboLineQuantity() {
-        ProductOrder o = order();
-        o.setQuantity(6); // 订单总数量含其他行,组合行自身仅 1
-        OrderItem comboLine = new OrderItem();
-        comboLine.setSkuId(9L);
-        comboLine.setQuantity(1);
-        when(orderItemMapper.selectList(any())).thenReturn(java.util.Collections.singletonList(comboLine));
-        when(skuService.decreaseStock(100L, 2)).thenReturn(true); // 2(组合unit) × 1(行数量)
-        when(productService.decreaseStock(11L, 1)).thenReturn(true);
-
-        orderService.deductComboStock(combo(), o, "PO1");
-
-        verify(skuService).decreaseStock(100L, 2);
-        verify(inventoryService).recordOutbound(10L, 100L, 2, "order", 77L, null, "组合出库:PO1");
-        verify(productService).decreaseStock(11L, 1);
-        verify(inventoryService).recordOutbound(11L, null, 1, "order", 77L, null, "组合出库:PO1");
-    }
-}

+ 0 - 117
cfc-backend/src/test/java/com/etotem/cfc/unit/ProductSkuComboServiceTest.java

@@ -1,117 +0,0 @@
-package com.etotem.cfc.unit;
-
-import com.baomidou.mybatisplus.core.MybatisConfiguration;
-import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
-import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.entity.ProductSkuComboItem;
-import org.apache.ibatis.builder.MapperBuilderAssistant;
-import com.etotem.cfc.mapper.ProductMapper;
-import com.etotem.cfc.mapper.ProductSkuComboItemMapper;
-import com.etotem.cfc.mapper.ProductSkuMapper;
-import com.etotem.cfc.service.ProductSkuService;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.util.Collections;
-import java.util.Arrays;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.*;
-
-class ProductSkuComboServiceTest {
-
-    private ProductSkuService service;
-    private ProductSkuMapper skuMapper;
-    private ProductMapper productMapper;
-    private ProductSkuComboItemMapper comboItemMapper;
-
-    @BeforeEach
-    void setUp() {
-        // LambdaUpdateWrapper/LambdaQueryWrapper 解析列名依赖 MyBatis-Plus TableInfo 缓存(与 ProductUpdateClearTest 同约定)
-        MapperBuilderAssistant assistant =
-                new MapperBuilderAssistant(new MybatisConfiguration(), "");
-        TableInfoHelper.initTableInfo(assistant, ProductSku.class);
-        service = new ProductSkuService();
-        skuMapper = mock(ProductSkuMapper.class);
-        productMapper = mock(ProductMapper.class);
-        comboItemMapper = mock(ProductSkuComboItemMapper.class);
-        ReflectionTestUtils.setField(service, "skuMapper", skuMapper);
-        ReflectionTestUtils.setField(service, "productMapper", productMapper);
-        ReflectionTestUtils.setField(service, "comboItemMapper", comboItemMapper);
-    }
-
-    private ProductSkuComboItem item(Long productId, Long skuId, int qty) {
-        ProductSkuComboItem it = new ProductSkuComboItem();
-        it.setChildProductId(productId);
-        it.setChildSkuId(skuId);
-        it.setQuantity(qty);
-        return it;
-    }
-
-    @Test
-    void create_withComboItems_persistsRowsWithSkuId() {
-        ProductSku sku = new ProductSku();
-        sku.setProductId(1L);
-        sku.setComboItems(Arrays.asList(item(10L, 100L, 2), item(11L, null, 1)));
-        when(skuMapper.findByProductId(10L)).thenReturn(Collections.emptyList());
-        when(skuMapper.findByProductId(11L)).thenReturn(Collections.emptyList());
-        doAnswer(inv -> { sku.setId(55L); return 1; }).when(skuMapper).insert(any(ProductSku.class));
-
-        service.create(sku);
-
-        verify(comboItemMapper, times(2)).insert(any(ProductSkuComboItem.class));
-        assertEquals(55L, sku.getComboItems().get(0).getSkuId());
-    }
-
-    @Test
-    void update_emptyComboItems_deletesRows() {
-        ProductSku sku = new ProductSku();
-        sku.setId(55L);
-        sku.setComboItems(Collections.emptyList());
-
-        service.update(sku);
-
-        verify(comboItemMapper).delete(any());
-        verify(comboItemMapper, never()).insert(any());
-    }
-
-    @Test
-    void update_comboItems_clearsLinkedProductId() {
-        ProductSku sku = new ProductSku();
-        sku.setId(55L);
-        sku.setComboItems(Collections.singletonList(item(10L, 100L, 1)));
-        when(skuMapper.findByProductId(10L)).thenReturn(Collections.emptyList());
-
-        service.update(sku);
-
-        verify(skuMapper).update(eq(null), any());
-    }
-
-    @Test
-    void create_childProductHasEnabledSku_withoutChildSkuId_throws() {
-        ProductSku childSku = new ProductSku();
-        childSku.setId(100L);
-        childSku.setEnabled(1);
-        when(skuMapper.findByProductId(10L)).thenReturn(Collections.singletonList(childSku));
-
-        ProductSku sku = new ProductSku();
-        sku.setProductId(1L);
-        sku.setComboItems(Collections.singletonList(item(10L, null, 1)));
-
-        assertThrows(IllegalArgumentException.class, () -> service.create(sku));
-        verify(skuMapper, never()).insert(any(ProductSku.class));
-    }
-
-    @Test
-    void buildComboSummary_formatsNameAndQuantity() {
-        ProductSkuComboItem a = item(10L, null, 2);
-        a.setChildProductName("商品A");
-        ProductSkuComboItem b = item(11L, null, 1);
-        b.setChildProductName("商品B");
-
-        assertEquals("商品A×2;商品B×1", service.buildComboSummary(Arrays.asList(a, b)));
-    }
-}