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

refactor(backend): 删除bundle计价机制(迁移439删product_bundle_items+定价/库存/购物车/详情引用清理)

Xiaogang Liao 1 неделя назад
Родитель
Сommit
6d528dd7a3
22 измененных файлов с 20 добавлено и 1560 удалено
  1. 17 13
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 0 29
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java
  3. 0 63
      cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java
  4. 0 3
      cfc-backend/src/main/java/com/etotem/cfc/dto/CartListResultDTO.java
  5. 0 13
      cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java
  6. 0 23
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductBundleItem.java
  7. 0 9
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductBundleItemMapper.java
  8. 0 22
      cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleChild.java
  9. 0 31
      cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleDef.java
  10. 0 32
      cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleDiscountResult.java
  11. 0 33
      cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleHit.java
  12. 0 216
      cfc-backend/src/main/java/com/etotem/cfc/pricing/BundlePricingCalculator.java
  13. 0 85
      cfc-backend/src/main/java/com/etotem/cfc/service/BundlePricingService.java
  14. 0 226
      cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java
  15. 0 76
      cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java
  16. 2 64
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  17. 1 26
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java
  18. 0 25
      cfc-backend/src/main/resources/schema.sql
  19. 0 168
      cfc-backend/src/test/java/com/etotem/cfc/unit/BundlePricingCalculatorTest.java
  20. 0 126
      cfc-backend/src/test/java/com/etotem/cfc/unit/BundlePricingServiceTest.java
  21. 0 137
      cfc-backend/src/test/java/com/etotem/cfc/unit/CartServiceTest.java
  22. 0 140
      cfc-backend/src/test/java/com/etotem/cfc/unit/ProductOrderServiceTest.java

+ 17 - 13
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3411,6 +3411,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移441: 删除旧 SKU 组合明细表(新模型:套餐内容在 product_combo_items,0数据安全删)
         dropTableSkuCombo();
+
+        // 迁移439: 删除旧 bundle 机制明细表(新模型:套餐=独立商品,内容在 product_combo_items,0数据安全删)
+        dropTableBundleItems();
     }
 
     /**
@@ -3653,6 +3656,19 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         }
     }
 
+    /**
+     * 迁移439:删除旧 bundle 商品组成明细表 product_bundle_items。
+     * <p>新模型:bundle 机制移除,套餐=独立商品,内容在 product_combo_items(迁移437)。线上该表 0 数据,安全 DROP。
+     */
+    private void dropTableBundleItems() {
+        try {
+            jdbcTemplate.execute("DROP TABLE IF EXISTS product_bundle_items");
+            log.info("迁移439: 已删除 product_bundle_items 表");
+        } catch (Exception e) {
+            log.warn("迁移439: 删除 product_bundle_items 表失败: {}", e.getMessage());
+        }
+    }
+
     private void migrateHealthDimensionsTable() {
         try {
             if (tableExists("health_dimension_score")) {
@@ -15022,19 +15038,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.warn("迁移426: 创建 attribution_reports 表失败(可能已存在): {}", e.getMessage());
         }
 
-        // 迁移435: product_bundle_items 增加套餐SKU锚点列(订单套餐自动定价)
-        // bundle_sku_id 非空 = 定价套餐行(套餐价取该SKU.price,CartService/ProductOrderService 匹配用);
-        // 为空 = 仅出库联动行(InventoryService 语义,不参与定价)。
-        ensureColumn("product_bundle_items", "bundle_sku_id",
-                "BIGINT NULL COMMENT '套餐SKU锚点:非空=订单套餐自动定价(套餐价取该SKU.price);空=仅出库联动'");
-        // 同时放开 bundle_product_id 可空:定价套餐行置 NULL,避免 InventoryService
-        // (按 bundle_product_id 关联、仅 productType=bundle 商品)误触发出库联动。
-        try {
-            jdbcTemplate.execute("ALTER TABLE product_bundle_items MODIFY COLUMN bundle_product_id BIGINT NULL COMMENT '套餐商品ID(出库联动行=套餐商品ID;定价套餐行置NULL)'");
-            log.info("迁移435: product_bundle_items.bundle_product_id 已放开可空");
-        } catch (Exception e) {
-            log.warn("迁移427: 放开 bundle_product_id 可空失败(可能已放开): {}", e.getMessage());
-        }
+
     }
 
     private static String text(JsonNode row, String field) {

+ 0 - 29
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java

@@ -1,7 +1,6 @@
 package com.etotem.cfc.controller.admin;
 
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductComboItem;
 import com.etotem.cfc.service.ComboService;
 import com.etotem.cfc.service.InventoryService;
@@ -154,24 +153,6 @@ public class AdminInventoryController {
 
     // ========== 套餐商品 ==========
 
-    @Operation(summary = "配置套餐商品组成")
-    @PostMapping("/bundle/items")
-    public Result<String> saveBundleItems(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
-        if (bundleProductId == null) return Result.error("bundleProductId不能为空");
-        @SuppressWarnings("unchecked")
-        List<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
-        return inventoryService.saveBundleItems(bundleProductId, items);
-    }
-
-    @Operation(summary = "查询套餐商品组成")
-    @PostMapping("/bundle/items/list")
-    public Result<List<ProductBundleItem>> listBundleItems(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
-        if (bundleProductId == null) return Result.error("bundleProductId不能为空");
-        return inventoryService.listBundleItems(bundleProductId);
-    }
-
     @Operation(summary = "保存套餐内容")
     @PostMapping("/combo/items")
     public Result<String> saveComboItems(@RequestBody Map<String, Object> params) {
@@ -199,16 +180,6 @@ public class AdminInventoryController {
         }
     }
 
-    @Operation(summary = "检查套餐库存")
-    @PostMapping("/bundle/check-stock")
-    public Result<String> checkBundleStock(@RequestBody Map<String, Object> params) {
-        Long bundleProductId = ParamUtils.getLong(params.get("bundleProductId"));
-        Integer quantity = params.get("quantity") != null
-                ? Integer.valueOf(params.get("quantity").toString()) : 1;
-        if (bundleProductId == null) return Result.error("bundleProductId不能为空");
-        return inventoryService.checkBundleStock(bundleProductId, quantity);
-    }
-
     // ========== 手动出库 ==========
 
     @Operation(summary = "手动出库")

+ 0 - 63
cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java

@@ -20,40 +20,6 @@ public class CartItemDTO {
     private String giftLevelCode;
     private String giftLevelName;
 
-    // 购物车项本身就是套餐商品(productType=bundle)时非空:展示套餐组成
-    private BundleInfo bundleInfo;
-
-    // 购物车项为子商品、且与其它子商品恰好凑成某套餐时非空:自动套餐模式
-    private BundleMatch bundleMatch;
-
-    @Data
-    public static class BundleInfo {
-        private Long bundleProductId;
-        private List<Long> childProductIds;
-        private List<String> childProductNames;
-        private Integer childQuantityTotal;
-        private Long bundlePrice;
-        private Integer singleTotalPrice;
-        private Integer saving;
-    }
-
-    @Data
-    public static class BundleMatch {
-        private Long bundleProductId;
-        private String bundleProductName;
-        private Integer matchedGroups;
-        private Long bundlePrice;
-        private Integer singleTotalPrice;
-        private Integer saving;
-        private List<BundleChild> children;
-    }
-
-    @Data
-    public static class BundleChild {
-        private Long childProductId;
-        private Integer quantity;
-    }
-
     @Data
     public static class GiftPromo {
         /** 送会员商品原价合计(分) */
@@ -70,33 +36,4 @@ public class CartItemDTO {
         private Integer remaining;
     }
 
-    @Data
-    public static class BundlePromo {
-        /** 是否命中至少一套套餐(节省额>0) */
-        private Boolean active;
-        /** 命中套餐部分的基础款原价合计(分) */
-        private Integer originalTotal;
-        /** 命中套餐价合计(分) */
-        private Integer payPrice;
-        /** 套餐节省额合计(分) */
-        private Integer discount;
-        /** 命中套餐明细 */
-        private List<BundleItemHit> bundleList;
-    }
-
-    @Data
-    public static class BundleItemHit {
-        /** 套餐锚点 SKU id */
-        private Long bundleSkuId;
-        /** 套餐展示名 */
-        private String bundleName;
-        /** 命中套数 */
-        private Integer times;
-        /** 命中部分原价合计(分) */
-        private Integer originalTotal;
-        /** 套餐价合计(分) */
-        private Integer bundlePrice;
-        /** 节省额(分) */
-        private Integer saving;
-    }
 }

+ 0 - 3
cfc-backend/src/main/java/com/etotem/cfc/dto/CartListResultDTO.java

@@ -9,7 +9,4 @@ public class CartListResultDTO {
     private List<CartItemDTO> items;
 
     private CartItemDTO.GiftPromo giftPromo;
-
-    /** 订单套餐自动定价优惠(product_bundle_items 定价行 bundle_sku_id 匹配) */
-    private CartItemDTO.BundlePromo bundlePromo;
 }

+ 0 - 13
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

@@ -78,22 +78,9 @@ public class ProductDTO {
     // 配送与履约配置(按 deliveryMethod 附带 pickupLocations/servicePersons/onlineSlots)
     private Map<String, Object> deliveryConfig;
 
-    // 套餐商品组成(仅 productType=bundle 时非空)
-    private List<BundleItemVO> bundleItems;
-
     /** 套餐内容(新模型:套餐=独立商品,内容=子商品SKU×数量,挂套餐SKU级) */
     private List<ComboItemVO> comboItems;
 
-    @Data
-    public static class BundleItemVO {
-        private Long childProductId;
-        private String childProductName;
-        private String coverImage;
-        private Long childSkuId;
-        private String specs;        // SKU 规格描述(无 SKU 时为空)
-        private Integer quantity;
-    }
-
     @Data
     public static class ComboItemVO {
         private Long childProductId;

+ 0 - 23
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductBundleItem.java

@@ -1,23 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-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_bundle_items")
-public class ProductBundleItem implements Serializable {
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private Long bundleProductId;
-    /** 套餐SKU锚点:非空=定价套餐行(套餐价取该SKU.price,用于订单套餐自动定价匹配);为空=仅出库联动行 */
-    private Long bundleSkuId;
-    private Long childProductId;
-    private Long childSkuId;
-    private Integer quantity;
-    private Date createdAt;
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductBundleItemMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.ProductBundleItem;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface ProductBundleItemMapper extends BaseMapper<ProductBundleItem> {
-}

+ 0 - 22
cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleChild.java

@@ -1,22 +0,0 @@
-package com.etotem.cfc.pricing;
-
-import lombok.Data;
-
-/**
- * 套餐基础款组成:child_product_id 必填;child_sku_id 可空(空=按商品聚合所有 SKU 行);quantity=组成数量。
- */
-@Data
-public class BundleChild {
-    private Long childProductId;
-    private Long childSkuId;
-    private Integer quantity;
-
-    public BundleChild() {
-    }
-
-    public BundleChild(Long childProductId, Long childSkuId, Integer quantity) {
-        this.childProductId = childProductId;
-        this.childSkuId = childSkuId;
-        this.quantity = quantity;
-    }
-}

+ 0 - 31
cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleDef.java

@@ -1,31 +0,0 @@
-package com.etotem.cfc.pricing;
-
-import lombok.Data;
-
-import java.util.List;
-
-/**
- * 定价套餐定义(来自 product_bundle_items 中 bundle_sku_id 非空的行,按套餐锚点 SKU 聚合)。
- * bundlePrice 取锚点 SKU 的 price;children 为该套餐的基础款组成。
- */
-@Data
-public class BundleDef {
-    /** 套餐锚点 SKU id(product_skus.id) */
-    private Long bundleSkuId;
-    /** 套餐展示名(取锚点 SKU 的 label,缺省回落 specs) */
-    private String bundleName;
-    /** 套餐价(分) */
-    private Integer bundlePrice;
-    /** 基础款组成(每行: child_product_id + 可选 child_sku_id + quantity) */
-    private List<BundleChild> children;
-
-    public BundleDef() {
-    }
-
-    public BundleDef(Long bundleSkuId, String bundleName, Integer bundlePrice, List<BundleChild> children) {
-        this.bundleSkuId = bundleSkuId;
-        this.bundleName = bundleName;
-        this.bundlePrice = bundlePrice;
-        this.children = children;
-    }
-}

+ 0 - 32
cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleDiscountResult.java

@@ -1,32 +0,0 @@
-package com.etotem.cfc.pricing;
-
-import lombok.Data;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 订单套餐定价结果:active=至少命中一套(节省额>0)。
- * originalTotal=命中套餐部分的基础款原价合计;payPrice=命中套餐价合计;discount=节省额合计。
- * bundleList=命中套餐明细(含套数/原价/套餐价/节省)。
- */
-@Data
-public class BundleDiscountResult {
-    private boolean active;
-    private Integer originalTotal;
-    private Integer payPrice;
-    private Integer discount;
-    private List<BundleHit> bundleList;
-
-    public BundleDiscountResult() {
-        this.active = false;
-        this.originalTotal = 0;
-        this.payPrice = 0;
-        this.discount = 0;
-        this.bundleList = new ArrayList<>();
-    }
-
-    public static BundleDiscountResult inactive() {
-        return new BundleDiscountResult();
-    }
-}

+ 0 - 33
cfc-backend/src/main/java/com/etotem/cfc/pricing/BundleHit.java

@@ -1,33 +0,0 @@
-package com.etotem.cfc.pricing;
-
-import lombok.Data;
-
-/**
- * 命中的套餐(一次套餐 = 该套餐按组成完整消费 1 套)。
- */
-@Data
-public class BundleHit {
-    private Long bundleSkuId;
-    private String bundleName;
-    /** 命中套数 */
-    private Integer times;
-    /** 命中部分的基础款原价合计(消费的行 unitPrice×qty) */
-    private Integer originalTotal;
-    /** 套餐总价(bundlePrice × times) */
-    private Integer bundlePrice;
-    /** 节省额(originalTotal − bundlePrice) */
-    private Integer saving;
-
-    public BundleHit() {
-    }
-
-    public BundleHit(Long bundleSkuId, String bundleName, Integer times,
-                     Integer originalTotal, Integer bundlePrice, Integer saving) {
-        this.bundleSkuId = bundleSkuId;
-        this.bundleName = bundleName;
-        this.times = times;
-        this.originalTotal = originalTotal;
-        this.bundlePrice = bundlePrice;
-        this.saving = saving;
-    }
-}

+ 0 - 216
cfc-backend/src/main/java/com/etotem/cfc/pricing/BundlePricingCalculator.java

@@ -1,216 +0,0 @@
-package com.etotem.cfc.pricing;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 订单套餐自动定价核心算法(纯计算,无外部依赖,便于单测)。
- *
- * 规则:
- * 1. 商品行按 (productId, skuId) 聚合为「池」;
- * 2. 多套餐可凑时贪心取「节省额最大」者,每轮命中 1 套并扣减组成数量;
- * 3. 节省额 = 该套消费的基础款单价合计 − 套餐价,仅 >0 才触发(避免会员价低于套餐价反而更贵);
- * 4. 超出套餐组成数量的数量留给调用方按单品兜底,本算法不参与兜底计价。
- */
-public class BundlePricingCalculator {
-
-    /** 池内一行:某 (productId, skuId) 剩余数量与下单口径单价 */
-    private static class PoolEntry {
-        final Long productId;
-        final Long skuId;
-        int quantity;
-        final int unitPrice;
-
-        PoolEntry(Long productId, Long skuId, int quantity, int unitPrice) {
-            this.productId = productId;
-            this.skuId = skuId;
-            this.quantity = quantity;
-            this.unitPrice = unitPrice;
-        }
-    }
-
-    /** 某套餐的消费方案:从池中扣哪些行、各扣多少 */
-    private static class ConsumePlan {
-        final BundleDef def;
-        final List<PoolEntry> entries = new ArrayList<>();
-        final List<Integer> quantities = new ArrayList<>();
-        int originalTotal; // 该套消费的基础款单价合计
-
-        ConsumePlan(BundleDef def) {
-            this.def = def;
-        }
-
-        int saving() {
-            return originalTotal - safePrice(def.getBundlePrice());
-        }
-
-        int consumedOf(PoolEntry entry) {
-            int total = 0;
-            for (int i = 0; i < entries.size(); i++) {
-                if (entries.get(i) == entry) {
-                    total += quantities.get(i);
-                }
-            }
-            return total;
-        }
-    }
-
-    /**
-     * 计算订单内套餐优惠。
-     *
-     * @param lines 下单/结算口径商品行(productId/skuId 为解析后身份,unitPrice 为结算单价,单位分)
-     * @param defs  定价套餐定义(product_bundle_items 中 bundle_sku_id 非空的行按锚点聚合)
-     */
-    public BundleDiscountResult compute(List<LineItem> lines, List<BundleDef> defs) {
-        BundleDiscountResult result = new BundleDiscountResult();
-        if (lines == null || lines.isEmpty() || defs == null || defs.isEmpty()) {
-            return result;
-        }
-        Map<String, PoolEntry> pool = aggregate(lines);
-
-        // 同一套餐锚点命中多套时合并为一条 BundleHit(times>1)
-        Map<Long, BundleHit> hitByBundle = new LinkedHashMap<>();
-        while (true) {
-            ConsumePlan best = bestBundle(pool, defs);
-            if (best == null) {
-                break;
-            }
-            for (int i = 0; i < best.entries.size(); i++) {
-                PoolEntry entry = best.entries.get(i);
-                entry.quantity -= best.quantities.get(i);
-                if (entry.quantity < 0) {
-                    entry.quantity = 0;
-                }
-            }
-            pool.values().removeIf(e -> e.quantity <= 0);
-
-            BundleHit hit = hitByBundle.get(best.def.getBundleSkuId());
-            if (hit == null) {
-                hit = new BundleHit();
-                hit.setBundleSkuId(best.def.getBundleSkuId());
-                hit.setBundleName(best.def.getBundleName());
-                hit.setTimes(0);
-                hit.setOriginalTotal(0);
-                hit.setBundlePrice(0);
-                hit.setSaving(0);
-                hitByBundle.put(best.def.getBundleSkuId(), hit);
-            }
-            hit.setTimes(hit.getTimes() + 1);
-            hit.setOriginalTotal(hit.getOriginalTotal() + best.originalTotal);
-            hit.setBundlePrice(hit.getBundlePrice() + safePrice(best.def.getBundlePrice()));
-            hit.setSaving(hit.getSaving() + best.saving());
-        }
-
-        if (!hitByBundle.isEmpty()) {
-            result.setActive(true);
-            result.setBundleList(new ArrayList<>(hitByBundle.values()));
-            for (BundleHit hit : result.getBundleList()) {
-                result.setOriginalTotal(result.getOriginalTotal() + hit.getOriginalTotal());
-                result.setPayPrice(result.getPayPrice() + hit.getBundlePrice());
-                result.setDiscount(result.getDiscount() + hit.getSaving());
-            }
-        }
-        return result;
-    }
-
-    /** 聚合:同 (productId, skuId) 归并数量;单价取该聚合内首个非零值 */
-    private Map<String, PoolEntry> aggregate(List<LineItem> lines) {
-        Map<String, PoolEntry> pool = new LinkedHashMap<>();
-        for (LineItem line : lines) {
-            if (line == null || line.getProductId() == null
-                    || line.getQuantity() == null || line.getQuantity() <= 0) {
-                continue;
-            }
-            String key = line.getProductId() + "|" + (line.getSkuId() == null ? "" : line.getSkuId());
-            int unitPrice = safePrice(line.getUnitPrice());
-            PoolEntry entry = pool.get(key);
-            if (entry == null) {
-                pool.put(key, new PoolEntry(line.getProductId(), line.getSkuId(), line.getQuantity(), unitPrice));
-            } else {
-                entry.quantity += line.getQuantity();
-            }
-        }
-        return pool;
-    }
-
-    /** 在池上贪心找当前可凑且节省额最大的套餐(每轮只命中 1 套) */
-    private ConsumePlan bestBundle(Map<String, PoolEntry> pool, List<BundleDef> defs) {
-        ConsumePlan best = null;
-        int bestSaving = 0;
-        for (BundleDef def : defs) {
-            if (def == null || def.getBundlePrice() == null || def.getBundlePrice() <= 0) {
-                continue;
-            }
-            if (def.getChildren() == null || def.getChildren().isEmpty()) {
-                continue; // 无组成定义,防御脏数据
-            }
-            ConsumePlan trial = trialConsume(pool, def);
-            if (trial == null || trial.saving() <= 0) {
-                continue;
-            }
-            // 严格大于:节省额相同时取先出现的套餐(确定性)
-            if (trial.saving() > bestSaving) {
-                bestSaving = trial.saving();
-                best = trial;
-            }
-        }
-        return best;
-    }
-
-    /**
-     * 试凑一套套餐:不修改池,返回消费方案;任一基础款不足则返回 null。
-     * 同一 PoolEntry 可能被不同 child 消费(跨 child 共行),用 drawn 防重复扣。
-     */
-    private ConsumePlan trialConsume(Map<String, PoolEntry> pool, BundleDef def) {
-        ConsumePlan plan = new ConsumePlan(def);
-        Map<PoolEntry, Integer> drawn = new HashMap<>();
-        for (BundleChild child : def.getChildren()) {
-            if (child == null || child.getChildProductId() == null) {
-                return null;
-            }
-            int needQty = child.getQuantity() == null || child.getQuantity() <= 0 ? 1 : child.getQuantity();
-            boolean skuSpecified = child.getChildSkuId() != null;
-            int remaining = needQty;
-            for (PoolEntry entry : pool.values()) {
-                if (remaining <= 0) {
-                    break;
-                }
-                if (!matches(entry, child, skuSpecified)) {
-                    continue;
-                }
-                int already = drawn.getOrDefault(entry, 0);
-                int available = entry.quantity - already;
-                if (available <= 0) {
-                    continue;
-                }
-                int take = Math.min(available, remaining);
-                plan.entries.add(entry);
-                plan.quantities.add(take);
-                drawn.put(entry, already + take);
-                plan.originalTotal += entry.unitPrice * take;
-                remaining -= take;
-            }
-            if (remaining > 0) {
-                return null; // 该基础款数量不足
-            }
-        }
-        return plan;
-    }
-
-    private boolean matches(PoolEntry entry, BundleChild child, boolean skuSpecified) {
-        if (!entry.productId.equals(child.getChildProductId())) {
-            return false;
-        }
-        if (skuSpecified) {
-            return entry.skuId != null && entry.skuId.equals(child.getChildSkuId());
-        }
-        return true; // 未指定 childSkuId:按商品聚合,同商品任意 SKU 行都算
-    }
-
-    private static int safePrice(Integer price) {
-        return price == null || price < 0 ? 0 : price;
-    }
-}

+ 0 - 85
cfc-backend/src/main/java/com/etotem/cfc/service/BundlePricingService.java

@@ -1,85 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.etotem.cfc.entity.ProductBundleItem;
-import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.mapper.ProductBundleItemMapper;
-import com.etotem.cfc.pricing.BundleChild;
-import com.etotem.cfc.pricing.BundleDef;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.BundlePricingCalculator;
-import com.etotem.cfc.pricing.LineItem;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * 订单套餐自动定价:装载 product_bundle_items 中 bundle_sku_id 非空的定价套餐定义,
- * 组装为 {@link BundleDef} 后委托 {@link BundlePricingCalculator} 计算。
- */
-@Service
-public class BundlePricingService {
-
-    private final ProductBundleItemMapper productBundleItemMapper;
-    private final ProductSkuService productSkuService;
-    private final BundlePricingCalculator calculator = new BundlePricingCalculator();
-
-    public BundlePricingService(ProductBundleItemMapper productBundleItemMapper,
-                                ProductSkuService productSkuService) {
-        this.productBundleItemMapper = productBundleItemMapper;
-        this.productSkuService = productSkuService;
-    }
-
-    /**
-     * 计算订单内套餐优惠。
-     *
-     * @param lines 下单/结算口径商品行(productId/skuId 为解析后身份,unitPrice 为结算单价)
-     */
-    public BundleDiscountResult compute(List<LineItem> lines) {
-        List<ProductBundleItem> rows = productBundleItemMapper.selectList(
-                new QueryWrapper<ProductBundleItem>().isNotNull("bundle_sku_id"));
-        if (rows == null || rows.isEmpty()) {
-            return BundleDiscountResult.inactive();
-        }
-        // 按套餐锚点 SKU 分组组装定义
-        Map<Long, BundleDef> defMap = new LinkedHashMap<>();
-        for (ProductBundleItem row : rows) {
-            if (row == null || row.getBundleSkuId() == null || row.getChildProductId() == null) {
-                continue; // 脏数据防御
-            }
-            BundleDef def = defMap.get(row.getBundleSkuId());
-            if (def == null) {
-                ProductSku sku = productSkuService.getById(row.getBundleSkuId());
-                if (sku == null || sku.getPrice() == null || sku.getPrice() <= 0) {
-                    continue; // 锚点 SKU 不存在或无价,跳过该套餐(防御)
-                }
-                def = new BundleDef();
-                def.setBundleSkuId(row.getBundleSkuId());
-                def.setBundleName(chooseName(sku));
-                def.setBundlePrice(sku.getPrice());
-                def.setChildren(new ArrayList<>());
-                defMap.put(row.getBundleSkuId(), def);
-            }
-            BundleChild child = new BundleChild();
-            child.setChildProductId(row.getChildProductId());
-            child.setChildSkuId(row.getChildSkuId());
-            child.setQuantity(row.getQuantity() == null || row.getQuantity() <= 0 ? 1 : row.getQuantity());
-            def.getChildren().add(child);
-        }
-        if (defMap.isEmpty()) {
-            return BundleDiscountResult.inactive();
-        }
-        return calculator.compute(lines, new ArrayList<>(defMap.values()));
-    }
-
-    /** 套餐展示名:锚点 SKU 的 label 优先,缺省回落 specs */
-    private String chooseName(ProductSku sku) {
-        if (sku.getLabel() != null && !sku.getLabel().trim().isEmpty()) {
-            return sku.getLabel();
-        }
-        return sku.getSpecs();
-    }
-}

+ 0 - 226
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java

@@ -5,15 +5,10 @@ import com.etotem.cfc.dto.CartItemDTO;
 import com.etotem.cfc.dto.CartListResultDTO;
 import com.etotem.cfc.entity.Cart;
 import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductSku;
 import com.etotem.cfc.entity.ProductGiftRule;
 import com.etotem.cfc.mapper.CartMapper;
-import com.etotem.cfc.mapper.ProductBundleItemMapper;
 import com.etotem.cfc.mapper.ProductMapper;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.BundleHit;
-import com.etotem.cfc.pricing.LineItem;
 import com.etotem.cfc.service.PpointService;
 import com.etotem.cfc.service.ProductGiftRuleService;
 import com.etotem.cfc.service.ProductSkuService;
@@ -41,9 +36,6 @@ public class CartService {
     @Resource
     private ProductMapper productMapper;
 
-    @Resource
-    private ProductBundleItemMapper bundleItemMapper;
-
     @Resource
     private ProductSkuService productSkuService;
 
@@ -53,9 +45,6 @@ public class CartService {
     @Resource
     private SysConfigService sysConfigService;
 
-    @Resource
-    private BundlePricingService bundlePricingService;
-
     @Resource
     private MemberPriceResolver memberPriceResolver;
 
@@ -126,24 +115,17 @@ public class CartService {
             CartItemDTO dto = buildCartItemDTO(item);
             if (dto != null) {
                 Product product = productMap.get(item.getProductId());
-                if (product != null && "bundle".equals(product.getProductType())) {
-                    dto.setBundleInfo(buildBundleInfo(product, dto));
-                }
                 result.add(dto);
             }
         }
 
-        // 自动套餐模式:检测购物车子商品组合是否凑成某套餐,命中则挂 bundleMatch 提示
-        attachBundleMatches(cartList, result);
 
         // 订单套餐自动定价优惠(替换原"送会员一口价"结算价逻辑;giftPromo 置空保持旧前端兼容)
         CartItemDTO.GiftPromo giftPromo = computeGiftPromo(cartList, productMap);
-        CartItemDTO.BundlePromo bundlePromo = computeBundlePromo(cartList, productMap, userId);
 
         CartListResultDTO cartResult = new CartListResultDTO();
         cartResult.setItems(result);
         cartResult.setGiftPromo(giftPromo);
-        cartResult.setBundlePromo(bundlePromo);
         return cartResult;
     }
 
@@ -152,144 +134,6 @@ public class CartService {
      * (每个子商品数量 ≥ 组成数量),则在该子商品项上挂 bundleMatch(含套餐价/节省金额/组成明细)。
      * 命中多个套餐时,参与项上只挂第一个命中结果,供前端提示「可享套餐价」。
      */
-    private void attachBundleMatches(List<Cart> cartList, List<CartItemDTO> result) {
-        if (cartList == null || cartList.isEmpty() || result == null || result.isEmpty()) {
-            return;
-        }
-
-        // 1. 聚合购物车数量(按 productId / productId+skuId 两级)
-        Map<Long, Integer> productQtyMap = new HashMap<>();
-        Map<String, Integer> productSkuQtyMap = new HashMap<>();
-        for (Cart c : cartList) {
-            productQtyMap.put(c.getProductId(), productQtyMap.getOrDefault(c.getProductId(), 0) + c.getQuantity());
-            if (c.getSkuId() != null) {
-                String key = c.getProductId() + "_" + c.getSkuId();
-                productSkuQtyMap.put(key, productSkuQtyMap.getOrDefault(key, 0) + c.getQuantity());
-            }
-        }
-
-        // 2. 查询所有 bundle 商品
-        List<Product> bundles = productMapper.selectList(
-            new LambdaQueryWrapper<Product>().eq(Product::getProductType, "bundle"));
-        if (bundles == null || bundles.isEmpty()) {
-            return;
-        }
-
-        // 3. 对每个 bundle 检测购物车覆盖情况
-        for (Product bundle : bundles) {
-            if (bundle.getId() == null) {
-                continue;
-            }
-            List<ProductBundleItem> items = bundleItemMapper.selectList(
-                new LambdaQueryWrapper<ProductBundleItem>()
-                    .eq(ProductBundleItem::getBundleProductId, bundle.getId()));
-            if (items == null || items.isEmpty()) {
-                continue;
-            }
-
-            // 计算最大可匹配组数 + 1 组子商品单品总价
-            int maxGroups = Integer.MAX_VALUE;
-            int singleTotalPrice = 0;
-            boolean matchable = true;
-            for (ProductBundleItem bi : items) {
-                int needQty = bi.getQuantity() != null ? bi.getQuantity() : 1;
-                Integer haveQty;
-                if (bi.getChildSkuId() != null) {
-                    haveQty = productSkuQtyMap.get(bi.getChildProductId() + "_" + bi.getChildSkuId());
-                } else {
-                    haveQty = productQtyMap.get(bi.getChildProductId());
-                }
-                if (haveQty == null || haveQty < needQty) {
-                    matchable = false;
-                    break;
-                }
-                maxGroups = Math.min(maxGroups, haveQty / needQty);
-                Product child = productMapper.selectById(bi.getChildProductId());
-                if (child != null && child.getPrice() != null) {
-                    singleTotalPrice += child.getPrice() * needQty;
-                }
-            }
-            if (!matchable || maxGroups <= 0) {
-                continue;
-            }
-
-            Long bundlePrice = bundle.getPrice() != null ? bundle.getPrice().longValue() : 0L;
-            int saving = bundlePrice < singleTotalPrice ? singleTotalPrice - bundlePrice.intValue() : 0;
-            if (saving <= 0) {
-                continue; // 无优惠不提示
-            }
-
-            // 构造 BundleMatch
-            CartItemDTO.BundleMatch match = new CartItemDTO.BundleMatch();
-            match.setBundleProductId(bundle.getId());
-            match.setBundleProductName(bundle.getName());
-            match.setMatchedGroups(maxGroups);
-            match.setBundlePrice(bundlePrice);
-            match.setSingleTotalPrice(singleTotalPrice);
-            match.setSaving(saving);
-            List<CartItemDTO.BundleChild> children = new ArrayList<>();
-            for (ProductBundleItem bi : items) {
-                CartItemDTO.BundleChild bc = new CartItemDTO.BundleChild();
-                bc.setChildProductId(bi.getChildProductId());
-                bc.setQuantity(bi.getQuantity() != null ? bi.getQuantity() : 1);
-                children.add(bc);
-            }
-            match.setChildren(children);
-
-            // 4. 挂到参与组成的子商品项上(每项只挂第一个命中,供前端提示横幅)
-            for (ProductBundleItem bi : items) {
-                for (CartItemDTO dto : result) {
-                    if (dto.getProductId() != null && dto.getProductId().equals(bi.getChildProductId())
-                            && dto.getBundleMatch() == null) {
-                        dto.setBundleMatch(match);
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    private CartItemDTO.BundleInfo buildBundleInfo(Product bundleProduct, CartItemDTO dto) {
-        List<ProductBundleItem> bundleItems = bundleItemMapper.selectList(
-            new LambdaQueryWrapper<ProductBundleItem>()
-                .eq(ProductBundleItem::getBundleProductId, bundleProduct.getId())
-        );
-        if (bundleItems == null || bundleItems.isEmpty()) {
-            return null;
-        }
-
-        CartItemDTO.BundleInfo info = new CartItemDTO.BundleInfo();
-        info.setBundleProductId(bundleProduct.getId());
-
-        List<Long> memberIds = new ArrayList<>();
-        List<String> childNames = new ArrayList<>();
-        int childQtyPerBundle = 0;
-        for (ProductBundleItem bi : bundleItems) {
-            memberIds.add(bi.getChildProductId());
-            if (bi.getQuantity() != null) {
-                childQtyPerBundle += bi.getQuantity();
-            }
-            Product child = productMapper.selectById(bi.getChildProductId());
-            childNames.add(child != null ? child.getName() : "");
-        }
-        int cartQty = dto.getQuantity() != null ? dto.getQuantity() : 1;
-        info.setChildProductIds(memberIds);
-        info.setChildProductNames(childNames);
-        info.setChildQuantityTotal(childQtyPerBundle * cartQty);
-
-        Integer unitPrice = dto.getUnitPrice() != null ? dto.getUnitPrice() : 0;
-        Integer quantity = dto.getQuantity() != null ? dto.getQuantity() : 1;
-        Integer singleTotal = unitPrice * quantity;
-        Long bundlePrice = bundleProduct.getPrice() != null ? bundleProduct.getPrice().longValue() : 0L;
-        info.setBundlePrice(bundlePrice);
-        info.setSingleTotalPrice(singleTotal);
-
-        int saving = bundlePrice < singleTotal ? singleTotal - bundlePrice.intValue() : 0;
-        info.setSaving(saving);
-        return info;
-    }
-
-    @Transactional
     public void updateQuantity(Long userId, Long productId, Integer quantity) {
         Cart cart = cartMapper.selectOne(
             new LambdaQueryWrapper<Cart>()
@@ -425,76 +269,6 @@ public class CartService {
         return buildEmptyGiftPromo();
     }
 
-    /**
-     * 订单套餐自动定价优惠:扫描购物车商品,按下单结算口径(会员/SKU/linked 价)聚合,
-     * 匹配 product_bundle_items 中 bundle_sku_id 非空的定价套餐,贪心选省最多,返回 bundlePromo。
-     * 展示口径与下单一致(MemberPriceResolver),保证页面提示=实际扣款。
-     */
-    private CartItemDTO.BundlePromo computeBundlePromo(List<Cart> cartList, Map<Long, Product> productMap, Long userId) {
-        if (cartList == null || cartList.isEmpty()) {
-            return emptyBundlePromo();
-        }
-        List<LineItem> lines = new ArrayList<>();
-        for (Cart cart : cartList) {
-            if (cart == null || cart.getProductId() == null || cart.getQuantity() == null || cart.getQuantity() <= 0) {
-                continue;
-            }
-            Product product = productMap.get(cart.getProductId());
-            if (product == null) {
-                continue;
-            }
-            ProductSku sku = null;
-            if (cart.getSkuId() != null) {
-                sku = productSkuService.getById(cart.getSkuId());
-            }
-            // 解析后身份 + 下单口径单价(linked 转发商品、SKU 价、否则会员价)
-            Long resolvedProductId = product.getId();
-            int unitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, userId);
-            if (sku != null && sku.getLinkedProductId() != null) {
-                Product linked = productMapper.selectById(sku.getLinkedProductId());
-                if (linked != null) {
-                    resolvedProductId = linked.getId();
-                    unitPrice = linked.getPrice() != null ? linked.getPrice() : 0;
-                }
-            } else if (sku != null && sku.getPrice() != null) {
-                unitPrice = sku.getPrice();
-            }
-            lines.add(new LineItem(resolvedProductId, cart.getSkuId(), cart.getQuantity(), unitPrice));
-        }
-        if (lines.isEmpty()) {
-            return emptyBundlePromo();
-        }
-        BundleDiscountResult result = bundlePricingService.compute(lines);
-        return toBundlePromo(result);
-    }
-
-    private CartItemDTO.BundlePromo toBundlePromo(BundleDiscountResult r) {
-        CartItemDTO.BundlePromo promo = new CartItemDTO.BundlePromo();
-        promo.setActive(r.isActive());
-        promo.setOriginalTotal(r.getOriginalTotal() != null ? r.getOriginalTotal() : 0);
-        promo.setPayPrice(r.getPayPrice() != null ? r.getPayPrice() : 0);
-        promo.setDiscount(r.getDiscount() != null ? r.getDiscount() : 0);
-        List<CartItemDTO.BundleItemHit> hits = new ArrayList<>();
-        if (r.getBundleList() != null) {
-            for (BundleHit hit : r.getBundleList()) {
-                CartItemDTO.BundleItemHit h = new CartItemDTO.BundleItemHit();
-                h.setBundleSkuId(hit.getBundleSkuId());
-                h.setBundleName(hit.getBundleName());
-                h.setTimes(hit.getTimes());
-                h.setOriginalTotal(hit.getOriginalTotal());
-                h.setBundlePrice(hit.getBundlePrice());
-                h.setSaving(hit.getSaving());
-                hits.add(h);
-            }
-        }
-        promo.setBundleList(hits);
-        return promo;
-    }
-
-    private CartItemDTO.BundlePromo emptyBundlePromo() {
-        return toBundlePromo(BundleDiscountResult.inactive());
-    }
-
     private CartItemDTO.GiftPromo buildEmptyGiftPromo() {
         CartItemDTO.GiftPromo promo = new CartItemDTO.GiftPromo();
         promo.setGiftOriginalTotal(0);

+ 0 - 76
cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java

@@ -26,8 +26,6 @@ public class InventoryService {
     @Resource
     private InventoryCountingRecordMapper countingRecordMapper;
     @Resource
-    private ProductBundleItemMapper bundleItemMapper;
-    @Resource
     private ProductMapper productMapper;
     @Resource
     private ProductSkuMapper productSkuMapper;
@@ -434,83 +432,9 @@ public class InventoryService {
                         .last("LIMIT 10"));
         data.put("recentTransactions", recentTx);
 
-        if ("bundle".equals(product.getProductType())) {
-            List<ProductBundleItem> bundleItems = bundleItemMapper.selectList(
-                    new LambdaQueryWrapper<ProductBundleItem>()
-                            .eq(ProductBundleItem::getBundleProductId, productId));
-            List<Map<String, Object>> childProducts = new ArrayList<>();
-            for (ProductBundleItem bi : bundleItems) {
-                Product child = productMapper.selectById(bi.getChildProductId());
-                if (child != null) {
-                    Map<String, Object> cp = new HashMap<>();
-                    cp.put("productId", child.getId());
-                    cp.put("productName", child.getName());
-                    cp.put("stock", child.getStock());
-                    cp.put("quantity", bi.getQuantity());
-                    cp.put("skuId", bi.getChildSkuId());
-                    childProducts.add(cp);
-                }
-            }
-            data.put("bundleItems", childProducts);
-        }
-
         return Result.success(data);
     }
 
-    // ========== 套餐商品组成 ==========
-
-    @Transactional
-    public Result<String> saveBundleItems(Long bundleProductId, List<Map<String, Object>> items) {
-        bundleItemMapper.delete(new LambdaQueryWrapper<ProductBundleItem>()
-                .eq(ProductBundleItem::getBundleProductId, bundleProductId));
-
-        for (Map<String, Object> item : items) {
-            ProductBundleItem bi = new ProductBundleItem();
-            bi.setBundleProductId(bundleProductId);
-            bi.setChildProductId(ParamUtils.getLong(item.get("childProductId")));
-            if (item.get("childSkuId") != null) {
-                bi.setChildSkuId(ParamUtils.getLong(item.get("childSkuId")));
-            }
-            bi.setQuantity(item.get("quantity") != null ? Integer.valueOf(item.get("quantity").toString()) : 1);
-            bi.setCreatedAt(new Date());
-            bundleItemMapper.insert(bi);
-        }
-        return Result.success("套餐商品配置已保存");
-    }
-
-    public Result<List<ProductBundleItem>> listBundleItems(Long bundleProductId) {
-        List<ProductBundleItem> items = bundleItemMapper.selectList(
-                new LambdaQueryWrapper<ProductBundleItem>()
-                        .eq(ProductBundleItem::getBundleProductId, bundleProductId));
-        return Result.success(items);
-    }
-
-    // ========== 套餐库存检查 ==========
-
-    public Result<String> checkBundleStock(Long bundleProductId, Integer quantity) {
-        List<ProductBundleItem> items = bundleItemMapper.selectList(
-                new LambdaQueryWrapper<ProductBundleItem>()
-                        .eq(ProductBundleItem::getBundleProductId, bundleProductId));
-        if (items.isEmpty()) return Result.error("套餐未配置子商品");
-
-        for (ProductBundleItem bi : items) {
-            int need = bi.getQuantity() * quantity;
-            if (bi.getChildSkuId() != null) {
-                ProductSku sku = productSkuMapper.selectById(bi.getChildSkuId());
-                if (sku == null || sku.getStock() == null || sku.getStock() < need) {
-                    Product p = productMapper.selectById(bi.getChildProductId());
-                    return Result.error("子商品库存不足: " + (p != null ? p.getName() : "未知"));
-                }
-            } else {
-                Product p = productMapper.selectById(bi.getChildProductId());
-                if (p == null || p.getStock() == null || p.getStock() < need) {
-                    return Result.error("子商品库存不足: " + (p != null ? p.getName() : "未知"));
-                }
-            }
-        }
-        return Result.success("库存充足");
-    }
-
     // ========== 手动出库 ==========
 
     @Transactional

+ 2 - 64
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -12,11 +12,9 @@ import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.Consignee;
 import com.etotem.cfc.entity.UserAddress;
 import com.etotem.cfc.mapper.UserAddressMapper;
-import com.etotem.cfc.pricing.LineItem;
 import com.etotem.cfc.entity.Coupon;
 import com.etotem.cfc.entity.FamilyCoupon;
 import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductGiftRule;
 import com.etotem.cfc.entity.ProductOnlineSlot;
 import com.etotem.cfc.entity.OrderItem;
@@ -42,9 +40,7 @@ import com.etotem.cfc.mapper.FamilyCouponMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 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.mapper.ProductBundleItemMapper;
 import com.etotem.cfc.service.ProductSkuService;
 import com.etotem.cfc.service.AssessmentProductService;
 import com.etotem.cfc.service.AssessmentQuotaService;
@@ -98,15 +94,9 @@ public class ProductOrderService {
     private ProductSkuService productSkuService;
     @Resource
     private MemberPriceResolver memberPriceResolver;
-    @Resource
-    private BundlePricingService bundlePricingService;
-
     @Resource
     private InventoryService inventoryService;
 
-    @Resource
-    private ProductBundleItemMapper bundleItemMapper;
-
     @Resource
     private CfCommissionService cfCommissionService;
 
@@ -245,13 +235,6 @@ public class ProductOrderService {
                 && product.getStock() < dto.getQuantity()) {
             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
                 && dto.getQuantity() < product.getMinQuantity()) {
@@ -275,12 +258,7 @@ public class ProductOrderService {
         order.setPointsUsed(dto.getPointsUsed() != null ? dto.getPointsUsed() : 0);
         int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
         order.setPointsCost(pointsCost);
-        // 套餐自动定价优惠(服务端权威):按下单最终单价(含履约覆盖价)计算;前端 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;
+        int discountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0);
         // 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
         Long appliedCouponId = null;
         if (dto.getCouponId() != null) {
@@ -412,12 +390,6 @@ public class ProductOrderService {
                 && product.getStock() < quantity) {
             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
                 && quantity < product.getMinQuantity()) {
             return Result.error("该商品最少需购买" + product.getMinQuantity() + "件");
@@ -755,8 +727,6 @@ public class ProductOrderService {
         java.util.List<OrderItem> orderItemRecords = new java.util.ArrayList<>();
         Product firstProduct = null;
         int firstUnitPrice = 0;
-        // 收集下单口径行项(解析后身份 + 结算单价),用于服务端权威的套餐自动定价
-        java.util.List<LineItem> bundleLines = new java.util.ArrayList<>();
 
         for (OrderItemVO item : dto.getItems()) {
             ProductSku itemSku = item.getSkuId() != null ? productSkuService.getById(item.getSkuId()) : null;
@@ -773,13 +743,6 @@ public class ProductOrderService {
                     && product.getStock() < 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());
-                }
-            }
             int unitPrice = memberPriceResolver.resolveEligibleMemberPrice(product, buyerId);
             if (itemSku != null) {
                 if (itemSku.getLinkedProductId() != null) {
@@ -824,7 +787,6 @@ public class ProductOrderService {
             itemRecord.setQuantity(item.getQuantity());
             itemRecord.setSubtotal(java.math.BigDecimal.valueOf((long) unitPrice * item.getQuantity()));
             orderItemRecords.add(itemRecord);
-            bundleLines.add(new LineItem(product.getId(), item.getSkuId(), item.getQuantity(), unitPrice));
         }
 
         User buyer = userMapper.selectById(buyerId);
@@ -844,8 +806,7 @@ public class ProductOrderService {
         int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
         order.setPointsCost(pointsCost);
         // 套餐自动定价优惠(服务端权威):前端 discountAmount 仅含规则优惠,套餐部分由服务端重算加入
-        int bundleDiscount = bundlePricingService.compute(bundleLines).getDiscount();
-        int multiDiscountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0) + bundleDiscount;
+        int multiDiscountAmount = (dto.getDiscountAmount() != null ? dto.getDiscountAmount() : 0);
         // 优惠券抵扣:校验并计入优惠金额(orderType=PRODUCT)
         Long appliedCouponId = null;
         if (dto.getCouponId() != null) {
@@ -1062,29 +1023,6 @@ public class ProductOrderService {
             // 记录出库流水
             inventoryService.recordOutbound(order.getProductId(), null, order.getQuantity(),
                     "order", order.getId(), null, "订单支付:" + orderNo);
-        } 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);
-                }
-            }
         }
         order.setStatus("paid");
         order.setTransactionId(transactionId);

+ 1 - 26
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -14,13 +14,11 @@ import com.etotem.cfc.entity.AssessmentProduct;
 import com.etotem.cfc.entity.ContentDimensionThreshold;
 import com.etotem.cfc.entity.GrowthArchiveProduct;
 import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.entity.ProductBundleItem;
 import com.etotem.cfc.entity.ProductGiftRule;
 import com.etotem.cfc.entity.ProductComboItem;
 import com.etotem.cfc.entity.ProductSku;
 import com.etotem.cfc.entity.SupplySystemMember;
 import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.ProductBundleItemMapper;
 import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.mapper.ProductSkuMapper;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
@@ -72,9 +70,6 @@ public class ProductService {
     @Resource
     private ProductDeliveryConfigService productDeliveryConfigService;
 
-    @Resource
-    private ProductBundleItemMapper bundleItemMapper;
-
     @Resource
     private ProductSkuMapper productSkuMapper;
     @Resource
@@ -298,28 +293,8 @@ wrapper.in(Product::getId, productIds)
             return Result.error("商品未上架");
         }
         ProductDTO dto = ProductDTO.from(product);
-        // 套餐商品:填充套餐组成(子商品名称/封面/SKU规格/数量)
+        // 套餐商品:套餐内容(新模型:内容=子商品SKU×数量,挂套餐SKU级;下单只扣套餐SKU库存)
         if ("bundle".equals(product.getProductType())) {
-            List<ProductBundleItem> items = bundleItemMapper.selectList(
-                    new LambdaQueryWrapper<ProductBundleItem>()
-                            .eq(ProductBundleItem::getBundleProductId, id));
-            List<ProductDTO.BundleItemVO> bundleItems = new ArrayList<>();
-            for (ProductBundleItem bi : items) {
-                Product child = productMapper.selectById(bi.getChildProductId());
-                if (child == null) continue;   // 子商品已删除则跳过
-                ProductDTO.BundleItemVO vo = new ProductDTO.BundleItemVO();
-                vo.setChildProductId(child.getId());
-                vo.setChildProductName(child.getName());
-                vo.setCoverImage(child.getCoverImage());
-                vo.setChildSkuId(bi.getChildSkuId());
-                vo.setQuantity(bi.getQuantity());
-                if (bi.getChildSkuId() != null) {
-                    ProductSku sku = productSkuMapper.selectById(bi.getChildSkuId());
-                    if (sku != null) vo.setSpecs(sku.getSpecs());
-                }
-                bundleItems.add(vo);
-            }
-            dto.setBundleItems(bundleItems);
             // 套餐内容(新模型:内容=子商品SKU×数量,挂套餐SKU级;下单只扣套餐SKU库存)
             List<ProductDTO.ComboItemVO> comboItems = new ArrayList<>();
             List<ProductSku> comboSkus = productSkuMapper.selectList(

+ 0 - 25
cfc-backend/src/main/resources/schema.sql

@@ -5631,31 +5631,6 @@ CREATE TABLE IF NOT EXISTS system_prompt_versions (
     INDEX idx_prompt_key (prompt_key),
     INDEX idx_version (prompt_key, version)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统提示词版本历史';
--- 套餐商品组成明细表(套餐 = 商品 productType=bundle,子商品在 product_bundle_items 中配置)
-CREATE TABLE IF NOT EXISTS product_bundle_items (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    bundle_product_id BIGINT DEFAULT NULL COMMENT '套餐商品ID(定价套餐行置NULL以避免误触发出库联动;出库联动行=$套餐商品ID)',
-    bundle_sku_id BIGINT DEFAULT NULL COMMENT '套餐SKU锚点:非空=定价套餐行(订单套餐自动定价,套餐价取该SKU.price);空=仅出库联动',
-    child_product_id BIGINT NOT NULL COMMENT '子商品ID',
-    child_sku_id BIGINT DEFAULT NULL COMMENT '子SKU ID(为空时直接扣 product.stock)',
-    quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量',
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_bundle (bundle_product_id),
-    INDEX idx_child (child_product_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成明细';
-
--- 商品SKU组合明细表(组合SKU = 多个商品/子SKU + 数量,整体手填售价售卖)
-CREATE TABLE IF NOT EXISTS product_sku_combo_items (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    sku_id BIGINT NOT NULL COMMENT '组合SKU ID(product_skus.id)',
-    child_product_id BIGINT NOT NULL COMMENT '组成商品ID',
-    child_sku_id BIGINT DEFAULT NULL COMMENT '组成商品SKU ID(组成商品有SKU时必填)',
-    quantity INT NOT NULL DEFAULT 1 COMMENT '该组成商品数量',
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_sku (sku_id),
-    INDEX idx_child_product (child_product_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细';
-
 -- 套餐内容明细表(套餐=独立商品 product_type='bundle',内容=子商品SKU×数量,挂套餐SKU级)
 CREATE TABLE IF NOT EXISTS product_combo_items (
     id BIGINT NOT NULL AUTO_INCREMENT,

+ 0 - 168
cfc-backend/src/test/java/com/etotem/cfc/unit/BundlePricingCalculatorTest.java

@@ -1,168 +0,0 @@
-package com.etotem.cfc.unit;
-
-import com.etotem.cfc.pricing.BundleChild;
-import com.etotem.cfc.pricing.BundleDef;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.BundleHit;
-import com.etotem.cfc.pricing.BundlePricingCalculator;
-import com.etotem.cfc.pricing.LineItem;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * 订单套餐自动定价核心算法单测(TDD)。
- * 规则:凑整套餐+单品兜底;多套餐贪心选节省额最大;节省额<=0不触发;
- * child_sku_id 为空按商品聚合(跨 SKU);不同基础款套餐互不干扰。
- */
-class BundlePricingCalculatorTest {
-
-    private final BundlePricingCalculator calc = new BundlePricingCalculator();
-
-    private LineItem line(Long productId, Long skuId, int qty, int unitPrice) {
-        return new LineItem(productId, skuId, qty, unitPrice);
-    }
-
-    private BundleChild child(Long productId, Long childSkuId, int qty) {
-        return new BundleChild(productId, childSkuId, qty);
-    }
-
-    private BundleDef def(Long bundleSkuId, String name, int bundlePrice, BundleChild... children) {
-        return new BundleDef(bundleSkuId, name, bundlePrice, Arrays.asList(children));
-    }
-
-    @Test
-    void singleBundleHit_twoBasicItems() {
-        // 2×69900 配套餐(2个基础款=128000) → 命中,省 11800
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 2, 69900));
-        List<BundleDef> defs = Collections.singletonList(def(3L, "套餐一", 128000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertTrue(r.isActive());
-        assertEquals(139800, r.getOriginalTotal());
-        assertEquals(128000, r.getPayPrice());
-        assertEquals(11800, r.getDiscount());
-        assertEquals(1, r.getBundleList().size());
-        BundleHit hit = r.getBundleList().get(0);
-        assertEquals(3L, hit.getBundleSkuId());
-        assertEquals(1, hit.getTimes());
-        assertEquals(11800, hit.getSaving());
-    }
-
-    @Test
-    void wholeBundlePlusSingleFallback_threeItems() {
-        // 3×69900 → 1套套餐(128000) + 1个单品兜底(69900),只算命中部分
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 3, 69900));
-        List<BundleDef> defs = Collections.singletonList(def(3L, "套餐一", 128000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertTrue(r.isActive());
-        assertEquals(1, r.getBundleList().get(0).getTimes());
-        assertEquals(139800, r.getOriginalTotal());
-        assertEquals(128000, r.getPayPrice());
-        assertEquals(11800, r.getDiscount());
-    }
-
-    @Test
-    void multiBundle_pickMaxSaving() {
-        // 2×69900,两个套餐都凑得齐:defA省11800、defB省29800 → 选defB
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 2, 69900));
-        List<BundleDef> defs = Arrays.asList(
-                def(3L, "套餐一", 128000, child(10L, null, 2)),
-                def(9L, "套餐二", 110000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertTrue(r.isActive());
-        assertEquals(1, r.getBundleList().size());
-        assertEquals(9L, r.getBundleList().get(0).getBundleSkuId());
-        assertEquals(29800, r.getDiscount());
-        assertEquals(110000, r.getPayPrice());
-    }
-
-    @Test
-    void savingNotPositive_notTriggered_memberPriceBelowBundle() {
-        // 会员价 55900×2=111800 < 128000 → 按套餐反而贵,不触发
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 2, 55900));
-        List<BundleDef> defs = Collections.singletonList(def(3L, "套餐一", 128000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertFalse(r.isActive());
-        assertEquals(0, r.getDiscount());
-    }
-
-    @Test
-    void childSkuNull_aggregateAcrossSkus() {
-        // 两行不同 sku(69900 + 59900)配 product10 qty2(child_sku_id 空→按商品聚合)
-        List<LineItem> lines = Arrays.asList(
-                line(10L, 2L, 1, 69900),
-                line(10L, 4L, 1, 59900));
-        List<BundleDef> defs = Collections.singletonList(def(3L, "套餐一", 120000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertTrue(r.isActive());
-        assertEquals(129800, r.getOriginalTotal());
-        assertEquals(9800, r.getDiscount());
-    }
-
-    @Test
-    void emptyLinesOrDefs_inactive() {
-        assertFalse(calc.compute(Collections.emptyList(), Collections.singletonList(
-                def(3L, "套餐一", 128000, child(10L, null, 2)))).isActive());
-        assertFalse(calc.compute(Collections.singletonList(line(10L, null, 2, 69900)), Collections.emptyList()).isActive());
-    }
-
-    @Test
-    void bundlePriceAboveOriginal_notTriggered() {
-        // 套餐价 150000 > 原价 139800 → 不触发
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 2, 69900));
-        List<BundleDef> defs = Collections.singletonList(def(3L, "套餐一", 150000, child(10L, null, 2)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertFalse(r.isActive());
-        assertEquals(0, r.getDiscount());
-    }
-
-    @Test
-    void twoIndependentBundles_bothHit() {
-        // 不同基础款的两个套餐同单各自命中:product10×2(69900)→套餐一128000省11800;
-        // product20×1(99900)→套餐二89900省10000
-        List<LineItem> lines = Arrays.asList(
-                line(10L, null, 2, 69900),
-                line(20L, null, 1, 99900));
-        List<BundleDef> defs = Arrays.asList(
-                def(3L, "套餐一", 128000, child(10L, null, 2)),
-                def(9L, "套餐二", 89900, child(20L, null, 1)));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertTrue(r.isActive());
-        assertEquals(2, r.getBundleList().size());
-        assertEquals(239700, r.getOriginalTotal());
-        assertEquals(217900, r.getPayPrice());
-        assertEquals(21800, r.getDiscount());
-    }
-
-    @Test
-    void defWithoutChildren_skipped() {
-        List<LineItem> lines = Collections.singletonList(line(10L, null, 2, 69900));
-        List<BundleDef> defs = new ArrayList<>();
-        defs.add(new BundleDef(3L, "无子商品", 128000, Collections.emptyList()));
-
-        BundleDiscountResult r = calc.compute(lines, defs);
-
-        assertFalse(r.isActive());
-    }
-}

+ 0 - 126
cfc-backend/src/test/java/com/etotem/cfc/unit/BundlePricingServiceTest.java

@@ -1,126 +0,0 @@
-package com.etotem.cfc.unit;
-
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
-import com.etotem.cfc.entity.ProductBundleItem;
-import com.etotem.cfc.entity.ProductSku;
-import com.etotem.cfc.mapper.ProductBundleItemMapper;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.LineItem;
-import com.etotem.cfc.service.BundlePricingService;
-import com.etotem.cfc.service.ProductSkuService;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.when;
-
-/**
- * BundlePricingService 映射单测:装载 bundle_sku_id 非空行、组装 BundleDef、委托计算。
- */
-@ExtendWith(MockitoExtension.class)
-class BundlePricingServiceTest {
-
-    @Mock
-    private ProductBundleItemMapper productBundleItemMapper;
-    @Mock
-    private ProductSkuService productSkuService;
-    @InjectMocks
-    private BundlePricingService service;
-
-    private ProductBundleItem row(Long bundleSkuId, Long childProductId, Long childSkuId, int qty) {
-        ProductBundleItem r = new ProductBundleItem();
-        r.setBundleSkuId(bundleSkuId);
-        r.setChildProductId(childProductId);
-        r.setChildSkuId(childSkuId);
-        r.setQuantity(qty);
-        return r;
-    }
-
-    private ProductSku sku(Long id, String label, int price) {
-        ProductSku s = new ProductSku();
-        s.setId(id);
-        s.setLabel(label);
-        s.setPrice(price);
-        return s;
-    }
-
-    @Test
-    void noPricingRows_inactive() {
-        when(productBundleItemMapper.selectList(any())).thenReturn(Collections.emptyList());
-        BundleDiscountResult r = service.compute(Collections.singletonList(
-                new LineItem(10L, null, 2, 69900)));
-        assertFalse(r.isActive());
-    }
-
-    @Test
-    void assembleDefsAndDelegate_hit() {
-        when(productBundleItemMapper.selectList(any(Wrapper.class))).thenReturn(
-                Collections.singletonList(row(3L, 10L, null, 2)));
-        when(productSkuService.getById(3L)).thenReturn(sku(3L, "套餐一", 128000));
-
-        BundleDiscountResult r = service.compute(Collections.singletonList(
-                new LineItem(10L, null, 2, 69900)));
-
-        assertTrue(r.isActive());
-        assertEquals(11800, r.getDiscount());
-        assertEquals("套餐一", r.getBundleList().get(0).getBundleName());
-        assertEquals(1, r.getBundleList().get(0).getTimes());
-    }
-
-    @Test
-    void anchorSkuMissing_skipDef() {
-        when(productBundleItemMapper.selectList(any(Wrapper.class))).thenReturn(
-                Collections.singletonList(row(3L, 10L, null, 2)));
-        when(productSkuService.getById(3L)).thenReturn(null);
-
-        BundleDiscountResult r = service.compute(Collections.singletonList(
-                new LineItem(10L, null, 2, 69900)));
-
-        assertFalse(r.isActive());
-    }
-
-    @Test
-    void multipleChildrenSameDef_merged() {
-        // 同一套餐锚点两行子商品:childA(10,qty2) + childB(20,qty1)
-        when(productBundleItemMapper.selectList(any(Wrapper.class))).thenReturn(Arrays.asList(
-                row(3L, 10L, null, 2),
-                row(3L, 20L, null, 1)));
-        when(productSkuService.getById(3L)).thenReturn(sku(3L, "套餐一", 128000));
-
-        BundleDiscountResult r = service.compute(Arrays.asList(
-                new LineItem(10L, null, 2, 69900),
-                new LineItem(20L, null, 1, 50000)));
-
-        assertTrue(r.isActive());
-        assertEquals(1, r.getBundleList().size());
-        // 原价 = 2×69900 + 1×50000 = 189800;省 = 189800 - 128000 = 61800
-        assertEquals(61800, r.getDiscount());
-    }
-
-    @Test
-    void labelFallbackToSpecs() {
-        when(productBundleItemMapper.selectList(any(Wrapper.class))).thenReturn(
-                Collections.singletonList(row(3L, 10L, null, 2)));
-        ProductSku s = new ProductSku();
-        s.setId(3L);
-        s.setLabel(null);
-        s.setSpecs("成长烦恼、学业提升");
-        s.setPrice(128000);
-        when(productSkuService.getById(3L)).thenReturn(s);
-
-        BundleDiscountResult r = service.compute(Collections.singletonList(
-                new LineItem(10L, null, 2, 69900)));
-
-        assertEquals("成长烦恼、学业提升", r.getBundleList().get(0).getBundleName());
-    }
-}

+ 0 - 137
cfc-backend/src/test/java/com/etotem/cfc/unit/CartServiceTest.java

@@ -1,137 +0,0 @@
-package com.etotem.cfc.unit;
-
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
-import com.etotem.cfc.dto.CartItemDTO;
-import com.etotem.cfc.dto.CartListResultDTO;
-import com.etotem.cfc.entity.Cart;
-import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.mapper.CartMapper;
-import com.etotem.cfc.mapper.ProductBundleItemMapper;
-import com.etotem.cfc.mapper.ProductMapper;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.BundleHit;
-import com.etotem.cfc.pricing.LineItem;
-import com.etotem.cfc.service.BundlePricingService;
-import com.etotem.cfc.service.CartService;
-import com.etotem.cfc.service.MemberPriceResolver;
-import com.etotem.cfc.service.ProductGiftRuleService;
-import com.etotem.cfc.service.ProductSkuService;
-import com.etotem.cfc.service.SysConfigService;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyList;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.when;
-
-/**
- * CartService.getCartList 单测:bundlePromo 绑定(委托 BundlePricingService)、giftPromo 置空(一口价废弃)。
- */
-@ExtendWith(MockitoExtension.class)
-class CartServiceTest {
-
-    @Mock private CartMapper cartMapper;
-    @Mock private ProductMapper productMapper;
-    @Mock private ProductBundleItemMapper bundleItemMapper;
-    @Mock private ProductSkuService productSkuService;
-    @Mock private ProductGiftRuleService productGiftRuleService;
-    @Mock private SysConfigService sysConfigService;
-    @Mock private BundlePricingService bundlePricingService;
-    @Mock private MemberPriceResolver memberPriceResolver;
-    @InjectMocks private CartService cartService;
-
-    private Cart cart(Long productId, Long skuId, int qty) {
-        Cart c = new Cart();
-        c.setId(1L);
-        c.setUserId(1L);
-        c.setProductId(productId);
-        c.setSkuId(skuId);
-        c.setQuantity(qty);
-        return c;
-    }
-
-    private Product product(Long id, String name, int price) {
-        Product p = new Product();
-        p.setId(id);
-        p.setName(name);
-        p.setPrice(price);
-        p.setProductType("assessment"); // 非 bundle,避免 buildBundleInfo
-        p.setStatus("on_shelf");
-        return p;
-    }
-
-    @Test
-    void getCartList_bindsBundlePromo_andGiftPromoEmpty() {
-        when(cartMapper.selectList(any(Wrapper.class)))
-                .thenReturn(Collections.singletonList(cart(10L, null, 2)));
-        when(productMapper.selectBatchIds(anyList()))
-                .thenReturn(Collections.singletonList(product(10L, "DAN测评", 69900)));
-        // attachBundleMatches:查询 bundle 商品 → 空,直接返回
-        when(productMapper.selectList(any(Wrapper.class))).thenReturn(Collections.emptyList());
-        when(memberPriceResolver.resolveEligibleMemberPrice(any(Product.class), any())).thenReturn(69900);
-        // 模拟定价命中:1 套套餐,省 11800
-        BundleDiscountResult r = new BundleDiscountResult();
-        r.setActive(true);
-        r.setOriginalTotal(139800);
-        r.setPayPrice(128000);
-        r.setDiscount(11800);
-        BundleHit hit = new BundleHit(3L, "套餐一", 1, 139800, 128000, 11800);
-        r.setBundleList(new ArrayList<>(Collections.singletonList(hit)));
-        when(bundlePricingService.compute(anyList())).thenReturn(r);
-
-        CartListResultDTO result = cartService.getCartList(1L);
-
-        assertNotNull(result.getBundlePromo());
-        assertTrue(result.getBundlePromo().getActive());
-        assertEquals(11800, result.getBundlePromo().getDiscount());
-        assertEquals(1, result.getBundlePromo().getBundleList().size());
-        assertEquals("套餐一", result.getBundlePromo().getBundleList().get(0).getBundleName());
-        // 一口价废弃:giftPromo 恒空
-        assertNotNull(result.getGiftPromo());
-        assertFalse(result.getGiftPromo().getActive());
-        assertEquals(0, result.getGiftPromo().getDiscount());
-    }
-
-    @Test
-    void getCartList_emptyCart_bundlePromoInactive() {
-        when(cartMapper.selectList(any(Wrapper.class))).thenReturn(Collections.emptyList());
-
-        CartListResultDTO result = cartService.getCartList(1L);
-
-        assertNotNull(result.getBundlePromo());
-        assertFalse(result.getBundlePromo().getActive());
-        assertEquals(0, result.getBundlePromo().getDiscount());
-        assertFalse(result.getGiftPromo().getActive());
-    }
-
-    @Test
-    void getCartList_noBundleMatch_bundlePromoInactive() {
-        when(cartMapper.selectList(any(Wrapper.class)))
-                .thenReturn(Collections.singletonList(cart(10L, null, 1)));
-        when(productMapper.selectBatchIds(anyList()))
-                .thenReturn(Collections.singletonList(product(10L, "DAN测评", 69900)));
-        when(productMapper.selectList(any(Wrapper.class))).thenReturn(Collections.emptyList());
-        when(memberPriceResolver.resolveEligibleMemberPrice(any(Product.class), any())).thenReturn(69900);
-        // 定价服务判定不命中
-        when(bundlePricingService.compute(anyList())).thenReturn(BundleDiscountResult.inactive());
-
-        CartListResultDTO result = cartService.getCartList(1L);
-
-        assertNotNull(result.getBundlePromo());
-        assertFalse(result.getBundlePromo().getActive());
-        assertEquals(0, result.getBundlePromo().getDiscount());
-    }
-}

+ 0 - 140
cfc-backend/src/test/java/com/etotem/cfc/unit/ProductOrderServiceTest.java

@@ -1,140 +0,0 @@
-package com.etotem.cfc.unit;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.CreateProductOrderDTO;
-import com.etotem.cfc.dto.OrderItemVO;
-import com.etotem.cfc.dto.ProductOrderDTO;
-import com.etotem.cfc.entity.Product;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.OrderItemMapper;
-import com.etotem.cfc.mapper.ProductMapper;
-import com.etotem.cfc.mapper.ProductOrderMapper;
-import com.etotem.cfc.mapper.UserMapper;
-import com.etotem.cfc.pricing.BundleDiscountResult;
-import com.etotem.cfc.pricing.LineItem;
-import com.etotem.cfc.service.BundlePricingService;
-import com.etotem.cfc.service.CouponService;
-import com.etotem.cfc.service.MemberPriceResolver;
-import com.etotem.cfc.service.ProductGiftRuleService;
-import com.etotem.cfc.service.ProductOrderService;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyList;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * ProductOrderService.createMultiItem 单测:服务端权威重算套餐优惠计入 moneyAmount(前端 discountAmount 仅含规则优惠)。
- */
-@ExtendWith(MockitoExtension.class)
-class ProductOrderServiceTest {
-
-    @Mock private ProductMapper productMapper;
-    @Mock private UserMapper userMapper;
-    @Mock private ProductOrderMapper orderMapper;
-    @Mock private OrderItemMapper orderItemMapper;
-    @Mock private MemberPriceResolver memberPriceResolver;
-    @Mock private BundlePricingService bundlePricingService;
-    @Mock private ProductGiftRuleService productGiftRuleService;
-    @Mock private CouponService couponService;
-    @InjectMocks private ProductOrderService orderService;
-
-    private Product product(Long id, String name, int price) {
-        Product p = new Product();
-        p.setId(id);
-        p.setName(name);
-        p.setPrice(price);
-        p.setProductType("assessment");
-        p.setStatus("on_shelf");
-        return p;
-    }
-
-    private CreateProductOrderDTO dto(long productId, int qty, Integer discountAmount) {
-        CreateProductOrderDTO dto = new CreateProductOrderDTO();
-        OrderItemVO item = new OrderItemVO();
-        item.setProductId(productId);
-        item.setQuantity(qty);
-        dto.setItems(Collections.singletonList(item));
-        dto.setDiscountAmount(discountAmount);
-        return dto;
-    }
-
-    @Test
-    void createMultiItem_addsServerBundleDiscountToMoneyAmount() {
-        // 2×69900 配套餐省11800 → moneyAmount = 139800 - 0 - 11800 = 128000
-        when(productMapper.selectById(10L)).thenReturn(product(10L, "DAN测评", 69900));
-        when(memberPriceResolver.resolveEligibleMemberPrice(any(Product.class), anyLong())).thenReturn(69900);
-        BundleDiscountResult r = new BundleDiscountResult();
-        r.setActive(true);
-        r.setOriginalTotal(139800);
-        r.setPayPrice(128000);
-        r.setDiscount(11800);
-        r.setBundleList(new ArrayList<>());
-        when(bundlePricingService.compute(anyList())).thenReturn(r);
-        when(userMapper.selectById(anyLong())).thenReturn(new User());
-        when(orderMapper.insert(any())).thenReturn(1);
-        when(orderItemMapper.insert(any())).thenReturn(1);
-
-        Result<ProductOrderDTO> result = orderService.create(dto(10L, 2, null), 1L);
-
-        assertTrue(result.getCode() == 200, "下单应成功: " + result.getMessage());
-        assertEquals(128000, result.getData().getMoneyAmount());
-    }
-
-    @Test
-    void createMultiItem_bundleLinesCollectResolvedIdentityAndPrice() {
-        // 收集行项:解析后 productId、skuId=null、quantity、unitPrice=69900
-        when(productMapper.selectById(10L)).thenReturn(product(10L, "DAN测评", 69900));
-        when(memberPriceResolver.resolveEligibleMemberPrice(any(Product.class), anyLong())).thenReturn(69900);
-        when(bundlePricingService.compute(anyList())).thenReturn(BundleDiscountResult.inactive());
-        when(userMapper.selectById(anyLong())).thenReturn(new User());
-        when(orderMapper.insert(any())).thenReturn(1);
-        when(orderItemMapper.insert(any())).thenReturn(1);
-
-        orderService.create(dto(10L, 2, null), 1L);
-
-        ArgumentCaptor<List<LineItem>> captor = ArgumentCaptor.forClass(List.class);
-        verify(bundlePricingService).compute(captor.capture());
-        List<LineItem> lines = captor.getValue();
-        assertEquals(1, lines.size());
-        assertEquals(10L, lines.get(0).getProductId());
-        assertEquals(null, lines.get(0).getSkuId());
-        assertEquals(2, lines.get(0).getQuantity());
-        assertEquals(69900, lines.get(0).getUnitPrice());
-    }
-
-    @Test
-    void createMultiItem_frontendRuleDiscountNotMixedWithBundle() {
-        // 前端传入规则优惠5000(不含套餐)+ 套餐11800 → moneyAmount = 139800 - 5000 - 11800 = 123000
-        when(productMapper.selectById(10L)).thenReturn(product(10L, "DAN测评", 69900));
-        when(memberPriceResolver.resolveEligibleMemberPrice(any(Product.class), anyLong())).thenReturn(69900);
-        BundleDiscountResult r = new BundleDiscountResult();
-        r.setActive(true);
-        r.setOriginalTotal(139800);
-        r.setPayPrice(128000);
-        r.setDiscount(11800);
-        r.setBundleList(new ArrayList<>());
-        when(bundlePricingService.compute(anyList())).thenReturn(r);
-        when(userMapper.selectById(anyLong())).thenReturn(new User());
-        when(orderMapper.insert(any())).thenReturn(1);
-        when(orderItemMapper.insert(any())).thenReturn(1);
-
-        Result<ProductOrderDTO> result = orderService.create(dto(10L, 2, 5000), 1L);
-
-        assertTrue(result.getCode() == 200);
-        assertEquals(123000, result.getData().getMoneyAmount());
-    }
-}