|
|
@@ -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} 必须 > 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 > 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) {
|