Bläddra i källkod

feat.coupon.non-member-could-claim-membership-coupon

liaoxg 2 månader sedan
förälder
incheckning
694ac739fb

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java

@@ -33,6 +33,9 @@ public class CouponService {
     @Resource
     private CouponGrantLogMapper couponGrantLogMapper;
 
+    @Resource
+    private MembershipService membershipService;
+
     public List<Coupon> listAvailable(Long userId) {
         List<UserCoupon> userCoupons = userCouponMapper.selectList(
                 new LambdaQueryWrapper<UserCoupon>()
@@ -75,6 +78,7 @@ public class CouponService {
         if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
             return "优惠券已过期";
         }
+        // 非会员也可领取会员券,但在使用时会校验会员身份
         Long count = userCouponMapper.selectCount(
                 new LambdaQueryWrapper<UserCoupon>()
                         .eq(UserCoupon::getUserId, userId)
@@ -93,6 +97,15 @@ public class CouponService {
         return "领取成功";
     }
 
+    /**
+     * 判断用户是否为有效会员(FAMILY 或 PREMIUM 且未过期)
+     */
+    private boolean isMember(Long userId) {
+        if (userId == null) return false;
+        String level = membershipService.getMemberLevel(userId);
+        return "FAMILY".equals(level) || "PREMIUM".equals(level);
+    }
+
     public Integer apply(Long userId, Long userCouponId, String orderType, Integer orderAmount) {
         UserCoupon uc = userCouponMapper.selectById(userCouponId);
         if (uc == null || !uc.getUserId().equals(userId) || !"AVAILABLE".equals(uc.getStatus())) {
@@ -115,6 +128,10 @@ public class CouponService {
         if (!"ALL".equals(coupon.getApplicableTo()) && !coupon.getApplicableTo().equals(orderType)) {
             return null;
         }
+        // 新增:会员专属券(applicableTo=MEMBERSHIP)仅允许会员使用
+        if ("MEMBERSHIP".equals(coupon.getApplicableTo()) && !isMember(userId)) {
+            return null;
+        }
         // 无门槛券(CASH)忽略起用门槛;满减/折扣券校验
         if (!"CASH".equals(coupon.getType()) && coupon.getMinSpend() != null && orderAmount < coupon.getMinSpend()) {
             return null;
@@ -154,6 +171,10 @@ public class CouponService {
         return couponMapper.selectList(null);
     }
 
+    public Coupon getById(Long couponId) {
+        return couponMapper.selectById(couponId);
+    }
+
     @Transactional
     public void issueToUser(Long couponId, Long userId) {
         UserCoupon uc = new UserCoupon();

+ 62 - 5
cfc-frontend/pages/membership/pay.vue

@@ -21,6 +21,11 @@
           <text v-else class="coupon-row-placeholder">选择优惠券</text>
           <text class="coupon-row-arrow">›</text>
         </view>
+        <view v-if="!isMember && hasMembershipCoupons" class="member-hint-row" @click="goToMembership">
+          <text class="member-hint-icon">👑</text>
+          <text class="member-hint-text">开通会员可使用会员专属优惠券</text>
+          <text class="coupon-row-arrow">›</text>
+        </view>
         <view class="divider"></view>
         <view class="total-row">
           <text class="total-label">实付</text>
@@ -67,7 +72,7 @@
             </view>
             <view class="picker-item-right">
               <text class="picker-item-name">{{ coupon.name }}</text>
-              <text class="picker-item-condition">{{ getCouponCondition(coupon.minSpend) }}</text>
+              <text class="picker-item-condition">{{ getCouponCondition(coupon) }}</text>
             </view>
             <view class="picker-item-check" v-if="selectedCoupon && selectedCoupon.id === coupon.id">
               <text>✓</text>
@@ -105,7 +110,8 @@ export default {
       currentPeriod: 1,
       splitAmount: 0,
       orderNo: null,
-      paying: false
+      paying: false,
+      isMember: false
     }
   },
   computed: {
@@ -144,8 +150,16 @@ export default {
       return this.coupons.filter(function(c) {
         if (c.status && c.status !== 'AVAILABLE') return false
         if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'MEMBERSHIP') return false
+        // 会员专属券:非会员不显示在可选列表中
+        if (c.applicableTo === 'MEMBERSHIP' && !self.isMember) return false
         return true
       })
+    },
+    hasMembershipCoupons: function() {
+      var self = this
+      return this.coupons.some(function(c) {
+        return c.applicableTo === 'MEMBERSHIP' && (!c.status || c.status === 'AVAILABLE')
+      })
     }
   },
   onLoad: function(options) {
@@ -154,6 +168,7 @@ export default {
     this.periodLabel = this.getPeriodLabel(this.period)
     this.loadPlans()
     this.loadCoupons()
+    this.checkMemberStatus()
   },
   methods: {
     getPeriodLabel: function(period) {
@@ -206,9 +221,27 @@ export default {
       this.selectedCoupon = null
       this.showCouponPicker = false
     },
-    getCouponCondition: function(minSpend) {
-      if (!minSpend || minSpend <= 0) return '无门槛'
-      return '满' + (minSpend / 100).toFixed(2) + '元可用'
+    checkMemberStatus: function() {
+      var self = this
+      uni.request({
+        url: getApp().$config.baseUrl + '/api/membership/my',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var membership = res.data.data.membership
+            self.isMember = membership && membership.isActive
+          }
+        }
+      })
+    },
+    getCouponCondition: function(coupon) {
+      if (!coupon) return '无门槛'
+      if (coupon.applicableTo === 'MEMBERSHIP' && !this.isMember) {
+        return '开通会员后可用'
+      }
+      if (!coupon.minSpend || coupon.minSpend <= 0) return '无门槛'
+      return '满' + (coupon.minSpend / 100).toFixed(2) + '元可用'
     },
     formatPrice: function(value) {
       if (value == null) return '0.00'
@@ -331,6 +364,9 @@ export default {
         self.paying = false
         uni.redirectTo({ url: '/pages/membership/result?orderNo=' + (self.orderNo || '') + '&status=success' })
       }, 800)
+    },
+    goToMembership: function() {
+      uni.navigateTo({ url: '/pages/membership/index' })
     }
   }
 }
@@ -422,6 +458,27 @@ export default {
   color: #ccc;
   margin-left: 8rpx;
 }
+.member-hint-row {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  margin-top: 8rpx;
+  background: #FFF7ED;
+  border-radius: 12rpx;
+}
+.member-hint-row:active {
+  opacity: 0.7;
+}
+.member-hint-icon {
+  font-size: 28rpx;
+  margin-right: 12rpx;
+}
+.member-hint-text {
+  flex: 1;
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: 500;
+}
 .divider {
   height: 1rpx;
   background: #F1F5F9;

+ 51 - 4
cfc-frontend/pages/profile-extra/coupons.vue

@@ -71,13 +71,16 @@
               <text v-if="getStatusLabel(coupon)" class="card-status-badge" :class="statusClassMap[coupon.id]">{{ getStatusLabel(coupon) }}</text>
             </view>
             <text class="card-condition">{{ getConditionText(coupon.minSpend) }}</text>
-            <text class="card-scope">{{ getScopeText(coupon.applicableTo) }}</text>
+            <text class="card-scope">{{ getScopeText(coupon.applicableTo, isMember) }}</text>
             <text class="card-expiry">有效期至 {{ formatDate(coupon.validUntil) }}</text>
             <button
               v-if="canClaim(coupon)"
               class="claim-btn"
               @click="handleClaim(coupon)"
             >立即领取</button>
+            <view v-else-if="coupon.applicableTo === 'MEMBERSHIP' && !isMember && (!coupon.status || coupon.status === 'AVAILABLE')" class="member-guide-btn" @click="goToMembership">
+              <text>去开通会员</text>
+            </view>
           </view>
         </view>
       </view>
@@ -102,7 +105,8 @@ export default {
       coupons: [],
       exchangeableList: [],
       loading: false,
-      refreshing: false
+      refreshing: false,
+      isMember: false
     }
   },
   computed: {
@@ -137,6 +141,7 @@ export default {
   onShow() {
     this.loadCoupons()
     this.loadExchangeable()
+    this.checkMemberStatus()
   },
   onPullDownRefresh() {
     this.refreshing = true
@@ -177,6 +182,20 @@ export default {
         console.error('加载可兑换优惠券失败', e)
       })
     },
+    checkMemberStatus() {
+      var self = this
+      uni.request({
+        url: getApp().$config.baseUrl + '/api/membership/my',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
+        success: function(res) {
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var membership = res.data.data.membership
+            self.isMember = membership && membership.isActive
+          }
+        }
+      })
+    },
     getCouponKey(item, index) {
       return item.id + '-' + index
     },
@@ -209,6 +228,7 @@ export default {
       })
     },
     canClaim(coupon) {
+      // 非会员也可领取会员券,但使用时需开通会员
       return this.tabIndex === 0 && (!coupon.status || coupon.status === 'AVAILABLE')
     },
     async handleClaim(coupon) {
@@ -224,9 +244,11 @@ export default {
       if (!minSpend || minSpend <= 0) return '无门槛'
       return '满' + (minSpend / 100).toFixed(2) + '元可用'
     },
-    getScopeText(applicableTo) {
+    getScopeText(applicableTo, isMember) {
       if (applicableTo === 'ALL') return '全品类适用'
-      if (applicableTo === 'MEMBERSHIP') return '仅限会员'
+      if (applicableTo === 'MEMBERSHIP') {
+        return isMember ? '会员专享' : '开通会员后可用'
+      }
       return applicableTo || '全品类适用'
     },
     getTypeLabel(type) {
@@ -252,6 +274,9 @@ export default {
       var m = String(d.getMonth() + 1).padStart(2, '0')
       var day = String(d.getDate()).padStart(2, '0')
       return y + '.' + m + '.' + day
+    },
+    goToMembership() {
+      uni.navigateTo({ url: '/pages/membership/index' })
     }
   }
 }
@@ -473,4 +498,26 @@ export default {
 .claim-btn:active {
   opacity: 0.85;
 }
+
+.member-guide-btn {
+  position: absolute;
+  right: 24rpx;
+  bottom: 20rpx;
+  background: #5B9BD5;
+  color: #fff;
+  font-size: 24rpx;
+  padding: 10rpx 28rpx;
+  border-radius: 28rpx;
+  border: none;
+  line-height: 1.4;
+  height: auto;
+}
+
+.member-guide-btn::after {
+  border: none;
+}
+
+.member-guide-btn:active {
+  opacity: 0.85;
+}
 </style>