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

fix(admin+shop): 会员升级自动发JOIN券/用户列表倒序/商品券支付可选/兑换提醒移至支付

- AdminController.updateMembershipUser: 等级升级到付费等级时自动调
  grantFamilyJoinCoupons 发放 JOIN 赠券(幂等防重), 记录发券结果
- AdminController /users: 无 sort 参数时默认 orderByDesc(createdAt) 倒序
- CouponService.toCouponVO: 补 productId 字段, 前端可按商品匹配券
- checkout.vue: ownedCoupons/availableCoupons 对 PRODUCT 券按当前商品
  productId 匹配(原来一刀切过滤非 ALL 券导致商品券选不到); 新增
  affordableExchangeCoupons - CF 不足时不再显示兑换提醒
- product-detail.vue: 移除商品页积分兑换优惠券区块, 兑换提醒移至支付时
  (无对应券+CF 足够才提示)
Xiaogang Liao 1 неделя назад
Родитель
Сommit
a139c10d6e

+ 25 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -165,7 +165,11 @@ public class AdminController {
         List<Map<String, String>> sortSpecs = params.get("sort") != null
                 ? (List<Map<String, String>>) params.get("sort")
                 : null;
-        SortUtil.applySort(wrapper, sortSpecs, null);
+        if (sortSpecs != null && !sortSpecs.isEmpty()) {
+            SortUtil.applySort(wrapper, sortSpecs, null);
+        } else {
+            wrapper.orderByDesc(User::getCreatedAt);
+        }
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
         return Result.success(result);
     }
@@ -1398,6 +1402,9 @@ public class AdminController {
     @Resource
     private com.etotem.cfc.service.ProductOrderService productOrderService;
 
+    @Resource
+    private com.etotem.cfc.service.CouponService couponService;
+
     @PostMapping("/product/pending")
     public Result<java.util.List<com.etotem.cfc.dto.ProductDTO>> productPending() {
         return productService.pendingReview();
@@ -1730,6 +1737,23 @@ public class AdminController {
                 record.setApproveRemark("管理员会员维护");
                 record.setCreatedAt(new Date());
                 memberUpgradeRecordMapper.insert(record);
+
+                // 升级到付费等级(非 FREE)时自动发放 JOIN 赠券(幂等:family_coupon 唯一键防重)
+                if (!"FREE".equals(newLevel)) {
+                    User changedUser = userMapper.selectById(targetUserId);
+                    if (changedUser != null && changedUser.getFamilyId() != null) {
+                        try {
+                            couponService.grantFamilyJoinCoupons(
+                                    changedUser.getFamilyId(), newLevel,
+                                    "admin:" + adminUserId);
+                            changes.append("发放").append(newLevel).append("JOIN券; ");
+                        } catch (Exception ex) {
+                            changes.append("发放").append(newLevel).append("JOIN券异常:").append(ex.getMessage()).append("; ");
+                        }
+                    } else {
+                        changes.append("升级").append(newLevel).append("(无家庭,跳过发券); ");
+                    }
+                }
             }
         }
 

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

@@ -86,6 +86,7 @@ public class CouponService {
         item.put("receivedAt", fc.getReceivedAt());
         item.put("usedAt", fc.getUsedAt());
         item.put("orderId", fc.getOrderId());
+        item.put("productId", coupon.getProductId());
         return item;
     }
 

+ 5 - 66
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -99,19 +99,6 @@
         </view>
       </view>
 
-      <!-- 积分兑换优惠券 -->
-      <view class="coupon-exchange-section" v-if="productCoupons.length > 0">
-        <text class="section-title">积分兑换优惠券</text>
-        <view class="coupon-item" v-for="(item, index) in productCoupons" :key="getCouponKey(item, index)">
-          <view class="coupon-info">
-            <text class="coupon-name">{{ item.name }}</text>
-            <text class="coupon-desc">{{ getCouponDesc(item) }}</text>
-          </view>
-          <view class="coupon-price">{{ item.pointsPrice }}积分</view>
-          <button class="coupon-exchange-btn" @click="doExchangeCoupon(item)">兑换</button>
-        </view>
-      </view>
-
       <view class="intro-section" v-if="product.intro">
         <text class="section-title">商品介绍</text>
         <mp-html :content="product.intro" :domain="apiBaseUrl" />
@@ -160,7 +147,7 @@
 
 <script>
 import config from '@/config.js'
-import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getProductCoupons, exchangeCoupon, getMyMembership } from '@/utils/api.js'
+import { productDetail, productOrderCreate, productOrderPay, cartAdd, getShareQrCode, productSpecMap, getMyMembership } from '@/utils/api.js'
 import MpHtml from '@/components/mp-html/mp-html.vue'
 import ContentSharePoster from '@/components/ContentSharePoster.vue'
 import shareMixin from '../../../components/share-mixin.js'
@@ -180,7 +167,6 @@ export default {
       specGroups: [],
       selectedSpecs: {},
       selectedSku: null,
-      productCoupons: [],
       cfEarnedValue: 0,
       // 会员身份(实时调 getMyMembership,FREE=非会员)
       isMember: false,
@@ -379,6 +365,10 @@ export default {
         if (res.code === 200 && res.data) {
           that.product = res.data
           that.bundleItems = (res.data && res.data.bundleItems) || []
+          // 计算可得CF值:floor(原价/100) × pointsMultiplier
+          if (res.data.price && res.data.pointsMultiplier) {
+            that.cfEarnedValue = Math.floor(res.data.price / 100) * res.data.pointsMultiplier;
+          }
           // 赠品项初始化选择状态(响应式)
           var cfgList = res.data.giftConfigs
           if (cfgList && cfgList.length > 0) {
@@ -394,7 +384,6 @@ export default {
             '/pages/discover-detail/product-detail/product-detail?id=' + that.product.id
           )
           that.loadSkuData(id)
-          that.loadProductCoupons(parseInt(id))
         }
       }).catch(function() {
         that.loading = false
@@ -418,60 +407,10 @@ export default {
       })
     },
 
-    loadProductCoupons: function(productId) {
-      if (!productId) return
-      var that = this
-      // 计算可得CF值:floor(原价/100) × pointsMultiplier
-      if (that.product && that.product.price && that.product.pointsMultiplier) {
-        that.cfEarnedValue = Math.floor(that.product.price / 100) * that.product.pointsMultiplier;
-      }
-      // 未登录(如从分享链接进入)时跳过优惠券查询,避免 /api/coupon/product 返回 401
-      // 触发"登录已过期"并强制跳登录页;优惠券兑换是登录用户功能,未登录无需展示
-      if (!uni.getStorageSync('token')) return
-      getProductCoupons({ productId: productId }).then(function(res) {
-        if (res.code === 200) {
-          that.productCoupons = res.data || []
-        }
-      }).catch(function() {
-        // 兑换券加载失败不阻塞页面
-      })
-    },
-
-    getCouponKey: function(item, index) {
-      return item.id + '-' + index
-    },
     getBundleKey: function(item, idx) {
       return item ? (item.childProductId || idx) : idx
     },
 
-    getCouponDesc: function(item) {
-      if (item.type === 'DISCOUNT') {
-        return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
-      }
-      return '立减 ' + (item.value / 100).toFixed(2) + '元'
-    },
-
-    doExchangeCoupon: function(item) {
-      var that = this
-      uni.showModal({
-        title: '确认兑换',
-        content: '消耗 ' + item.pointsPrice + ' 积分兑换「' + item.name + '」?',
-        success: function(r) {
-          if (!r.confirm) return
-          exchangeCoupon({ couponId: item.id }).then(function(res) {
-            if (res.code === 200) {
-              uni.showToast({ title: '兑换成功', icon: 'success' })
-              that.loadProductCoupons(that.product.id)
-            } else {
-              uni.showToast({ title: res.message || '兑换失败', icon: 'none' })
-            }
-          }).catch(function(e) {
-            uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
-          })
-        }
-      })
-    },
-
     selectSpec: function(groupId, option) {
       if (this.selectedSpecs[groupId] === option.id) {
         this.$delete(this.selectedSpecs, groupId)

+ 20 - 6
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -254,12 +254,12 @@
               <text class="check-icon">✓</text>
             </view>
           </view>
-          <!-- 可兑换优惠券(需CF值) -->
-          <view class="picker-section-title" v-if="exchangeableCoupons.length > 0">
+          <!-- 可兑换优惠券(需CF值):仅在 CF 足够兑换其中一张券时才提醒 -->
+          <view class="picker-section-title" v-if="affordableExchangeCoupons.length > 0">
             可用CF值兑换(当前余额 {{ cfBalance }} CF)
           </view>
           <view
-            v-for="coupon in exchangeableCoupons"
+            v-for="coupon in affordableExchangeCoupons"
             :key="coupon.id"
             class="picker-item picker-item-exchange"
           >
@@ -278,7 +278,7 @@
               {{ coupon.exchanging ? '兑换中...' : '兑换' }}
             </button>
           </view>
-          <view class="picker-empty" v-if="ownedCoupons.length === 0 && exchangeableCoupons.length === 0">
+          <view class="picker-empty" v-if="ownedCoupons.length === 0 && affordableExchangeCoupons.length === 0">
             <text>暂无可用优惠券</text>
           </view>
         </scroll-view>
@@ -480,22 +480,36 @@ export default {
     },
     ownedCoupons() {
       var self = this
+      var currentProductId = (self.items && self.items.length > 0) ? self.items[0].productId : null
       return this.coupons.filter(function(c) {
         if (c.status && c.status !== 'AVAILABLE') return false
-        if (c.applicableTo && c.applicableTo !== 'ALL') return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
+        if (c.applicableTo === 'PRODUCT' && c.productId != null) {
+          return c.productId === currentProductId
+        }
+        if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'PRODUCT') return false
         return true
       })
     },
     availableCoupons() {
       var self = this
+      var currentProductId = (self.items && self.items.length > 0) ? self.items[0].productId : null
       return this.coupons.filter(function(c) {
         if (c.status && c.status !== 'AVAILABLE') return false
-        if (c.applicableTo && c.applicableTo !== 'ALL') return false
         if (c.minSpend && c.minSpend > self.actualAmount) return false
+        if (c.applicableTo === 'PRODUCT' && c.productId != null) {
+          return c.productId === currentProductId
+        }
+        if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'PRODUCT') return false
         return true
       })
     },
+    affordableExchangeCoupons() {
+      var self = this
+      return (this.exchangeableCoupons || []).filter(function(c) {
+        return c.pointsPrice > 0 && self.cfBalance >= c.pointsPrice
+      })
+    },
     onlineTimeSlots() {
       if (this.deliveryMethod === 4 && this.selectedServicePerson) {
         return this.selectedServicePerson.slots || []