Bläddra i källkod

@ApiOperation(value = "查询用户拥有的优惠券", notes = "需登录。含商家逻辑删除模板(仍展示券属性);每张用户券一行。可选 storeId。金额 amount 仅影响排序不参与过滤(建议传订单应付金额);未传金额时按面值等规则排序。") 维护接口

lutong 17 timmar sedan
förälder
incheckning
5434fcf7a1

+ 1 - 1
alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java

@@ -113,7 +113,7 @@ public class DiningCouponController {
     /**
      * 查询用户拥有的优惠券(按符合支付条件优先,其次优惠力度大的优先)
      */
-    @ApiOperation(value = "查询用户拥有的优惠券", notes = "需登录。查询用户拥有的优惠券,排序:符合支付条件的优先,其次优惠力度大的优先")
+    @ApiOperation(value = "查询用户拥有的优惠券", notes = "需登录。含商家逻辑删除模板(仍展示券属性);每张用户券一行。可选 storeId。金额 amount 仅影响排序不参与过滤(建议传订单应付金额);未传金额时按面值等规则排序。")
     @GetMapping("/userOwned")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "门店ID(可选,如果提供则只查询该门店的优惠券)", dataType = "String", paramType = "query", required = false),

+ 3 - 2
alien-dining/src/main/java/shop/alien/dining/service/DiningCouponService.java

@@ -54,10 +54,11 @@ public interface DiningCouponService {
     R<Map<String, Object>> getStoreUserUsableCouponList(String authorization, String storeId, BigDecimal amount, Integer couponType);
 
     /**
-     * 查询用户拥有的优惠券(按符合支付条件优先,其次优惠力度大的优先)
+     * 查询用户拥有的优惠券:每张用户券一条;未过期待使用;可选按门店过滤。
+     * 传入的 {@code amount} <strong>只影响排序</strong>(无门槛券需传入 {@code amount &gt; 0} 才参与「可满足」的比较;满足的优先,同等再比估算优惠)。
      *
      * @param storeId 门店ID(可选,如果提供则只查询该门店的优惠券)
-     * @param amount  当前消费金额(用于判断是否符合支付条件)
+     * @param amount  可选;当前订单/消费金额参考值,用于排序对比门槛与折算优惠金额,不改变返回条数
      * @return 优惠券列表
      */
     R<List<LifeDiscountCouponVo>> getUserOwnedCoupons(String storeId, BigDecimal amount);

+ 127 - 80
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java

@@ -19,8 +19,10 @@ import shop.alien.mapper.LifeDiscountCouponUserMapper;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -121,6 +123,8 @@ public class DiningCouponServiceImpl implements DiningCouponService {
             userWrapper.and(w -> w.isNull(LifeDiscountCouponUser::getExpirationTime)
                     .or()
                     .ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()));
+            userWrapper.orderByDesc(LifeDiscountCouponUser::getReceiveTime);
+            userWrapper.orderByDesc(LifeDiscountCouponUser::getId);
             List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
 
             if (userCoupons == null || userCoupons.isEmpty()) {
@@ -128,107 +132,53 @@ public class DiningCouponServiceImpl implements DiningCouponService {
                 return R.data(new ArrayList<>());
             }
 
-            // 获取优惠券ID列表
             List<Integer> couponIds = userCoupons.stream()
                     .map(LifeDiscountCouponUser::getCouponId)
+                    .filter(Objects::nonNull)
+                    .distinct()
                     .collect(Collectors.toList());
+            if (couponIds.isEmpty()) {
+                return R.data(new ArrayList<>());
+            }
 
-            // 查询优惠券详情
-            LambdaQueryWrapper<LifeDiscountCoupon> couponWrapper = new LambdaQueryWrapper<>();
-            couponWrapper.in(LifeDiscountCoupon::getId, couponIds);
-            couponWrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
+            // 模板含逻辑删除:用户已领的券在商家删除模板后仍要能展示属性(与 store 端 includeDeleted 一致)
+            List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectByIdsIncludeDeleted(couponIds);
+            if (coupons == null) {
+                coupons = Collections.emptyList();
+            }
             if (StringUtils.hasText(storeId)) {
-                couponWrapper.eq(LifeDiscountCoupon::getStoreId, storeId);
+                String sid = storeId.trim();
+                coupons = coupons.stream()
+                        .filter(c -> c != null && sid.equals(c.getStoreId()))
+                        .collect(Collectors.toList());
             }
-            List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(couponWrapper);
 
-            if (coupons == null || coupons.isEmpty()) {
+            if (coupons.isEmpty()) {
                 log.info("未找到优惠券详情, userId={}, couponIds={}", userId, couponIds);
                 return R.data(new ArrayList<>());
             }
 
-            // 转换为VO并设置用户券ID,同时过滤掉已过期和未在使用时间内的优惠券
+            Map<Integer, LifeDiscountCoupon> templateById = coupons.stream()
+                    .collect(Collectors.toMap(LifeDiscountCoupon::getId, c -> c, (a, b) -> a));
+
             LocalDate now = LocalDate.now();
             List<LifeDiscountCouponVo> couponVos = new ArrayList<>();
-            for (LifeDiscountCoupon coupon : coupons) {
-                // 找到对应的用户券
-                LifeDiscountCouponUser userCoupon = userCoupons.stream()
-                        .filter(uc -> uc.getCouponId().equals(coupon.getId()))
-                        .findFirst()
-                        .orElse(null);
-
-                if (userCoupon == null) {
+            for (LifeDiscountCouponUser userCoupon : userCoupons) {
+                Integer cid = userCoupon.getCouponId();
+                if (cid == null) {
                     continue;
                 }
-
-                // 过滤1:检查用户券的过期时间(expirationTime)
-                if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
-                    log.debug("过滤已过期的用户券, couponId={}, expirationTime={}, now={}", 
-                            coupon.getId(), userCoupon.getExpirationTime(), now);
+                LifeDiscountCoupon coupon = templateById.get(cid);
+                if (coupon == null) {
                     continue;
                 }
-
-                // 过滤2:检查优惠券的使用时间范围(startDate 和 endDate)
-                // 如果优惠券有设置使用时间范围,则检查当前日期是否在范围内
-                if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
-                    boolean inTimeRange = true;
-                    if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
-                        // 当前日期早于开始日期,未在使用时间内
-                        inTimeRange = false;
-                    }
-                    if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
-                        // 当前日期晚于结束日期,未在使用时间内
-                        inTimeRange = false;
-                    }
-                    if (!inTimeRange) {
-                        log.debug("过滤未在使用时间内的优惠券, couponId={}, startDate={}, endDate={}, now={}", 
-                                coupon.getId(), coupon.getStartDate(), coupon.getEndDate(), now);
-                        continue;
-                    }
-                }
-
-                // 过滤3:检查优惠券的有效期(validDate)
-                if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
-                    log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}", 
-                            coupon.getId(), coupon.getValidDate(), now);
+                if (!passesOwnedCouponUsableFilters(coupon, userCoupon, now)) {
                     continue;
                 }
-
-                LifeDiscountCouponVo vo = convertToVo(coupon, userCoupon);
-                couponVos.add(vo);
+                couponVos.add(convertToVo(coupon, userCoupon));
             }
 
-            // 排序:符合支付条件的优先,其次优惠力度大的优先
-            if (amount != null) {
-                couponVos.sort((vo1, vo2) -> {
-                    // 判断是否符合支付条件(满足最低消费要求)
-                    boolean vo1CanUse = vo1.getMinimumSpendingAmount() == null
-                            || vo1.getMinimumSpendingAmount().compareTo(amount) <= 0;
-                    boolean vo2CanUse = vo2.getMinimumSpendingAmount() == null
-                            || vo2.getMinimumSpendingAmount().compareTo(amount) <= 0;
-
-                    // 第一优先级:符合支付条件的优先
-                    if (vo1CanUse && !vo2CanUse) {
-                        return -1; // vo1 排在前面
-                    }
-                    if (!vo1CanUse && vo2CanUse) {
-                        return 1; // vo2 排在前面
-                    }
-
-                    // 第二优先级:优惠力度大的优先(根据优惠券类型计算实际优惠金额)
-                    BigDecimal discountAmount1 = calculateDiscountAmountForVO(vo1, amount);
-                    BigDecimal discountAmount2 = calculateDiscountAmountForVO(vo2, amount);
-                    return discountAmount2.compareTo(discountAmount1); // 降序排列
-                });
-            } else {
-                // 如果没有提供金额,无法计算折扣券的实际优惠金额,只按面值排序(满减券)
-                // 注意:这种情况下折扣券无法准确排序,建议前端传入订单金额
-                couponVos.sort((vo1, vo2) -> {
-                    BigDecimal nominalValue1 = vo1.getNominalValue() != null ? vo1.getNominalValue() : BigDecimal.ZERO;
-                    BigDecimal nominalValue2 = vo2.getNominalValue() != null ? vo2.getNominalValue() : BigDecimal.ZERO;
-                    return nominalValue2.compareTo(nominalValue1); // 降序排列
-                });
-            }
+            sortOwnedCouponsForCheckout(couponVos, amount); // amount 仅参与排序,不参与过滤
 
             log.info("查询用户拥有的优惠券成功, userId={}, count={}", userId, couponVos.size());
             return R.data(couponVos);
@@ -401,6 +351,103 @@ public class DiningCouponServiceImpl implements DiningCouponService {
     }
 
     /**
+     * /userOwned:排除已过期(用户券有效期、模板可使用日期、模板 validDate)。
+     */
+    private boolean passesOwnedCouponUsableFilters(LifeDiscountCoupon coupon, LifeDiscountCouponUser userCoupon, LocalDate now) {
+        if (userCoupon.getExpirationTime() != null && now.isAfter(userCoupon.getExpirationTime())) {
+            log.debug("过滤已过期的用户券, userCouponId={}, couponId={}, expirationTime={}, now={}",
+                    userCoupon.getId(), coupon.getId(), userCoupon.getExpirationTime(), now);
+            return false;
+        }
+
+        if (coupon.getStartDate() != null || coupon.getEndDate() != null) {
+            if (coupon.getStartDate() != null && now.isBefore(coupon.getStartDate())) {
+                return false;
+            }
+            if (coupon.getEndDate() != null && now.isAfter(coupon.getEndDate())) {
+                return false;
+            }
+        }
+
+        if (coupon.getValidDate() != null && now.isAfter(coupon.getValidDate())) {
+            log.debug("过滤已过期的优惠券(validDate), couponId={}, validDate={}, now={}",
+                    coupon.getId(), coupon.getValidDate(), now);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * /userOwned 排序:{@code amount} 仅用于比较「可否视为满足门槛」及估算优惠力度,不参与列表过滤。<br/>
+     * 规则:{@code amount} 必须 &gt; 0;无门槛(最低消费为空或 ≤ 0)即视为门槛已满足(需金额大于 0);有门槛时需 {@code amount ≥ 最低消费}。
+     */
+    private void sortOwnedCouponsForCheckout(List<LifeDiscountCouponVo> couponVos, BigDecimal amount) {
+        if (couponVos == null || couponVos.size() <= 1) {
+            return;
+        }
+        if (amount != null) {
+            couponVos.sort((vo1, vo2) -> {
+                boolean vo1CanUse = canUseCouponForCheckoutSort(vo1, amount);
+                boolean vo2CanUse = canUseCouponForCheckoutSort(vo2, amount);
+
+                if (vo1CanUse != vo2CanUse) {
+                    return vo1CanUse ? -1 : 1;
+                }
+
+                BigDecimal discountAmount1 = calculateDiscountAmountForVO(vo1, amount);
+                BigDecimal discountAmount2 = calculateDiscountAmountForVO(vo2, amount);
+                int cmp = discountAmount2.compareTo(discountAmount1);
+                if (cmp != 0) {
+                    return cmp;
+                }
+                return compareUserCouponIdDesc(vo1, vo2);
+            });
+            return;
+        }
+
+        couponVos.sort((vo1, vo2) -> {
+            BigDecimal nominalValue1 = vo1.getNominalValue() != null ? vo1.getNominalValue() : BigDecimal.ZERO;
+            BigDecimal nominalValue2 = vo2.getNominalValue() != null ? vo2.getNominalValue() : BigDecimal.ZERO;
+            int cmp = nominalValue2.compareTo(nominalValue1);
+            if (cmp != 0) {
+                return cmp;
+            }
+            return compareUserCouponIdDesc(vo1, vo2);
+        });
+    }
+
+    private static int compareUserCouponIdDesc(LifeDiscountCouponVo vo1, LifeDiscountCouponVo vo2) {
+        Integer id1 = vo1 != null ? vo1.getUserCouponId() : null;
+        Integer id2 = vo2 != null ? vo2.getUserCouponId() : null;
+        if (id1 == null && id2 == null) {
+            return 0;
+        }
+        if (id1 == null) {
+            return 1;
+        }
+        if (id2 == null) {
+            return -1;
+        }
+        return id2.compareTo(id1);
+    }
+
+    /**
+     * /userOwned 排序专用:不传金额或金额 ≤ 0 时一律视为不可用(排序靠后)。
+     * 无门槛券:{@code minimumSpendingAmount == null || ≤ 0},需当前 {@code amount &gt; 0};
+     * 有门槛券:需 {@code amount ≥ minimumSpendingAmount}。
+     */
+    private static boolean canUseCouponForCheckoutSort(LifeDiscountCouponVo vo, BigDecimal amount) {
+        if (vo == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+            return false;
+        }
+        BigDecimal min = vo.getMinimumSpendingAmount();
+        if (min == null || min.compareTo(BigDecimal.ZERO) <= 0) {
+            return true;
+        }
+        return min.compareTo(amount) <= 0;
+    }
+
+    /**
      * 将优惠券实体转换为VO
      */
     private LifeDiscountCouponVo convertToVo(LifeDiscountCoupon coupon, LifeDiscountCouponUser userCoupon) {