From 48bdc9af3304ee8a61264c5bb22bd3532e833d44 Mon Sep 17 00:00:00 2001 From: irving Date: Thu, 30 Oct 2025 22:07:37 -0400 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=94=B6=E7=9B=8A=E4=B8=8E?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E9=80=BB=E8=BE=91=EF=BC=9A=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=BD=BF=E7=94=A8=20orderAmount=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BC=98=E6=83=A0=E5=88=B8=E7=8A=B6=E6=80=81=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E4=B8=8E=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IPlayOrderInfoService/PlayOrderInfoServiceImpl/ClerkRevenueCalculator 将参数 finalAmount 更名为 orderAmount,避免语义混淆 - 预计收益计算兼容 null 与 0,防止 NPE 并明确边界 - 结算/回填:EarningsServiceImpl、EarningsBackfillServiceImpl 改为使用 orderMoney 兜底;0 金额允许,负数跳过 - 新增枚举:CouponOnlineState、CouponValidityPeriodType 用于券上下架与有效期判定 - IPlayCouponInfoService/Impl 增加 getCouponDetailRestrictionReason,支持已使用/过期/下架等状态校验 - WxCouponController 列表与下单查询增加状态/有效期/库存/白名单过滤逻辑 - OrderLifecycleServiceImpl 下单时校验优惠券状态,预计收益入参从 finalAmount 调整为 orderMoney - 完善单元测试:订单生命周期、优惠券过滤、收益生成与回填等覆盖 --- .../order/service/IPlayOrderInfoService.java | 8 +- .../impl/OrderLifecycleServiceImpl.java | 9 +- .../impl/PlayOrderInfoServiceImpl.java | 25 ++-- .../support/ClerkRevenueCalculator.java | 29 ++--- .../shop/module/enums/CouponOnlineState.java | 24 ++++ .../enums/CouponValidityPeriodType.java | 25 ++++ .../shop/service/IPlayCouponInfoService.java | 18 +++ .../impl/PlayCouponInfoServiceImpl.java | 59 +++++++++ .../controller/WxCouponController.java | 40 ++++++- .../impl/EarningsBackfillServiceImpl.java | 6 +- .../service/impl/EarningsServiceImpl.java | 6 +- .../impl/OrderLifecycleServiceImplTest.java | 43 ++++++- .../shop/service/CouponWhitelistTest.java | 112 ++++++++++++++++++ .../impl/EarningsBackfillServiceImplTest.java | 57 +++++++++ .../service/impl/EarningsServiceImplTest.java | 71 +++++++++++ 15 files changed, 494 insertions(+), 38 deletions(-) create mode 100644 play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponOnlineState.java create mode 100644 play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponValidityPeriodType.java create mode 100644 play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImplTest.java create mode 100644 play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java index 1bcbd89..e9488bc 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/IPlayOrderInfoService.java @@ -52,13 +52,13 @@ public interface IPlayOrderInfoService extends IService { * @param croupIds 优惠券ID列表 * @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单) * @param firstOrder 是否是首单【0:不是,1:是】 - * @param finalAmount 订单支付金额 + * @param orderAmount 订单金额 * @return com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo * @author admin * @since 2024/7/18 16:39 **/ ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List croupIds, String placeType, - String firstOrder, BigDecimal finalAmount); + String firstOrder, BigDecimal orderAmount); /** * 根据店员等级和订单金额,获取店员预计收入 @@ -66,12 +66,12 @@ public interface IPlayOrderInfoService extends IService { * @param clerkId 店员ID * @param placeType 下单类型(-1:其他类型;0:指定单;1:随机单;2:打赏单) * @param firstOrder 是否是首单【0:不是,1:是】 - * @param finalAmount 订单支付金额 + * @param orderAmount 订单金额 * @return math.BigDecimal 店员预计收入 * @author admin * @since 2024/6/3 11:12 **/ - BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal finalAmount); + BigDecimal getEstimatedRevenue(String clerkId, String placeType, String firstOrder, BigDecimal orderAmount); /** * 根据店员等级,获取店员提成比例 diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java index 860f215..bdac190 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/OrderLifecycleServiceImpl.java @@ -226,6 +226,13 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService { throw new CustomException("优惠券不存在"); } PlayCouponInfoEntity couponInfo = playCouponInfoService.selectPlayCouponInfoById(couponDetails.getCouponId()); + playCouponInfoService.getCouponDetailRestrictionReason( + couponInfo, + couponDetails.getUseState(), + couponDetails.getObtainingTime()) + .ifPresent(reason -> { + throw new CustomException("优惠券不可用 - " + reason); + }); String reason = playCouponInfoService.getCouponReasonForUnavailableUse( couponInfo, pricingInput.getPlaceType().getCode(), @@ -617,7 +624,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService { context.getPaymentInfo().getCouponIds(), context.getPlaceType().getCode(), entity.getFirstOrder(), - context.getPaymentInfo().getFinalAmount()); + context.getPaymentInfo().getOrderMoney()); entity.setEstimatedRevenue(estimatedRevenue.getRevenueAmount()); entity.setEstimatedRevenueRatio(estimatedRevenue.getRevenueRatio()); } diff --git a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java index fb3dc7c..9464fb6 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/order/service/impl/PlayOrderInfoServiceImpl.java @@ -116,8 +116,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl croupIds, String placeType, - String firstOrder, BigDecimal finalAmount) { - return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, finalAmount); + String firstOrder, BigDecimal orderAmount) { + return clerkRevenueCalculator.calculateEstimatedRevenue(clerkId, croupIds, placeType, firstOrder, orderAmount); } /** @@ -126,35 +126,36 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl couponIds, String placeType, String firstOrder, - BigDecimal finalAmount) { + BigDecimal orderAmount) { PlayClerkLevelInfoEntity levelInfo = playClerkUserInfoService.queryLevelCommission(clerkId); + BigDecimal baseAmount = orderAmount == null ? BigDecimal.ZERO : orderAmount; ClerkEstimatedRevenueVo estimatedRevenueVo = new ClerkEstimatedRevenueVo(); boolean fallbackToOther = false; @@ -48,20 +49,20 @@ public class ClerkRevenueCalculator { switch (placeTypeEnum) { case SPECIFIED: // 指定单 - fillRegularOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo); + fillRegularOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo); break; case RANDOM: // 随机单 - fillRandomOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo); + fillRandomOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo); break; case REWARD: // 打赏单 - fillRewardOrderRevenue(firstOrder, finalAmount, levelInfo, estimatedRevenueVo); + fillRewardOrderRevenue(firstOrder, baseAmount, levelInfo, estimatedRevenueVo); break; case OTHER: default: if (!fallbackToOther) { log.warn("按其他下单类型计算预计收益,placeType={},clerkId={}", placeType, clerkId); } - estimatedRevenueVo.setRevenueAmount(finalAmount); + estimatedRevenueVo.setRevenueAmount(baseAmount); estimatedRevenueVo.setRevenueRatio(100); break; } @@ -70,36 +71,36 @@ public class ClerkRevenueCalculator { return estimatedRevenueVo; } - private void fillRegularOrderRevenue(String firstOrder, BigDecimal finalAmount, + private void fillRegularOrderRevenue(String firstOrder, BigDecimal orderAmount, PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) { if ("1".equals(firstOrder)) { vo.setRevenueRatio(levelInfo.getFirstRegularRatio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRegularRatio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRegularRatio())); } else { vo.setRevenueRatio(levelInfo.getNotFirstRegularRatio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRegularRatio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRegularRatio())); } } - private void fillRandomOrderRevenue(String firstOrder, BigDecimal finalAmount, + private void fillRandomOrderRevenue(String firstOrder, BigDecimal orderAmount, PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) { if ("1".equals(firstOrder)) { vo.setRevenueRatio(levelInfo.getFirstRandomRadio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRandomRadio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRandomRadio())); } else { vo.setRevenueRatio(levelInfo.getNotFirstRandomRadio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRandomRadio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRandomRadio())); } } - private void fillRewardOrderRevenue(String firstOrder, BigDecimal finalAmount, + private void fillRewardOrderRevenue(String firstOrder, BigDecimal orderAmount, PlayClerkLevelInfoEntity levelInfo, ClerkEstimatedRevenueVo vo) { if ("1".equals(firstOrder)) { vo.setRevenueRatio(levelInfo.getFirstRewardRatio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getFirstRewardRatio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getFirstRewardRatio())); } else { vo.setRevenueRatio(levelInfo.getNotFirstRewardRatio()); - vo.setRevenueAmount(scaleAmount(finalAmount, levelInfo.getNotFirstRewardRatio())); + vo.setRevenueAmount(scaleAmount(orderAmount, levelInfo.getNotFirstRewardRatio())); } } diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponOnlineState.java b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponOnlineState.java new file mode 100644 index 0000000..b2ea276 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponOnlineState.java @@ -0,0 +1,24 @@ +package com.starry.admin.modules.shop.module.enums; + +import java.util.Arrays; +import java.util.Optional; +import lombok.Getter; + +/** + * 优惠券上下架状态 + */ +@Getter +public enum CouponOnlineState { + OFFLINE("0"), + ONLINE("1"); + + private final String code; + + CouponOnlineState(String code) { + this.code = code; + } + + public static Optional fromCode(String code) { + return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst(); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponValidityPeriodType.java b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponValidityPeriodType.java new file mode 100644 index 0000000..928428f --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/module/enums/CouponValidityPeriodType.java @@ -0,0 +1,25 @@ +package com.starry.admin.modules.shop.module.enums; + +import java.util.Arrays; +import java.util.Optional; +import lombok.Getter; + +/** + * 优惠券有效期类型 + */ +@Getter +public enum CouponValidityPeriodType { + PERMANENT("0"), + FIXED_RANGE("1"), + RELATIVE_DAYS("2"); + + private final String code; + + CouponValidityPeriodType(String code) { + this.code = code; + } + + public static Optional fromCode(String code) { + return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst(); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponInfoService.java b/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponInfoService.java index c228bd0..1d254b6 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponInfoService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/service/IPlayCouponInfoService.java @@ -7,7 +7,9 @@ import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity; import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo; import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; /** * 优惠券信息Service接口 @@ -35,6 +37,22 @@ public interface IPlayCouponInfoService extends IService { String getCouponReasonForUnavailableUse(PlayCouponInfoEntity couponInfo, String placeType, String commodityId, Integer commodityQuantity, BigDecimal price); + /** + * 获取优惠券在状态层面的不可用原因(如已使用、已过期等)。 + * + * @param couponInfo + * 优惠券信息 + * @param useState + * 优惠券详情使用状态 + * @param obtainingTime + * 优惠券领取时间 + * @return 不可用原因 + */ + Optional getCouponDetailRestrictionReason( + PlayCouponInfoEntity couponInfo, + String useState, + LocalDateTime obtainingTime); + /** * 获取优惠券不可领取的原因 * diff --git a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java index 9dbbb9d..40c9ccb 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/shop/service/impl/PlayCouponInfoServiceImpl.java @@ -9,15 +9,20 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.starry.admin.common.exception.CustomException; import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.shop.mapper.PlayCouponInfoMapper; +import com.starry.admin.modules.shop.module.constant.CouponUseState; import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity; import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType; +import com.starry.admin.modules.shop.module.enums.CouponOnlineState; +import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType; import com.starry.admin.modules.shop.module.vo.PlayCouponInfoQueryVo; import com.starry.admin.modules.shop.module.vo.PlayCouponInfoReturnVo; import com.starry.admin.modules.shop.service.IPlayCouponInfoService; import com.starry.common.utils.IdUtils; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; +import java.util.Optional; import javax.annotation.Resource; import org.springframework.stereotype.Service; @@ -71,6 +76,60 @@ public class PlayCouponInfoServiceImpl extends ServiceImpl getCouponDetailRestrictionReason( + PlayCouponInfoEntity couponInfo, + String useState, + LocalDateTime obtainingTime) { + if (couponInfo == null) { + return Optional.of("优惠券不存在"); + } + if (StrUtil.isBlank(useState)) { + return Optional.of("优惠券状态异常"); + } + + CouponUseState state; + try { + state = CouponUseState.fromCode(useState); + } catch (IllegalArgumentException ex) { + return Optional.of("优惠券状态异常"); + } + if (state != CouponUseState.UNUSED) { + return Optional.of("优惠券已使用"); + } + + Optional onlineStateOpt = CouponOnlineState.fromCode(couponInfo.getCouponOnLineState()); + if (onlineStateOpt.isEmpty()) { + return Optional.of("优惠券状态异常"); + } + if (onlineStateOpt.get() != CouponOnlineState.ONLINE) { + return Optional.of("优惠券已下架"); + } + + LocalDateTime now = LocalDateTime.now(); + CouponValidityPeriodType validityType = CouponValidityPeriodType + .fromCode(couponInfo.getValidityPeriodType()) + .orElse(CouponValidityPeriodType.PERMANENT); + if (validityType == CouponValidityPeriodType.FIXED_RANGE) { + if (couponInfo.getProductiveTime() != null && now.isBefore(couponInfo.getProductiveTime())) { + return Optional.of("优惠券未生效"); + } + if (couponInfo.getExpirationTime() != null && now.isAfter(couponInfo.getExpirationTime())) { + return Optional.of("优惠券已过期"); + } + } else if (validityType == CouponValidityPeriodType.RELATIVE_DAYS) { + Integer effectiveDay = couponInfo.getEffectiveDay(); + if (obtainingTime == null || effectiveDay == null || effectiveDay <= 0) { + return Optional.of("优惠券有效期未配置"); + } + LocalDateTime expiration = obtainingTime.plusDays(effectiveDay); + if (now.isAfter(expiration)) { + return Optional.of("优惠券已过期"); + } + } + return Optional.empty(); + } + /** * 获取优惠券不可领取的原因 * diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java index 60b43d6..e0cc73e 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java @@ -10,6 +10,8 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity; import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType; +import com.starry.admin.modules.shop.module.enums.CouponOnlineState; +import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType; import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo; import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.service.IPlayCommodityInfoService; @@ -26,10 +28,12 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; @@ -99,11 +103,37 @@ public class WxCouponController { List obtainedCoupons = couponDetailsService .selectByCustomId(currentCustomId); List couponInfoEntities = couponInfoService.queryAll(); + LocalDateTime now = LocalDateTime.now(); List returnVos = new ArrayList<>(couponInfoEntities.size()); for (PlayCouponInfoEntity couponInfoEntity : couponInfoEntities) { - if ("0".equals(couponInfoEntity.getCouponOnLineState())) { + boolean online = CouponOnlineState.fromCode(couponInfoEntity.getCouponOnLineState()) + .map(state -> state == CouponOnlineState.ONLINE) + .orElse(false); + if (!online) { continue; } + if (couponInfoEntity.getRemainingQuantity() != null && couponInfoEntity.getRemainingQuantity() <= 0) { + continue; + } + CouponValidityPeriodType validityType = CouponValidityPeriodType + .fromCode(couponInfoEntity.getValidityPeriodType()) + .orElse(CouponValidityPeriodType.PERMANENT); + if (validityType == CouponValidityPeriodType.FIXED_RANGE) { + if (couponInfoEntity.getProductiveTime() != null + && now.isBefore(couponInfoEntity.getProductiveTime())) { + continue; + } + if (couponInfoEntity.getExpirationTime() != null + && now.isAfter(couponInfoEntity.getExpirationTime())) { + continue; + } + } else if (validityType == CouponValidityPeriodType.RELATIVE_DAYS) { + Integer effectiveDay = couponInfoEntity.getEffectiveDay(); + if (effectiveDay == null || effectiveDay <= 0) { + continue; + } + // RELATIVE_DAYS 按领取时间计算,这里仅进行配置有效性校验,实际过期由详情校验处理 + } // 领取白名单:非白名单用户不可见 if (CouponClaimConditionType.WHITELIST.code().equals(couponInfoEntity.getClaimConditionType())) { List wl = couponInfoEntity.getCustomWhitelist(); @@ -151,6 +181,14 @@ public class WxCouponController { try { PlayCouponInfoEntity couponInfo = couponInfoService.selectPlayCouponInfoById(couponDetails.getCouponId()); WxCouponOrderReturnVo wxCouponReturnVo = ConvertUtil.entityToVo(couponDetails, WxCouponOrderReturnVo.class); + Optional statusReason = couponInfoService.getCouponDetailRestrictionReason( + couponInfo, + couponDetails.getUseState(), + couponDetails.getObtainingTime()); + if (statusReason.isPresent()) { + // 已使用、过期、下架等状态的优惠券不展示 + continue; + } String couponReasonForUnavailableUse = couponInfoService.getCouponReasonForUnavailableUse(couponInfo, vo.getPlaceType(), vo.getCommodityId(), vo.getCommodityQuantity(), commodityInfo.getCommodityPrice()); diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImpl.java index 9ea70b1..95ceac5 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImpl.java @@ -162,9 +162,9 @@ public class EarningsBackfillServiceImpl implements IEarningsBackfillService { return null; } BigDecimal amount = order.getEstimatedRevenue() != null ? order.getEstimatedRevenue() - : (order.getFinalAmount() != null ? order.getFinalAmount() : BigDecimal.ZERO); - if (amount.compareTo(BigDecimal.ZERO) <= 0) { - addWarning(warnings, "订单 " + order.getId() + " 收益金额为0,跳过"); + : (order.getOrderMoney() != null ? order.getOrderMoney() : BigDecimal.ZERO); + if (amount.compareTo(BigDecimal.ZERO) < 0) { + addWarning(warnings, "订单 " + order.getId() + " 收益金额为负数,跳过"); return null; } if (order.getOrderEndTime() == null) { diff --git a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java index f70eace..0caa435 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java +++ b/play-admin/src/main/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImpl.java @@ -26,10 +26,10 @@ public class EarningsServiceImpl extends ServiceImpl coupons = Collections.singletonList("coupon-reused"); + OrderCreationContext context = baseContext( + PlaceType.SPECIFIED, + RewardType.NOT_APPLICABLE, + payment(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, coupons), + null, + "clerk-1"); + + stubDefaultPersistence(); + + PlayCouponDetailsEntity detail = new PlayCouponDetailsEntity(); + detail.setCouponId("coupon-master"); + detail.setUseState(CouponUseState.USED.getCode()); + detail.setObtainingTime(LocalDateTime.now().minusDays(1)); + when(playCouponDetailsService.getById("coupon-reused")).thenReturn(detail); + + PlayCouponInfoEntity info = new PlayCouponInfoEntity(); + info.setCouponOnLineState("1"); + info.setValidityPeriodType("0"); + when(playCouponInfoService.selectPlayCouponInfoById("coupon-master")).thenReturn(info); + when(playCouponInfoService.getCouponDetailRestrictionReason(eq(info), eq(detail.getUseState()), any(LocalDateTime.class))) + .thenReturn(Optional.of("优惠券已使用")); + + CustomException exception = assertThrows(CustomException.class, () -> lifecycleService.placeOrder(command( + context, + pricing(BigDecimal.valueOf(30), 1, coupons, "commodity-reused", PlaceType.SPECIFIED), + false, + null, + null))); + + assertEquals("优惠券不可用 - 优惠券已使用", exception.getMessage()); + verify(playCouponInfoService).getCouponDetailRestrictionReason(eq(info), eq(detail.getUseState()), any(LocalDateTime.class)); + } + @Test void placeOrder_withDeductionFetchesCustomerBalance() { OrderCreationContext context = baseContext( @@ -1157,6 +1194,8 @@ private PlayOrderLogInfoMapper orderLogInfoMapper; lenient().when(orderInfoMapper.insert(any())).thenReturn(1); lenient().doNothing().when(customUserInfoService).saveOrderInfo(any()); lenient().doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), anyString()); + lenient().when(playCouponInfoService.getCouponDetailRestrictionReason(any(), any(), any())) + .thenReturn(Optional.empty()); ClerkEstimatedRevenueVo revenueVo = new ClerkEstimatedRevenueVo(); revenueVo.setRevenueAmount(BigDecimal.ZERO); revenueVo.setRevenueRatio(0); @@ -1236,6 +1275,8 @@ private PlayOrderLogInfoMapper orderLogInfoMapper; when(playCouponInfoService.selectPlayCouponInfoById(masterId)).thenReturn(info); when(playCouponInfoService.getCouponReasonForUnavailableUse( eq(info), anyString(), anyString(), anyInt(), any())).thenReturn(reason); + when(playCouponInfoService.getCouponDetailRestrictionReason(eq(info), any(), any())) + .thenReturn(Optional.empty()); } private PlayCustomUserInfoEntity customer(String id, BigDecimal balance) { diff --git a/play-admin/src/test/java/com/starry/admin/modules/shop/service/CouponWhitelistTest.java b/play-admin/src/test/java/com/starry/admin/modules/shop/service/CouponWhitelistTest.java index b4656c3..1036938 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/shop/service/CouponWhitelistTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/shop/service/CouponWhitelistTest.java @@ -1,22 +1,33 @@ package com.starry.admin.modules.shop.service; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import com.starry.admin.common.conf.ThreadLocalRequestDetail; import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; +import com.starry.admin.modules.shop.module.constant.CouponUseState; import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity; +import com.starry.admin.modules.shop.module.enums.CouponOnlineState; +import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType; +import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo; +import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.service.IPlayCommodityInfoService; import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; import com.starry.admin.modules.shop.service.IPlayCouponInfoService; import com.starry.admin.modules.shop.service.impl.PlayCouponInfoServiceImpl; import com.starry.admin.modules.weichat.controller.WxCouponController; +import com.starry.admin.modules.weichat.entity.WxCouponOrderQueryVo; +import com.starry.admin.modules.weichat.entity.WxCouponOrderReturnVo; import com.starry.admin.modules.weichat.entity.WxCouponReceiveReturnVo; import com.starry.common.result.R; +import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -97,4 +108,105 @@ public class CouponWhitelistTest { assertEquals(1, list.size(), "非白名单券应被过滤不可见"); assertEquals("c1", list.get(0).getId()); } + + @Test + @DisplayName("列表过滤-状态:下架或过期优惠券不可见") + void testQueryAllFiltersInvalidStatus() { + PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity(); + current.setId("uid-2"); + ThreadLocalRequestDetail.setRequestDetail(current); + + PlayCouponInfoEntity offline = new PlayCouponInfoEntity(); + offline.setId("offline"); + offline.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode()); + + PlayCouponInfoEntity expired = new PlayCouponInfoEntity(); + expired.setId("expired"); + expired.setCouponOnLineState(CouponOnlineState.ONLINE.getCode()); + expired.setValidityPeriodType(CouponValidityPeriodType.FIXED_RANGE.getCode()); + expired.setProductiveTime(LocalDateTime.now().minusDays(5)); + expired.setExpirationTime(LocalDateTime.now().minusDays(1)); + expired.setRemainingQuantity(5); + + PlayCouponInfoEntity active = new PlayCouponInfoEntity(); + active.setId("active"); + active.setCouponOnLineState(CouponOnlineState.ONLINE.getCode()); + active.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode()); + active.setRemainingQuantity(10); + + when(couponDetailsService.selectByCustomId("uid-2")).thenReturn(Collections.emptyList()); + when(couponInfoService.queryAll()).thenReturn(Arrays.asList(offline, expired, active)); + + R resp = wxCouponController.queryAll(); + @SuppressWarnings("unchecked") + List list = (List) resp.getData(); + + assertNotNull(list); + assertEquals(1, list.size(), "仅应展示在线有效的优惠券"); + assertEquals("active", list.get(0).getId()); + } + + @Test + @DisplayName("下单查询:存在限制原因的优惠券被过滤") + void testQueryByOrderFiltersRestrictedCoupons() { + PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity(); + current.setId("uid-3"); + ThreadLocalRequestDetail.setRequestDetail(current); + + PlayCouponDetailsReturnVo restricted = new PlayCouponDetailsReturnVo(); + restricted.setId("detail-1"); + restricted.setCouponId("coupon-restrict"); + restricted.setUseState(CouponUseState.USED.getCode()); + restricted.setObtainingTime(LocalDateTime.now().minusDays(3)); + + PlayCouponDetailsReturnVo usable = new PlayCouponDetailsReturnVo(); + usable.setId("detail-2"); + usable.setCouponId("coupon-usable"); + usable.setUseState(CouponUseState.UNUSED.getCode()); + usable.setObtainingTime(LocalDateTime.now().minusDays(1)); + + when(couponDetailsService.selectByCustomId("uid-3")) + .thenReturn(Arrays.asList(restricted, usable)); + + PlayCouponInfoEntity restrictedInfo = new PlayCouponInfoEntity(); + restrictedInfo.setId("coupon-restrict"); + restrictedInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode()); + restrictedInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode()); + + PlayCouponInfoEntity usableInfo = new PlayCouponInfoEntity(); + usableInfo.setId("coupon-usable"); + usableInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode()); + usableInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode()); + + when(couponInfoService.selectPlayCouponInfoById("coupon-restrict")).thenReturn(restrictedInfo); + when(couponInfoService.selectPlayCouponInfoById("coupon-usable")).thenReturn(usableInfo); + when(couponInfoService.getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class))) + .thenReturn(Optional.of("优惠券已使用")); + when(couponInfoService.getCouponDetailRestrictionReason(eq(usableInfo), eq(usable.getUseState()), any(LocalDateTime.class))) + .thenReturn(Optional.empty()); + when(couponInfoService.getCouponReasonForUnavailableUse(eq(usableInfo), anyString(), anyString(), anyInt(), any())) + .thenReturn(""); + + PlayCommodityInfoVo commodityInfo = new PlayCommodityInfoVo(); + commodityInfo.setCommodityId("cmd-1"); + commodityInfo.setCommodityPrice(BigDecimal.valueOf(50)); + when(commodityInfoService.queryCommodityInfo("cmd-1", "level-1")).thenReturn(commodityInfo); + + WxCouponOrderQueryVo vo = new WxCouponOrderQueryVo(); + vo.setCommodityId("cmd-1"); + vo.setLevelId("level-1"); + vo.setClerkId(""); + vo.setPlaceType("0"); + vo.setCommodityQuantity(1); + + R resp = wxCouponController.queryByOrder(vo); + @SuppressWarnings("unchecked") + List list = (List) resp.getData(); + + assertNotNull(list); + assertEquals(1, list.size(), "受限优惠券应被过滤"); + assertEquals("detail-2", list.get(0).getId()); + assertEquals("1", list.get(0).getAvailable()); + verify(couponInfoService).getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class)); + } } diff --git a/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImplTest.java new file mode 100644 index 0000000..a17ac85 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsBackfillServiceImplTest.java @@ -0,0 +1,57 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class EarningsBackfillServiceImplTest { + + private final EarningsBackfillServiceImpl service = new EarningsBackfillServiceImpl(); + + @SuppressWarnings("unchecked") + private BigDecimal invokeResolveAmount(PlayOrderInfoEntity order, List warnings) throws Exception { + Method method = EarningsBackfillServiceImpl.class.getDeclaredMethod("resolveAmount", PlayOrderInfoEntity.class, + List.class); + method.setAccessible(true); + return (BigDecimal) method.invoke(service, order, warnings); + } + + @Test + void resolveAmount_acceptsZeroAmountWithoutWarnings() throws Exception { + PlayOrderInfoEntity order = new PlayOrderInfoEntity(); + order.setId("order-zero"); + order.setAcceptBy("clerk-001"); + order.setOrderEndTime(LocalDateTime.now()); + order.setOrderMoney(BigDecimal.ZERO); + + List warnings = new ArrayList<>(); + BigDecimal amount = invokeResolveAmount(order, warnings); + + assertEquals(BigDecimal.ZERO, amount); + assertTrue(warnings.isEmpty()); + } + + @Test + void resolveAmount_rejectsNegativeAmount() throws Exception { + PlayOrderInfoEntity order = new PlayOrderInfoEntity(); + order.setId("order-negative"); + order.setAcceptBy("clerk-001"); + order.setOrderEndTime(LocalDateTime.now()); + order.setEstimatedRevenue(BigDecimal.valueOf(-5)); + + List warnings = new ArrayList<>(); + BigDecimal amount = invokeResolveAmount(order, warnings); + + assertNull(amount); + assertEquals(1, warnings.size()); + assertTrue(warnings.get(0).contains("order-negative")); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java new file mode 100644 index 0000000..e6b45d0 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/modules/withdraw/service/impl/EarningsServiceImplTest.java @@ -0,0 +1,71 @@ +package com.starry.admin.modules.withdraw.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper; +import com.starry.admin.modules.withdraw.service.IFreezePolicyService; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class EarningsServiceImplTest { + + @InjectMocks + private EarningsServiceImpl earningsService; + + @Mock + private EarningsLineMapper baseMapper; + + @Mock + private IFreezePolicyService freezePolicyService; + + private PlayOrderInfoEntity baselineOrder() { + PlayOrderInfoEntity order = new PlayOrderInfoEntity(); + order.setId("order-001"); + order.setTenantId("tenant-001"); + order.setAcceptBy("clerk-001"); + order.setOrderEndTime(LocalDateTime.now()); + order.setOrderSettlementState("0"); + return order; + } + + @Test + void createFromOrder_usesOrderAmountWhenEstimatedMissing_evenIfZero() { + PlayOrderInfoEntity order = baselineOrder(); + order.setEstimatedRevenue(null); + order.setOrderMoney(BigDecimal.ZERO); + + when(freezePolicyService.resolveFreezeHours("tenant-001", "clerk-001")).thenReturn(0); + when(baseMapper.insert(any())).thenReturn(1); + + earningsService.createFromOrder(order); + + ArgumentCaptor captor = ArgumentCaptor.forClass(EarningsLineEntity.class); + verify(baseMapper).insert(captor.capture()); + assertEquals(BigDecimal.ZERO, captor.getValue().getAmount()); + } + + @Test + void createFromOrder_skipsWhenAmountNegative() { + PlayOrderInfoEntity order = baselineOrder(); + order.setEstimatedRevenue(BigDecimal.valueOf(-10)); + order.setOrderMoney(BigDecimal.valueOf(20)); + + earningsService.createFromOrder(order); + + verify(baseMapper, never()).insert(any()); + } +}