Browse Source

fix: 优惠券有效时间校验加强,修复多处时间校验缺失问题

- 领券中心/可用券查询 SQL 层增加有效时间过滤,防止过期券展示
- useCoupon 增加 validStartTime 校验,防止未生效券被使用
- receiveCoupon/distributeCoupon 增加模板过期校验
- 管理后台优惠券表单增加有效期校验和相对时间(领取后N天)支持

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 tuần trước cách đây
mục cha
commit
7f17eb7c8d

+ 56 - 12
haha-admin-web/src/views/marketing/coupon/utils/hook.tsx

@@ -58,7 +58,9 @@ interface CouponFormData {
   shopIds: number[];
   deviceIds: number[];
   productIds: number[];
+  validType: number; // 1-固定时间,2-相对时间
   validPeriod: string[];
+  validDays: number | null;
   description: string;
   applyScope: number;
   deviceScope: number;
@@ -639,17 +641,36 @@ export function useCoupon() {
                     placeholder="发放总数量"
                   />
                 </ElFormItem>
-                <ElFormItem label="有效期" required>
-                  <ElDatePicker
-                    v-model={formData.validPeriod}
-                    type="daterange"
-                    range-separator="至"
-                    start-placeholder="开始日期"
-                    end-placeholder="结束日期"
-                    class="w-full!"
-                  />
+                <ElFormItem label="有效期类型" required>
+                  <ElRadioGroup v-model={formData.validType}>
+                    <ElRadioButton value={1}>固定时间</ElRadioButton>
+                    <ElRadioButton value={2}>领取后生效</ElRadioButton>
+                  </ElRadioGroup>
                 </ElFormItem>
               </div>
+              <div class="form-grid-2" style="margin-top: 0;">
+                {formData.validType === 1 ? (
+                  <ElFormItem label="有效期" required>
+                    <ElDatePicker
+                      v-model={formData.validPeriod}
+                      type="daterange"
+                      range-separator="至"
+                      start-placeholder="开始日期"
+                      end-placeholder="结束日期"
+                      class="w-full!"
+                    />
+                  </ElFormItem>
+                ) : (
+                  <ElFormItem label="有效天数" required>
+                    <ElInputNumber
+                      v-model={formData.validDays}
+                      min={1}
+                      class="w-full!"
+                      placeholder="领取后有效天数"
+                    />
+                  </ElFormItem>
+                )}
+              </div>
             </ElForm>
           )}
         </div>
@@ -725,6 +746,26 @@ export function useCoupon() {
       message("请输入发放总量", { type: "warning" });
       return false;
     }
+    if (!formData.validType) {
+      message("请选择有效期类型", { type: "warning" });
+      return false;
+    }
+    if (formData.validType === 1) {
+      if (!formData.validPeriod || formData.validPeriod.length !== 2) {
+        message("请选择有效期起止时间", { type: "warning" });
+        return false;
+      }
+      const [start, end] = formData.validPeriod;
+      if (start && end && new Date(start) >= new Date(end)) {
+        message("有效期开始时间不能晚于结束时间", { type: "warning" });
+        return false;
+      }
+    } else if (formData.validType === 2) {
+      if (!formData.validDays || formData.validDays <= 0) {
+        message("请输入有效天数", { type: "warning" });
+        return false;
+      }
+    }
     return true;
   }
 
@@ -739,7 +780,9 @@ export function useCoupon() {
       shopIds: [],
       deviceIds: [],
       productIds: [],
+      validType: 1,
       validPeriod: [],
+      validDays: null,
       description: "",
       applyScope: 1,
       deviceScope: 1,
@@ -762,9 +805,10 @@ export function useCoupon() {
             receiveType: formData.receiveType,
             discountValue: formData.value,
             totalCount: formData.totalCount,
-            validType: 1, // 固定时间
-            validStartTime: formData.validPeriod?.[0],
-            validEndTime: formData.validPeriod?.[1],
+            validType: formData.validType,
+            validStartTime: formData.validType === 1 ? formData.validPeriod?.[0] : null,
+            validEndTime: formData.validType === 1 ? formData.validPeriod?.[1] : null,
+            validDays: formData.validType === 2 ? formData.validDays : null,
             applyScope: formData.applyScope,
             productScope: formData.productScope,
             activityId: formData.activityId ? Number(formData.activityId) : null,

+ 1 - 1
haha-mapper/src/main/java/com/haha/mapper/CouponTemplateMapper.java

@@ -15,7 +15,7 @@ public interface CouponTemplateMapper extends BaseMapper<CouponTemplate> {
     @Select("SELECT * FROM t_coupon_template WHERE deleted = 0 ORDER BY create_time DESC")
     List<CouponTemplate> selectAllTemplates();
 
-    @Select("SELECT * FROM t_coupon_template WHERE status = 1 AND deleted = 0 AND remain_count > 0 AND receive_type = 'Collect' ORDER BY create_time DESC")
+    @Select("SELECT * FROM t_coupon_template WHERE status = 1 AND deleted = 0 AND remain_count > 0 AND receive_type = 'Collect' AND (valid_type = 2 OR (valid_type = 1 AND valid_end_time >= NOW())) ORDER BY create_time DESC")
     List<CouponTemplate> selectAvailableTemplates();
 
     @Update("UPDATE t_coupon_template SET remain_count = remain_count - 1, update_time = NOW() WHERE id = #{id} AND remain_count > 0")

+ 1 - 1
haha-mapper/src/main/java/com/haha/mapper/UserCouponMapper.java

@@ -16,7 +16,7 @@ public interface UserCouponMapper extends BaseMapper<UserCoupon> {
     @Select("SELECT uc.*, ct.coupon_name, ct.coupon_type, ct.min_amount " +
             "FROM t_user_coupon uc " +
             "LEFT JOIN t_coupon_template ct ON uc.template_id = ct.id " +
-            "WHERE uc.user_id = #{userId} AND uc.status = #{status} " +
+            "WHERE uc.user_id = #{userId} AND uc.status = #{status} AND uc.valid_end_time >= NOW() " +
             "ORDER BY uc.valid_end_time ASC")
     List<UserCoupon> selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") Integer status);
 

+ 18 - 2
haha-service/src/main/java/com/haha/service/impl/UserCouponServiceImpl.java

@@ -106,6 +106,13 @@ public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCou
                 throw new BusinessException(400, "优惠券已领完");
             }
 
+            if (template.getValidType() == ValidTypeEnum.FIXED_TIME.getCode()
+                    && template.getValidEndTime() != null
+                    && LocalDateTime.now().isAfter(template.getValidEndTime())) {
+                log.warn("[优惠券服务] 领取优惠券失败 - 优惠券已过期, templateId: {}, validEndTime: {}", templateId, template.getValidEndTime());
+                throw new BusinessException(400, "优惠券已过期");
+            }
+
             int receivedCount = userCouponMapper.countByTemplateAndUser(templateId, userId);
             if (receivedCount >= template.getReceiveLimit()) {
                 log.warn("[优惠券服务] 领取优惠券失败 - 超过领取上限, userId: {}, templateId: {}, receivedCount: {}, limit: {}", 
@@ -157,11 +164,17 @@ public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCou
                 dto.getTemplateId(), template != null ? "存在" : "不存在");
         
         if (template == null || template.getDeleted() == CommonConstants.DELETED) {
-            log.error("[优惠券发放] 模板不存在或已删除 - templateId={}, deleted={}", 
+            log.error("[优惠券发放] 模板不存在或已删除 - templateId={}, deleted={}",
                     dto.getTemplateId(), template != null ? template.getDeleted() : "N/A");
             throw new BusinessException(404, "优惠券模板不存在");
         }
 
+        if (template.getValidType() == ValidTypeEnum.FIXED_TIME.getCode()
+                && template.getValidEndTime() != null
+                && LocalDateTime.now().isAfter(template.getValidEndTime())) {
+            throw new BusinessException(400, "优惠券已过期,无法发放");
+        }
+
         List<Long> targetUserIds = getTargetUserIds(dto.getTargetType(), dto.getUserIds());
         if (targetUserIds.isEmpty()) {
             throw new BusinessException(400, "没有目标用户");
@@ -269,7 +282,10 @@ public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCou
             throw new BusinessException(400, "优惠券不可用");
         }
         
-        // 4. 检查优惠券是否过期
+        // 4. 检查优惠券是否在有效期内
+        if (LocalDateTime.now().isBefore(userCoupon.getValidStartTime())) {
+            throw new BusinessException(400, "优惠券尚未生效");
+        }
         if (LocalDateTime.now().isAfter(userCoupon.getValidEndTime())) {
             throw new BusinessException(400, "优惠券已过期");
         }