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

fix(coupon): 心愿券单券日限次生效 + 结算页改用服务端抵扣

1) PointsExchangeService.exchangeCoupon 单券日限次统计源由遗留死表 coupon_grant_log 改为实际发券写入的 family_coupon_grant_log(family_id + coupon_id + grant_type=EXCHANGE + 当日),使 coupon_exchange_daily_limit 生效;全局 exchange_daily_limit 逻辑不变。\n2) 结算页选券后调用 /api/coupon/apply 由服务端计算抵扣额,前端展示与下单落库一致,不再用 selectedCoupon.value 本地减额。\n3) coupons.vue「可兑换」Tab 更名为「CF值兑换」,明确其为 CF 值兑换;心愿券仅指孩子个人积分兑换,CF 值兑换券不称心愿券。
zxyj-web 2 недель назад
Родитель
Сommit
8f33bc2ea1

+ 17 - 13
cfc-backend/src/main/java/com/etotem/cfc/service/PointsExchangeService.java

@@ -3,11 +3,11 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.Coupon;
-import com.etotem.cfc.entity.CouponGrantLog;
+import com.etotem.cfc.entity.FamilyCouponGrantLog;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsExchangeProduct;
 import com.etotem.cfc.entity.PointsExchangeRecord;
-import com.etotem.cfc.mapper.CouponGrantLogMapper;
+import com.etotem.cfc.mapper.FamilyCouponGrantLogMapper;
 import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsExchangeProductMapper;
@@ -47,7 +47,7 @@ public class PointsExchangeService {
     private CouponService couponService;
 
     @Resource
-    private CouponGrantLogMapper couponGrantLogMapper;
+    private FamilyCouponGrantLogMapper familyCouponGrantLogMapper;
 
     public Page<PointsExchangeProduct> getProductList(String category, Integer pageNum, Integer pageSize) {
         Page<PointsExchangeProduct> pageParam = new Page<>(pageNum, pageSize);
@@ -234,6 +234,12 @@ public class PointsExchangeService {
             throw new RuntimeException("今日兑换次数已达上限");
         }
 
+        // 目标家庭(单券日限次按家庭统计,发券也落到该家庭)
+        Long targetFamilyId = null;
+        FamilyMember targetMember = familyMemberMapper.selectById(targetFamilyMemberId);
+        if (targetMember != null) targetFamilyId = targetMember.getFamilyId();
+        if (targetFamilyId == null) throw new RuntimeException("未找到目标成员所在家庭");
+
         // 单券模板日限次(coupon_exchange_daily_limit 默认1)
         int couponDailyLimit = 1;
         try {
@@ -244,12 +250,14 @@ public class PointsExchangeService {
         } catch (Exception e) {
             couponDailyLimit = 1;
         }
-        long couponTodayCount = couponGrantLogMapper.selectCount(
-            new LambdaQueryWrapper<CouponGrantLog>()
-                .eq(CouponGrantLog::getUserId, userId)
-                .eq(CouponGrantLog::getCouponId, couponId)
-                .eq(CouponGrantLog::getGrantType, "EXCHANGE")
-                .ge(CouponGrantLog::getCreatedAt, todayStart)
+        // 统计源必须是实际发券写入的 family_coupon_grant_log(EXCHANGE 由 grantFamily 落库),
+        // 原 coupon_grant_log 为遗留死表,永远查不到记录导致日限次失效。
+        long couponTodayCount = familyCouponGrantLogMapper.selectCount(
+            new LambdaQueryWrapper<FamilyCouponGrantLog>()
+                .eq(FamilyCouponGrantLog::getFamilyId, targetFamilyId)
+                .eq(FamilyCouponGrantLog::getCouponId, couponId)
+                .eq(FamilyCouponGrantLog::getGrantType, "EXCHANGE")
+                .ge(FamilyCouponGrantLog::getCreatedAt, todayStart)
         );
         if (couponTodayCount >= couponDailyLimit) {
             throw new RuntimeException("该优惠券今日兑换次数已达上限");
@@ -276,10 +284,6 @@ public class PointsExchangeService {
         }
 
         // 发券 + 流水(EXCHANGE,period=null)— 发到用户所在家庭
-        Long targetFamilyId = null;
-        FamilyMember targetMember = familyMemberMapper.selectById(targetFamilyMemberId);
-        if (targetMember != null) targetFamilyId = targetMember.getFamilyId();
-        if (targetFamilyId == null) throw new RuntimeException("未找到目标成员所在家庭");
         couponService.grantFamily(targetFamilyId, couponId, "EXCHANGE", null, null);
 
         String redeemCode = UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();

+ 1 - 1
cfc-frontend/pages/profile-extra/coupons.vue

@@ -101,7 +101,7 @@ export default {
         { name: '可使用', status: 'AVAILABLE' },
         { name: '已使用', status: 'USED' },
         { name: '已过期', status: 'EXPIRED' },
-        { name: '可兑换' }
+        { name: 'CF值兑换' }
       ],
       tabIndex: 0,
       coupons: [],

+ 38 - 8
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -243,7 +243,7 @@
         <view class="info-row">
           <text class="info-label">优惠券</text>
           <view class="coupon-selected" v-if="selectedCoupon">
-            <text class="coupon-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }}</text>
+            <text class="coupon-discount">-{{ formatPriceWithSymbol(couponDiscount) }}</text>
             <text class="coupon-name-tag">{{ selectedCoupon.name }}</text>
           </view>
           <view class="coupon-placeholder" v-else>
@@ -484,7 +484,7 @@
 <script>
 import { BrandColors } from '@/utils/brand-colors.js'
 import config from '@/config.js'
-import { getCouponList, getCouponCheckoutList, addressList, getProductRequiredFields, getMyMembership, getGiftMembershipProgress, productDetail, getFamilyPlatformBalance, exchangeCouponByCf, shopDiscountCalculate, getSpecialPriceInfo } from '../../../utils/api.js'
+import { getCouponList, getCouponCheckoutList, addressList, getProductRequiredFields, getMyMembership, getGiftMembershipProgress, productDetail, getFamilyPlatformBalance, exchangeCouponByCf, shopDiscountCalculate, getSpecialPriceInfo, applyCoupon } from '../../../utils/api.js'
 import { parseDate } from '@/utils/format.js'
 import CountdownTag from '@/pages/shop/components/countdown-tag.vue'
 import HandSignature from '@/components/hand-signature/hand-signature.vue'
@@ -513,6 +513,8 @@ export default {
       exchangeableCoupons: [],
       cfBalance: 0,
       selectedCoupon: null,
+      couponDiscount: 0,        // 服务端 /api/coupon/apply 返回的抵扣金额(分),前端只展示
+      couponApplying: false,
       showCouponPicker: false,
       pointsUsed: 0,
       userTotalPoints: 0,
@@ -592,9 +594,7 @@ export default {
     },
     finalAmount() {
       var total = (this.actualAmount || 0) + (this.freight || 0)
-      if (this.selectedCoupon) {
-        total = total - this.selectedCoupon.value
-      }
+      total = total - (this.couponDiscount || 0)
       total = total - (this.pointsUsed || 0)
       total = total - this.giftPromoDiscount
       if (total < 0) total = 0
@@ -602,9 +602,7 @@ export default {
     },
     maxPoints() {
       var total = (this.actualAmount || 0) + (this.freight || 0)
-      if (this.selectedCoupon) {
-        total = total - this.selectedCoupon.value
-      }
+      total = total - (this.couponDiscount || 0)
       if (total < 0) total = 0
       if (this.userTotalPoints < total) {
         return this.userTotalPoints
@@ -789,11 +787,43 @@ export default {
     selectCoupon(coupon) {
       this.selectedCoupon = coupon
       this.showCouponPicker = false
+      this.applyCouponDiscount()
     },
     removeCoupon() {
       this.selectedCoupon = null
+      this.couponDiscount = 0
       this.showCouponPicker = false
     },
+    // 优惠券抵扣统一由服务端计算(券类型/满减门槛/会员范围/折扣千分比),
+    // 前端不再用 selectedCoupon.value 本地减额,保证展示与下单落库一致。
+    async applyCouponDiscount() {
+      if (!this.selectedCoupon) {
+        this.couponDiscount = 0
+        return
+      }
+      var applyingCouponId = this.selectedCoupon.id
+      this.couponApplying = true
+      try {
+        var res = await applyCoupon({
+          userCouponId: this.selectedCoupon.id,
+          orderType: 'PRODUCT',
+          orderAmount: this.actualAmount || 0
+        })
+        // 期间用户可能已改选/取消券,丢弃过期响应
+        if (!this.selectedCoupon || this.selectedCoupon.id !== applyingCouponId) {
+          return
+        }
+        this.couponDiscount = (res && res.data && res.data.discount) || 0
+      } catch (e) {
+        if (this.selectedCoupon && this.selectedCoupon.id === applyingCouponId) {
+          // 服务端判定不可用(request 已提示 message),本地同步清空避免错误展示
+          this.selectedCoupon = null
+          this.couponDiscount = 0
+        }
+      } finally {
+        this.couponApplying = false
+      }
+    },
     onPointsInput(e) {
       var val = parseInt(e.detail.value) || 0
       if (val < 0) val = 0

+ 5 - 0
cfc-frontend/utils/api.js

@@ -2625,6 +2625,11 @@ export const getCouponCheckoutList = (data) => {
   return request('/api/coupon/checkout-list', 'POST', data)
 }
 
+// 结算页优惠券抵扣试算:由服务端按券类型/门槛/会员范围计算,前端只做展示
+export const applyCoupon = (data) => {
+  return request('/api/coupon/apply', 'POST', data)
+}
+
 export const exchangeCoupon = (data) => {
   return request('/api/coupon/exchange', 'POST', data)
 }