Bläddra i källkod

chore: auto bump version and changelog [skip ci]

iwt 4 dagar sedan
förälder
incheckning
b27ceb1941

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1055,8 +1055,10 @@ public class DatabaseInitializer implements CommandLineRunner {
             insertSysConfigSeed("trial_days", "7", "试用天数");
             insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
             insertSysConfigSeed("member_pay_virtual_enabled", "1", "会员支付是否使用虚拟支付通道(1=虚拟,0=普通微信)");
-            insertSysConfigSeed("gift_membership_threshold", "198000", "购买送会员商品累计达到该金额(分)赠送会员");
-            insertSysConfigSeed("gift_membership_level", "FAMILY", "达到送会员额度后赠送的会员等级");
+insertSysConfigSeed("gift_membership_threshold", "198000", "购买送会员商品累计达到该金额(分)赠送会员");
+insertSysConfigSeed("gift_membership_level", "FAMILY", "达到送会员额度后赠送的会员等级");
+insertSysConfigSeed("gift_membership_original_threshold", "1000000", "送会员商品原价达标阈值(分),达到此金额后送会员商品部分实付一口价");
+insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达标后一口实付价(分)");
             insertSysConfigSeed("health_dimension_weights", "{\"growth\":0.20,\"sleep\":0.15,\"vision\":0.10,\"immunity\":0.15,\"nutrition\":0.15,\"gut\":0.10,\"exercise\":0.15}", "7维健康→身能量权重配置");
             insertSysConfigSeed("health_dimension_sync_factor", "0.5", "7维健康同步至身能量系数");
             log.info("SysConfig 种子数据已加载");

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java

@@ -1,7 +1,7 @@
 package com.etotem.cfc.controller.cart;
 
 import com.etotem.cfc.common.Result;
-import com.etotem.cfc.dto.CartItemDTO;
+import com.etotem.cfc.dto.CartListResultDTO;
 import com.etotem.cfc.service.CartService;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestAttribute;
@@ -36,8 +36,8 @@ public class CartController {
     }
 
     @PostMapping("/list")
-    public Result<List<CartItemDTO>> list(@RequestAttribute("userId") Long userId) {
-        List<CartItemDTO> list = cartService.getCartList(userId);
+    public Result<CartListResultDTO> list(@RequestAttribute("userId") Long userId) {
+        CartListResultDTO list = cartService.getCartList(userId);
         return Result.success(list);
     }
 

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

@@ -53,4 +53,20 @@ public class CartItemDTO {
         private Long childProductId;
         private Integer quantity;
     }
+
+    @Data
+    public static class GiftPromo {
+        /** 送会员商品原价合计(分) */
+        private Integer giftOriginalTotal;
+        /** 阈值(分) */
+        private Integer threshold;
+        /** 一口实付价(分),达标后送会员商品部分实付价 */
+        private Integer payPrice;
+        /** 折扣金额(分) */
+        private Integer discount;
+        /** 是否达标 */
+        private Boolean active;
+        /** 还差多少到阈值(分) */
+        private Integer remaining;
+    }
 }

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

@@ -0,0 +1,12 @@
+package com.etotem.cfc.dto;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class CartListResultDTO {
+
+    private List<CartItemDTO> items;
+
+    private CartItemDTO.GiftPromo giftPromo;
+}

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

@@ -2,6 +2,7 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 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;
@@ -13,6 +14,7 @@ import com.etotem.cfc.mapper.ProductMapper;
 import com.etotem.cfc.service.PpointService;
 import com.etotem.cfc.service.ProductGiftRuleService;
 import com.etotem.cfc.service.ProductSkuService;
+import com.etotem.cfc.service.SysConfigService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -45,6 +47,9 @@ public class CartService {
     @Resource
     private ProductGiftRuleService productGiftRuleService;
 
+    @Resource
+    private SysConfigService sysConfigService;
+
     @Transactional
     public CartItemDTO addToCart(Long userId, Long productId, Integer quantity, Long skuId) {
         Product product = productMapper.selectById(productId);
@@ -84,7 +89,7 @@ public class CartService {
         return buildCartItemDTO(saved);
     }
 
-    public List<CartItemDTO> getCartList(Long userId) {
+    public CartListResultDTO getCartList(Long userId) {
         List<Cart> cartList = cartMapper.selectList(
             new LambdaQueryWrapper<Cart>()
                 .eq(Cart::getUserId, userId)
@@ -122,7 +127,13 @@ public class CartService {
         // 自动套餐模式:检测购物车子商品组合是否凑成某套餐,命中则挂 bundleMatch 提示
         attachBundleMatches(cartList, result);
 
-        return result;
+        // 计算送会员商品聚合信息
+        CartItemDTO.GiftPromo giftPromo = computeGiftPromo(cartList, productMap);
+
+        CartListResultDTO cartResult = new CartListResultDTO();
+        cartResult.setItems(result);
+        cartResult.setGiftPromo(giftPromo);
+        return cartResult;
     }
 
     /**
@@ -395,4 +406,88 @@ public class CartService {
         }
         return dto;
     }
+
+    /**
+     * 计算购物车中送会员商品的聚合信息:原价合计、是否达标、折扣金额等。
+     * 购物车所有商品默认视为已选中(checked=true)。
+     */
+    private CartItemDTO.GiftPromo computeGiftPromo(List<Cart> cartList, Map<Long, Product> productMap) {
+        if (cartList == null || cartList.isEmpty()) {
+            return buildEmptyGiftPromo();
+        }
+
+        // 查询购物车中所有商品的送会员规则
+        List<Long> giftProductIds = new ArrayList<>();
+        for (Cart cart : cartList) {
+            Long pid = cart.getProductId();
+            if (pid != null && !giftProductIds.contains(pid)) {
+                giftProductIds.add(pid);
+            }
+        }
+        List<ProductGiftRule> giftRules = productGiftRuleService.listActiveByProductIds(giftProductIds);
+
+        // 按 productId 建立 rule 映射
+        Map<Long, ProductGiftRule> ruleMap = new HashMap<>();
+        for (ProductGiftRule rule : giftRules) {
+            if (rule.getProductId() != null) {
+                ruleMap.put(rule.getProductId(), rule);
+            }
+        }
+
+        // 计算送会员商品原价合计
+        int giftOriginalTotal = 0;
+        for (Cart cart : cartList) {
+            if (ruleMap.containsKey(cart.getProductId())) {
+                Product product = productMap.get(cart.getProductId());
+                if (product != null && product.getPrice() != null && cart.getQuantity() != null) {
+                    giftOriginalTotal += product.getPrice() * cart.getQuantity();
+                }
+            }
+        }
+
+        // 读取配置
+        String thresholdStr = sysConfigService.getValue("gift_membership_original_threshold");
+        int threshold;
+        try {
+            threshold = thresholdStr != null ? Integer.parseInt(thresholdStr) : 0;
+        } catch (NumberFormatException e) {
+            threshold = 0;
+        }
+
+        String payPriceStr = sysConfigService.getValue("gift_membership_pay_price");
+        int payPrice;
+        try {
+            payPrice = payPriceStr != null ? Integer.parseInt(payPriceStr) : 0;
+        } catch (NumberFormatException e) {
+            payPrice = 0;
+        }
+
+        if (threshold <= 0) {
+            return buildEmptyGiftPromo();
+        }
+
+        boolean active = giftOriginalTotal >= threshold;
+        int discount = active ? Math.max(0, giftOriginalTotal - payPrice) : 0;
+        int remaining = active ? 0 : Math.max(0, threshold - giftOriginalTotal);
+
+        CartItemDTO.GiftPromo promo = new CartItemDTO.GiftPromo();
+        promo.setGiftOriginalTotal(giftOriginalTotal);
+        promo.setThreshold(threshold);
+        promo.setPayPrice(active ? payPrice : 0);
+        promo.setDiscount(discount);
+        promo.setActive(active);
+        promo.setRemaining(remaining);
+        return promo;
+    }
+
+    private CartItemDTO.GiftPromo buildEmptyGiftPromo() {
+        CartItemDTO.GiftPromo promo = new CartItemDTO.GiftPromo();
+        promo.setGiftOriginalTotal(0);
+        promo.setThreshold(0);
+        promo.setPayPrice(0);
+        promo.setDiscount(0);
+        promo.setActive(false);
+        promo.setRemaining(0);
+        return promo;
+    }
 }

+ 31 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -59,6 +59,7 @@ import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.UnlockGateService;
 import com.etotem.cfc.service.UserService;
 import com.etotem.cfc.service.GiftMembershipProgressService;
+import com.etotem.cfc.service.SysConfigService;
 import com.alibaba.fastjson.JSON;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -145,6 +146,9 @@ public class ProductOrderService {
     @Resource
     private GiftMembershipProgressService giftMembershipProgressService;
 
+    @Resource
+    private SysConfigService sysConfigService;
+
     @Resource
     private MembershipService membershipService;
 
@@ -812,17 +816,41 @@ public class ProductOrderService {
             log.error("发放测评额度失败: orderNo={}", orderNo, e);
             // 不阻塞支付流程
         }
-        // 赠送会员:原价支付时,送会员商品金额累计到家庭进度,达到阈值自动赠送会员
+        // 赠送会员:单笔订单送会员商品原价合计达标即赠送会员
         try {
             if (order.getGiftAmount() != null && order.getGiftAmount() > 0 && order.getFamilyId() != null) {
-                // 原价判定:无积分抵扣、无优惠券、无折扣
+                String origThresholdStr = sysConfigService.getValue("gift_membership_original_threshold");
+                int origThreshold;
+                try {
+                    origThreshold = origThresholdStr != null ? Integer.parseInt(origThresholdStr) : 0;
+                } catch (NumberFormatException e) {
+                    origThreshold = 0;
+                }
+                // 只要单笔订单送会员商品原价合计>=阈值,即直接赠送会员(不再受是否有折扣/积分/优惠券限制)
+                if (origThreshold > 0 && order.getGiftAmount() >= origThreshold) {
+                    String levelCode = sysConfigService.getValue("gift_membership_level");
+                    if (levelCode == null || levelCode.isEmpty()) {
+                        levelCode = "FAMILY";
+                    }
+                    boolean granted = membershipService.grantMembershipByGift(
+                            order.getFamilyId(), order.getBuyerId(), levelCode, null, null);
+                    if (granted) {
+                        // 同步记录进度(已赠送标记)
+                        giftMembershipProgressService.accumulate(
+                                order.getFamilyId(), order.getBuyerId(), order.getGiftAmount());
+                        log.info("商品订单{}单笔达标送会员: giftAmount={}, threshold={}, granted={}", orderNo, order.getGiftAmount(), origThreshold, granted);
+                    }
+                }
+                // 兼容原有累计进度展示(仅统计无折扣/无券/无积分的订单)
                 boolean fullPrice = (order.getPointsUsed() == null || order.getPointsUsed() == 0)
                         && order.getCouponId() == null
                         && (order.getDiscountAmount() == null || order.getDiscountAmount() == 0);
                 if (fullPrice) {
                     boolean granted = giftMembershipProgressService.accumulate(
                             order.getFamilyId(), order.getBuyerId(), order.getGiftAmount());
-                    log.info("商品订单{}累计送会员金额={}, 触发赠送={}", orderNo, order.getGiftAmount(), granted);
+                    if (granted) {
+                        log.info("商品订单{}累计送会员金额={}, 触发赠送={}", orderNo, order.getGiftAmount(), granted);
+                    }
                 }
             }
         } catch (Exception e) {

+ 49 - 19
cfc-frontend/pages/shop/cart/cart.vue

@@ -73,22 +73,38 @@
     </view>
 
     <!-- Gift Membership Progress -->
-    <view v-if="giftProgress && !giftIsGranted && giftProgress.threshold > 0" class="gift-progress-section">
-      <view class="gift-progress-header">
-        <text class="gift-progress-icon">🎁</text>
-        <text class="gift-progress-title">购会员商品累计进度</text>
-        <text class="gift-progress-percent">{{ giftProgressPercent }}%</text>
-      </view>
-      <view class="gift-progress-bar-bg">
-        <view class="gift-progress-bar-fill" :style="{ width: giftProgressPercent + '%' }"></view>
+    <view v-if="!giftPromo || (giftPromo && !giftPromo.active)" class="gift-progress-section">
+      <view v-if="giftProgress && !giftIsGranted && giftProgress.threshold > 0" class="gift-progress-section">
+        <view class="gift-progress-header">
+          <text class="gift-progress-icon">🎁</text>
+          <text class="gift-progress-title">购会员商品累计进度</text>
+          <text class="gift-progress-percent">{{ giftProgressPercent }}%</text>
+        </view>
+        <view class="gift-progress-bar-bg">
+          <view class="gift-progress-bar-fill" :style="{ width: giftProgressPercent + '%' }"></view>
+        </view>
+        <view class="gift-progress-info">
+          <text class="gift-progress-accumulated">已累 {{ giftAccumulatedYuan }} 元</text>
+          <text class="gift-progress-remaining">还差 {{ giftRemainingYuan }} 元送会员</text>
+        </view>
+        <view v-if="giftProgress.grantedLevel" class="gift-progress-level">
+          <text>达标将赠送 {{ giftProgress.grantedLevel === 'FAMILY' ? '家庭会员' : giftProgress.grantedLevel }} 权益</text>
+        </view>
       </view>
-      <view class="gift-progress-info">
-        <text class="gift-progress-accumulated">已累 {{ giftAccumulatedYuan }} 元</text>
-        <text class="gift-progress-remaining">还差 {{ giftRemainingYuan }} 元送会员</text>
+    </view>
+
+    <!-- 送会员一口价促销区 -->
+    <view v-if="giftPromo && giftPromo.active && !giftIsGranted" class="gift-promo-section">
+      <view class="gift-promo-header">
+        <text class="gift-promo-icon">🎁</text>
+        <text class="gift-promo-title">购会员商品原价满 {{ giftPromo.threshold / 100 }} 元,实付 {{ giftPromo.payPrice / 100 }} 元</text>
       </view>
-      <view v-if="giftProgress.grantedLevel" class="gift-progress-level">
-        <text>达标将赠送 {{ giftProgress.grantedLevel === 'FAMILY' ? '家庭会员' : giftProgress.grantedLevel }} 权益</text>
+      <view class="gift-promo-bar">
+        <view v-if="giftPromo.remaining > 0" class="gift-promo-bar-fill" :style="{ width: Math.max(0, (100 - giftPromo.remaining / giftPromo.threshold * 100)) + '%' }"></view>
+        <view v-else class="gift-promo-bar-fill"></view>
       </view>
+      <view v-if="giftPromo.remaining > 0" class="gift-promo-remaining">还差 {{ giftPromo.remaining / 100 }} 元达标</view>
+      <view v-else class="gift-promo-reached">已达标,支付后自动赠送会员</view>
     </view>
 
     <view v-else-if="giftIsGranted" class="gift-granted-hint">
@@ -135,7 +151,8 @@ export default {
       refreshing: false,
       userId: null,
       isMember: false,
-      giftProgress: null  // 赠会员累计进度
+      giftProgress: null,  // 赠会员累计进度
+      giftPromo: null      // 送会员一口价促销信息
     }
   },
   computed: {
@@ -154,6 +171,9 @@ export default {
       return count
     },
     totalAmount() {
+      if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0) {
+        return this.originalTotalAmount - this.giftPromo.discount
+      }
       var total = 0
       for (var i = 0; i < this.items.length; i++) {
         if (this.items[i].checked) {
@@ -181,9 +201,17 @@ export default {
           if (this.isMember && this.items[i].memberPrice && this.items[i].memberPrice > 0 && this.items[i].memberPrice < price) {
             price = this.items[i].memberPrice
           }
+          // 送会员一口价折扣:若达标,从总价中减去 discount
+          if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0 && this.items[i].giftLevelName) {
+            // 不在此处单独扣除,由 totalAmount() 已在 top 层减去;这里仅作备选兼容
+          }
           total = total + (price * this.items[i].quantity)
         }
       }
+      // 若 giftPromo 达标,从总价中减去 discount
+      if (this.giftPromo && this.giftPromo.active === true && this.giftPromo.discount > 0) {
+        total = total - this.giftPromo.discount
+      }
       return total
     },
     hasGiftItems() {
@@ -274,12 +302,14 @@ export default {
           that.loading = false
           that.refreshing = false
           if (res.data && res.data.code === 200) {
-            var list = res.data.data || []
-            for (var i = 0; i < list.length; i++) {
-              list[i].checked = true
-              list[i].subtotal = list[i].unitPrice * list[i].quantity
+            var result = res.data.data
+            that.items = result.items || []
+            for (var i = 0; i < that.items.length; i++) {
+              that.items[i].checked = true
+              that.items[i].subtotal = that.items[i].unitPrice * that.items[i].quantity
             }
-            that.items = list
+            that.giftPromo = result.giftPromo || null
+            that.loadGiftProgress()
           }
         },
         fail: function() {

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-0abaf8e691758da5c4446a7bf48afc5e57b61d10
+08d52ac04cbd362171a9c7aac0d806fe04fab04b

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1505",
+  "version": "1.0.1506",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1505",
+      "version": "1.0.1506",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1506",
+  "version": "1.0.1507",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 10 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,16 @@
 
 ---
 
+## v1.0.1507 (2026-10-05)
+
+### Bug 修复
+- ProductEdit.vue addBundleItem 防止 null 报错
+
+### 其他
+- - 防止 TypeError: Cannot read properties of null (reading 'bundleItems')
+- 
+
+
 ## v1.0.1506 (2026-10-05)
 
 ### 文档

+ 11 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1506
+> 当前版本: v1.0.1507
 
 ## 历史版本
 
@@ -8,6 +8,16 @@
 
 ---
 
+## v1.0.1507 (2026-10-05)
+
+### Bug 修复
+- ProductEdit.vue addBundleItem 防止 null 报错
+
+### 其他
+- - 防止 TypeError: Cannot read properties of null (reading 'bundleItems')
+- 
+
+
 ## v1.0.1506 (2026-10-05)
 
 ### 文档