skyline 1 miesiąc temu
rodzic
commit
67d2cec025

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/CouponTemplate.java

@@ -80,4 +80,7 @@ public class CouponTemplate implements Serializable {
 
     @TableField(exist = false)
     private String statusColor;
+
+    @TableField(exist = false)
+    private Boolean alreadyReceived;
 }

+ 9 - 1
haha-miniapp/src/main/java/com/haha/miniapp/controller/AppCouponController.java

@@ -27,8 +27,16 @@ public class AppCouponController {
 
     @GetMapping("/available")
     public Result<List<CouponTemplate>> getAvailableCoupons() {
-        log.info("[优惠券] 查询可领取优惠券列表");
+        Long userId = StpUtil.getLoginIdAsLong();
+        log.info("[优惠券] 查询可领取优惠券列表 - userId: {}", userId);
         List<CouponTemplate> templates = templateService.getAvailableTemplates();
+        
+        // 填充用户已领取状态
+        templates.forEach(template -> {
+            int receivedCount = userCouponService.countReceivedByUser(userId, template.getId());
+            template.setAlreadyReceived(receivedCount > 0);
+        });
+        
         log.info("[优惠券] 可领取优惠券数量: {}", templates.size());
         return Result.success("查询成功", templates);
     }

+ 130 - 56
haha-mp/src/pages/couponCenter/couponCenter.vue

@@ -7,55 +7,76 @@
     </view>
 
     <!-- 优惠券列表 -->
-    <view v-if="!loading && templates.length > 0" class="coupon-list">
-      <view
-        v-for="item in templates"
-        :key="item.id"
-        class="ticket"
-      >
-        <!-- 左侧:金额 -->
-        <view :class="['ticket-face', item.couponType ? 'ticket-face--type' + item.couponType : 'ticket-face--type1']">
-          <view class="ticket-amount">
-            <text class="ticket-currency">{{ item.couponType === 2 ? '' : '¥' }}</text>
-            <text class="ticket-figure">{{ formatDiscountValue(item) }}</text>
+    <scroll-view
+      v-if="!loading && templates.length > 0"
+      class="coupon-scroll"
+      scroll-y
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
+    >
+      <view class="coupon-list">
+        <view
+          v-for="item in templates"
+          :key="item.id"
+          class="ticket"
+        >
+          <!-- 左侧:金额 -->
+          <view :class="['ticket-face', item.couponType ? 'ticket-face--type' + item.couponType : 'ticket-face--type1']">
+            <view class="ticket-amount">
+              <text class="ticket-currency">{{ item.couponType === 2 ? '' : '¥' }}</text>
+              <text class="ticket-figure">{{ formatDiscountValue(item) }}</text>
+            </view>
+            <text class="ticket-threshold">
+              {{ item.minAmount && item.minAmount > 0 ? '满' + item.minAmount + '可用' : '无门槛' }}
+            </text>
           </view>
-          <text class="ticket-threshold">
-            {{ item.minAmount && item.minAmount > 0 ? '满' + item.minAmount + '可用' : '无门槛' }}
-          </text>
-        </view>
-
-        <!-- 锯齿分割 -->
-        <view class="ticket-serration">
-          <view class="serration-gap serration-gap--top"></view>
-          <view class="serration-rail"></view>
-          <view class="serration-gap serration-gap--bottom"></view>
-        </view>
 
-        <!-- 右侧:信息 -->
-        <view class="ticket-body">
-          <text class="ticket-title">{{ item.couponName || '优惠券' }}</text>
-          <view class="ticket-meta">
-            <view class="ticket-badge">{{ getCouponTypeLabel(item.couponType) }}</view>
-            <text class="ticket-scope">{{ getScopeLabel(item.applyScope) }}</text>
+          <!-- 锯齿分割 -->
+          <view class="ticket-serration">
+            <view class="serration-gap serration-gap--top"></view>
+            <view class="serration-rail"></view>
+            <view class="serration-gap serration-gap--bottom"></view>
           </view>
-          <text v-if="item.couponDesc" class="ticket-desc">{{ item.couponDesc }}</text>
-          <text class="ticket-period">
-            <text v-if="item.validType === 1">{{ formatTime(item.validStartTime) }} ~ {{ formatTime(item.validEndTime) }}</text>
-            <text v-else>领取后{{ item.validDays }}天内有效</text>
-          </text>
-
-          <!-- 领取按钮 -->
-          <view
-            :class="['ticket-grab', canReceive(item) ? '' : 'ticket-grab--out']"
-            @click="handleReceive(item)"
-          >
-            <text class="ticket-grab-text">
-              {{ receiving[item.id] ? '...' : (canReceive(item) ? '领取' : '已抢光') }}
+
+          <!-- 右侧:信息 -->
+          <view class="ticket-body">
+            <text class="ticket-title">{{ item.couponName || '优惠券' }}</text>
+            <view class="ticket-meta">
+              <view class="ticket-badge">{{ getCouponTypeLabel(item.couponType) }}</view>
+              <text class="ticket-scope">{{ getScopeLabel(item.applyScope) }}</text>
+            </view>
+            <text v-if="item.couponDesc" class="ticket-desc">{{ item.couponDesc }}</text>
+            
+            <!-- 库存和领取信息 -->
+            <view class="ticket-stock-row">
+              <text class="ticket-stock">
+                剩余 <text class="stock-num">{{ item.remainCount }}</text> / {{ item.totalCount }}
+              </text>
+              <text v-if="item.receiveLimit > 0" class="ticket-limit">
+                每人限领{{ item.receiveLimit }}张
+              </text>
+            </view>
+            
+            <!-- 有效期 -->
+            <text class="ticket-period">
+              <text v-if="item.validType === 1">{{ formatTime(item.validStartTime) }} ~ {{ formatTime(item.validEndTime) }}</text>
+              <text v-else>领取后{{ item.validDays }}天内有效</text>
             </text>
+
+            <!-- 领取按钮 -->
+            <view
+              :class="['ticket-grab', canReceive(item) ? '' : (item.alreadyReceived ? 'ticket-grab--received' : 'ticket-grab--out')]"
+              @click="handleReceive(item)"
+            >
+              <text class="ticket-grab-text">
+                {{ receiving[item.id] ? '领取中...' : (item.alreadyReceived ? '已领取' : (canReceive(item) ? '立即领取' : '已抢光')) }}
+              </text>
+            </view>
           </view>
         </view>
       </view>
-    </view>
+    </scroll-view>
 
     <!-- 空状态 -->
     <view v-if="!loading && templates.length === 0" class="empty">
@@ -74,6 +95,7 @@
         </view>
       </view>
       <text class="empty-title">暂无可领取的优惠券</text>
+      <text class="empty-desc">过段时间再来看看吧</text>
       <view class="empty-cta" @click="goToMyCoupons">
         <text class="empty-cta-text">查看我的优惠券</text>
       </view>
@@ -93,16 +115,22 @@ import { getAvailableCoupons, receiveCoupon } from '../../api/coupon';
 import type { CouponTemplateInfo } from '../../api/coupon';
 import { checkAuth } from '../../utils/auth';
 
-const templates = ref<CouponTemplateInfo[]>([]);
+// 扩展接口,添加前端需要的字段
+interface CouponTemplateInfoExtended extends CouponTemplateInfo {
+  alreadyReceived?: boolean;
+}
+
+const templates = ref<CouponTemplateInfoExtended[]>([]);
 const loading = ref(false);
+const refreshing = ref(false);
 const receiving = reactive<Record<string, boolean>>({});
 
 /**
  * 格式化优惠值
  */
-const formatDiscountValue = (item: CouponTemplateInfo): string => {
+const formatDiscountValue = (item: CouponTemplateInfoExtended): string => {
   if (item.couponType === 2) {
-    return item.discountValue ? `${item.discountValue}折` : '';
+    return item.discountValue ? `${item.discountValue}折` : '0';
   }
   return item.discountValue ? `${item.discountValue}` : '0';
 };
@@ -143,14 +171,14 @@ const formatTime = (timeStr: string): string => {
 /**
  * 判断是否可领取
  */
-const canReceive = (item: CouponTemplateInfo): boolean => {
-  return item.remainCount > 0 && item.status === 1;
+const canReceive = (item: CouponTemplateInfoExtended): boolean => {
+  return item.remainCount > 0 && item.status === 1 && !item.alreadyReceived;
 };
 
 /**
  * 领取优惠券
  */
-const handleReceive = async (item: CouponTemplateInfo) => {
+const handleReceive = async (item: CouponTemplateInfoExtended) => {
   if (!canReceive(item) || receiving[item.id]) return;
 
   receiving[item.id] = true;
@@ -162,6 +190,7 @@ const handleReceive = async (item: CouponTemplateInfo) => {
     });
     // 更新剩余数量
     item.remainCount = Math.max(0, item.remainCount - 1);
+    item.alreadyReceived = true;
   } catch (error: any) {
     console.error('领取优惠券失败:', error);
     // 错误信息已在 request 工具中展示
@@ -170,6 +199,18 @@ const handleReceive = async (item: CouponTemplateInfo) => {
   }
 };
 
+/**
+ * 下拉刷新
+ */
+const onRefresh = async () => {
+  refreshing.value = true;
+  try {
+    await loadTemplates();
+  } finally {
+    refreshing.value = false;
+  }
+};
+
 /**
  * 跳转到我的优惠券
  */
@@ -183,16 +224,14 @@ const goToMyCoupons = () => {
  * 加载可领取优惠券列表
  */
 const loadTemplates = async () => {
-  if (loading.value) return;
-  loading.value = true;
-
   try {
     const result = await getAvailableCoupons();
-    templates.value = result;
+    templates.value = result.map(item => ({
+      ...item,
+      alreadyReceived: false // 初始化为未领取
+    }));
   } catch (error) {
     console.error('加载可领取优惠券失败:', error);
-  } finally {
-    loading.value = false;
   }
 };
 
@@ -210,6 +249,10 @@ onMounted(() => {
   background: linear-gradient(180deg, #FFF1F0 0%, #F5F5F5 30%);
 }
 
+.coupon-scroll {
+  height: calc(100vh - 140rpx);
+}
+
 /* ========== Header ========== */
 .header {
   padding: 28rpx 36rpx 16rpx;
@@ -231,7 +274,7 @@ onMounted(() => {
 
 /* ========== Coupon List ========== */
 .coupon-list {
-  padding: 8rpx 28rpx 0;
+  padding: 8rpx 28rpx 40rpx;
 }
 
 /* ========== Ticket Card ========== */
@@ -397,10 +440,27 @@ onMounted(() => {
   white-space: nowrap;
 }
 
+/* 库存行 */
+.ticket-stock-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10rpx;
+}
+
 .ticket-stock {
   font-size: 20rpx;
+  color: #999999;
+}
+
+.stock-num {
   color: #E8533E;
-  font-weight: 500;
+  font-weight: 600;
+}
+
+.ticket-limit {
+  font-size: 20rpx;
+  color: #BBBBBB;
 }
 
 .ticket-period {
@@ -432,6 +492,14 @@ onMounted(() => {
   color: #CCCCCC;
 }
 
+.ticket-grab--received {
+  background: #F0F0F0;
+}
+
+.ticket-grab--received .ticket-grab-text {
+  color: #999999;
+}
+
 /* ========== Empty State ========== */
 .empty {
   display: flex;
@@ -526,6 +594,12 @@ onMounted(() => {
   font-size: 30rpx;
   color: #999999;
   font-weight: 600;
+  margin-bottom: 12rpx;
+}
+
+.empty-desc {
+  font-size: 24rpx;
+  color: #BBBBBB;
   margin-bottom: 36rpx;
 }
 

+ 2 - 0
haha-service/src/main/java/com/haha/service/UserCouponService.java

@@ -30,4 +30,6 @@ public interface UserCouponService extends IService<UserCoupon> {
     Map<String, Object> getCouponStatistics(Long templateId);
 
     IPage<UserCoupon> getUserCouponList(Long userId, String couponCode, Integer status, int page, int pageSize);
+
+    int countReceivedByUser(Long userId, Long templateId);
 }

+ 9 - 0
haha-service/src/main/java/com/haha/service/impl/UserCouponServiceImpl.java

@@ -341,4 +341,13 @@ public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCou
 
         return result;
     }
+
+    @Override
+    public int countReceivedByUser(Long userId, Long templateId) {
+        log.debug("[优惠券服务] 查询用户已领取优惠券数量 - userId: {}, templateId: {}", userId, templateId);
+        LambdaQueryWrapper<UserCoupon> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserCoupon::getUserId, userId)
+               .eq(UserCoupon::getTemplateId, templateId);
+        return Math.toIntExact(this.count(wrapper));
+    }
 }