فهرست منبع

feat: 购物车功能完善 - CartItemDTO和CartService改造

Sisyphus Agent 15 ساعت پیش
والد
کامیت
b6a8c3b970

+ 42 - 6
cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java

@@ -2,6 +2,8 @@ package com.etotem.cfc.dto;
 
 import lombok.Data;
 
+import java.util.List;
+
 @Data
 public class CartItemDTO {
     private Long id;
@@ -10,11 +12,45 @@ public class CartItemDTO {
     private Long skuId;
     private Integer quantity;
     private String productName;
-    private Integer unitPrice;        // 原价(分)
-    private Integer memberPrice;      // 会员价(分),null=无会员价
+    private Integer unitPrice;
+    private Integer memberPrice;
     private String coverImage;
-    private String specDesc;      // SKU规格描述,如"颜色:红色;尺码:M"
-    private String supplierName; // 供应商名称
-    private String giftLevelCode;     // 购此商品赠会员等级编码(null=无赠送规则)
-    private String giftLevelName;     // 赠会员等级名称(如"家庭会员")
+    private String specDesc;
+    private String supplierName;
+    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;
+    }
 }

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

@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.dto.CartItemDTO;
 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.service.PpointService;
 import com.etotem.cfc.service.ProductGiftRuleService;
@@ -18,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.alibaba.fastjson.JSON;
@@ -33,6 +36,9 @@ public class CartService {
     @Resource
     private ProductMapper productMapper;
 
+    @Resource
+    private ProductBundleItemMapper bundleItemMapper;
+
     @Resource
     private ProductSkuService productSkuService;
 
@@ -85,16 +91,182 @@ public class CartService {
                 .orderByDesc(Cart::getCreatedAt)
         );
 
+        List<Long> productIds = new ArrayList<>();
+        for (Cart item : cartList) {
+            if (!productIds.contains(item.getProductId())) {
+                productIds.add(item.getProductId());
+            }
+        }
+        Map<Long, Product> productMap = new HashMap<>();
+        if (!productIds.isEmpty()) {
+            List<Product> products = productMapper.selectBatchIds(productIds);
+            for (Product p : products) {
+                if (p != null) {
+                    productMap.put(p.getId(), p);
+                }
+            }
+        }
+
         List<CartItemDTO> result = new ArrayList<>();
         for (Cart item : cartList) {
             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);
+
         return result;
     }
 
+    /**
+     * 自动套餐检测:聚合购物车中子商品数量,遍历所有 bundle 商品,若能覆盖套餐组成
+     * (每个子商品数量 ≥ 组成数量),则在该子商品项上挂 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> childIds = new ArrayList<>();
+        List<String> childNames = new ArrayList<>();
+        int childQtyPerBundle = 0;
+        for (ProductBundleItem bi : bundleItems) {
+            childIds.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(childIds);
+        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(