|
|
@@ -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(
|