xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpAppUserCouponController.java
@@ -10,6 +10,8 @@ import com.xiaopeng.account.model.vo.XpAppUserCouponVo; import com.xiaopeng.account.service.IXpAppUserCouponService; import com.xiaopeng.api.dto.account.GetUserCouponDto; import com.xiaopeng.api.dto.account.NavigationCouponPickerRequest; import com.xiaopeng.api.vo.account.NavigationCouponPickerVo; import com.xiaopeng.api.dto.account.UseUserCouponDto; import com.xiaopeng.api.feignClient.account.UserResourcesClient; import com.xiaopeng.api.feignClient.operation.XpCouponClient; @@ -23,6 +25,7 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; @@ -106,7 +109,7 @@ userCouponVo.setMoney(discountUserCoupons.get(0).getMoney().doubleValue()); return R.ok(userCouponVo); } List<XpAppUserCoupon> fullUserCoupons = couponList.stream().filter(e -> e.getCouponType() == 2 && e.getFullMoney().doubleValue()>dto.getTotalAmount()).sorted(Comparator.comparing(XpAppUserCoupon::getMoney).reversed()).collect(Collectors.toList()); List<XpAppUserCoupon> fullUserCoupons = couponList.stream().filter(e -> e.getCouponType() == 2 && dto.getTotalAmount() >= e.getFullMoney().doubleValue()).sorted(Comparator.comparing(XpAppUserCoupon::getMoney).reversed()).collect(Collectors.toList()); if(!fullUserCoupons.isEmpty()){ userCouponVo.setId(fullUserCoupons.get(0).getId()); userCouponVo.setMoney(fullUserCoupons.get(0).getMoney().doubleValue()); @@ -133,6 +136,15 @@ /** * 获取优惠券列表 */ @PostMapping("/api/v1/mini-program/coupon/navigation-picker") @ApiOperation(value = "导航下单-选券弹窗(排序、最佳优惠、气泡文案)", tags = {"小程序-优惠券"}) @XpClientCheck(checkToken = false, checkOtpToken = true) public R<NavigationCouponPickerVo> navigationCouponPicker(@RequestBody @Valid NavigationCouponPickerRequest request, HttpServletRequest httpRequest) { String uid = httpRequest.getHeader("XP-Uid"); return R.ok(xpAppUserCouponService.navigationCouponPicker(Long.valueOf(uid), request)); } @PostMapping("/api/v1/mini-program/coupon/list") @ApiOperation(value = "获取用户获取优惠券列表", tags = {"小程序-获取用户获取优惠券列表"}) @XpClientCheck(checkToken = false, checkOtpToken = true) xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/IXpAppUserCouponService.java
@@ -6,11 +6,12 @@ import com.xiaopeng.account.model.query.XpUserCouponListQuery; import com.xiaopeng.account.model.vo.XpAppUserCouponTotalVo; import com.xiaopeng.account.model.vo.XpAppUserCouponVo; import com.xiaopeng.api.dto.account.NavigationCouponPickerRequest; import com.xiaopeng.api.model.account.XpAppUserCoupon; import com.xiaopeng.api.vo.account.NavigationCouponPickerVo; import com.xiaopeng.common.core.domain.R; import java.util.List; import java.util.concurrent.CompletableFuture; /** * <p> @@ -52,6 +53,10 @@ */ List<XpAppUserCoupon> timeRule(List<XpAppUserCoupon> couponList); /** * 导航下单页:选券弹窗列表、气泡文案、最佳优惠标记 */ NavigationCouponPickerVo navigationCouponPicker(Long userId, NavigationCouponPickerRequest request); /** * 发送优惠券 xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpAppUserCouponServiceImpl.java
@@ -15,7 +15,11 @@ import com.xiaopeng.account.model.vo.XpAppUserCouponVo; import com.xiaopeng.account.service.IXpAppUserCouponService; import com.xiaopeng.account.util.holiday.HolidayUtil; import com.xiaopeng.api.dto.account.NavigationCouponPickerRequest; import com.xiaopeng.api.dto.account.UcUserInfo; import com.xiaopeng.api.enums.NavigationCouponBubbleType; import com.xiaopeng.api.vo.account.NavigationCouponItemVo; import com.xiaopeng.api.vo.account.NavigationCouponPickerVo; import com.xiaopeng.api.feignClient.account.UserResourcesClient; import com.xiaopeng.api.feignClient.operation.XpCouponClient; import com.xiaopeng.api.feignClient.operation.XpWhiteListClient; @@ -27,9 +31,13 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** * <p> @@ -225,4 +233,144 @@ return false; } @Override public NavigationCouponPickerVo navigationCouponPicker(Long userId, NavigationCouponPickerRequest req) { NavigationCouponPickerVo vo = new NavigationCouponPickerVo(); double orderAmount = req.getOrderAmount() == null ? 0d : req.getOrderAmount(); List<XpAppUserCoupon> couponList = baseMapper.selectList(new LambdaQueryWrapper<XpAppUserCoupon>() .eq(XpAppUserCoupon::getStatus, 1) .eq(XpAppUserCoupon::getAppUserId, userId) .apply("(FIND_IN_SET({0}, open_city_id) or open_city_id='0')", req.getOpenCityId())); couponList = timeRule(couponList); Long bestId = null; double bestEff = -1d; LocalDateTime bestExp = null; for (XpAppUserCoupon c : couponList) { if (!couponAmountAvailable(c, orderAmount)) { continue; } double eff = effectiveDiscountAmount(c, orderAmount); if (eff <= 0) { continue; } LocalDateTime exp = c.getExpirationTime(); if (bestId == null || eff > bestEff + 1e-6 || (Math.abs(eff - bestEff) < 1e-6 && exp != null && bestExp != null && exp.isBefore(bestExp)) || (Math.abs(eff - bestEff) < 1e-6 && bestExp == null && exp != null)) { bestId = c.getId(); bestEff = eff; bestExp = exp; } } vo.setRecommendedUserCouponId(bestId); Long selected = req.getSelectedUserCouponId(); Long applied; if (selected != null && selected == 0L) { applied = null; } else if (selected != null && selected > 0L) { XpAppUserCoupon chosen = couponList.stream().filter(c -> selected.equals(c.getId())).findFirst().orElse(null); if (chosen != null && couponAmountAvailable(chosen, orderAmount)) { applied = selected; } else { applied = null; } } else { applied = bestId; } vo.setAppliedUserCouponId(applied); long availCount = couponList.stream().filter(c -> couponAmountAvailable(c, orderAmount)).count(); if (availCount == 0) { vo.setBubbleType(NavigationCouponBubbleType.HIDDEN); List<NavigationCouponItemVo> hiddenItems = buildNavigationItems(couponList, orderAmount, bestId, applied); vo.setItems(hiddenItems); vo.setListTotalCount(hiddenItems.size()); return vo; } if (applied == null) { vo.setBubbleType(NavigationCouponBubbleType.MULTI_AVAILABLE); vo.setAvailableCouponCount((int) availCount); } else { vo.setBubbleType(NavigationCouponBubbleType.SELECTED); XpAppUserCoupon ap = couponList.stream().filter(c -> applied.equals(c.getId())).findFirst().orElse(null); vo.setSelectedDiscountAmount(ap != null ? effectiveDiscountAmount(ap, orderAmount) : 0d); } List<NavigationCouponItemVo> items = buildNavigationItems(couponList, orderAmount, bestId, applied); vo.setItems(items); vo.setListTotalCount(items.size()); return vo; } private List<NavigationCouponItemVo> buildNavigationItems(List<XpAppUserCoupon> couponList, double orderAmount, Long bestId, Long appliedId) { List<NavigationCouponItemVo> raw = new ArrayList<>(); for (XpAppUserCoupon c : couponList) { NavigationCouponItemVo it = new NavigationCouponItemVo(); it.setUserCouponId(c.getId()); it.setCouponName(c.getCouponName()); it.setCouponType(c.getCouponType()); it.setDiscountMoney(c.getMoney() != null ? c.getMoney().doubleValue() : 0d); it.setFullMoney(c.getFullMoney() != null ? c.getFullMoney().doubleValue() : null); it.setExpireTime(c.getExpirationTime()); boolean ok = couponAmountAvailable(c, orderAmount); it.setAvailable(ok); if (c.getCouponType() != null && c.getCouponType() == 2) { it.setThresholdLabel("满" + moneyStr(c.getFullMoney()) + "元可用"); if (!ok) { it.setUnavailableReason("未满足使用门槛"); } } else { it.setThresholdLabel("无门槛"); } it.setBestOffer(bestId != null && bestId.equals(c.getId())); it.setSelected(appliedId != null && appliedId.equals(c.getId())); raw.add(it); } List<NavigationCouponItemVo> available = raw.stream().filter(x -> Boolean.TRUE.equals(x.getAvailable())) .sorted(Comparator .comparing((NavigationCouponItemVo x) -> !Boolean.TRUE.equals(x.getBestOffer())) .thenComparing((NavigationCouponItemVo x) -> -effectiveDiscountAmount(findCoupon(couponList, x.getUserCouponId()), orderAmount)) .thenComparing(NavigationCouponItemVo::getExpireTime, Comparator.nullsLast(Comparator.naturalOrder()))) .collect(Collectors.toList()); List<NavigationCouponItemVo> unavailable = raw.stream().filter(x -> !Boolean.TRUE.equals(x.getAvailable())) .sorted(Comparator.comparing(NavigationCouponItemVo::getExpireTime, Comparator.nullsLast(Comparator.naturalOrder()))) .collect(Collectors.toList()); available.addAll(unavailable); return available; } private static XpAppUserCoupon findCoupon(List<XpAppUserCoupon> list, Long id) { return list.stream().filter(c -> Objects.equals(c.getId(), id)).findFirst().orElse(null); } private static String moneyStr(BigDecimal b) { if (b == null) { return "0"; } return b.stripTrailingZeros().toPlainString(); } private static boolean couponAmountAvailable(XpAppUserCoupon c, double orderAmount) { if (c.getCouponType() == null || c.getCouponType() == 1) { return true; } if (c.getCouponType() == 2) { return c.getFullMoney() != null && orderAmount >= c.getFullMoney().doubleValue(); } return true; } private static double effectiveDiscountAmount(XpAppUserCoupon c, double orderAmount) { if (c == null || c.getMoney() == null) { return 0d; } double m = c.getMoney().doubleValue(); if (!couponAmountAvailable(c, orderAmount)) { return 0d; } return Math.min(orderAmount, m); } } xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/account/NavigationCouponPickerRequest.java
New file @@ -0,0 +1,23 @@ package com.xiaopeng.api.dto.account; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.NotNull; @Data @ApiModel("导航页选券列表请求") public class NavigationCouponPickerRequest { @NotNull @ApiModelProperty(value = "开通城市 id", required = true) private Long openCityId; @NotNull @ApiModelProperty(value = "当前订单原价(未用券的预估总价)", required = true) private Double orderAmount; @ApiModelProperty("null=系统自动选最佳优惠;0=用户明确不使用券;>0=用户选中的用户券主键 id") private Long selectedUserCouponId; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteAndFareDto.java
@@ -27,7 +27,7 @@ private Integer routePlanRule; @ApiModelProperty(value = "优惠券id(是否指定使用某个优惠券)") @ApiModelProperty(value = "用户券主键:不传/null=系统自动选最佳;0=明确不使用优惠券;>0=指定使用该券") private Long userCouponId; @ApiModelProperty("车辆id") xp-robotaxi-api/src/main/java/com/xiaopeng/api/enums/NavigationCouponBubbleType.java
New file @@ -0,0 +1,16 @@ package com.xiaopeng.api.enums; import lombok.Getter; /** * 导航页优惠券入口气泡展示 */ @Getter public enum NavigationCouponBubbleType { /** 无可用券,不展示热区 */ HIDDEN, /** 多张可用但未应用券(含用户点「不使用」),展示「N张优惠券可用」 */ MULTI_AVAILABLE, /** 已应用某张券(含系统自动最佳),展示「已优惠xx元」 */ SELECTED } xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/NavigationCouponItemVo.java
New file @@ -0,0 +1,47 @@ package com.xiaopeng.api.vo.account; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDateTime; @Data @ApiModel("导航选券列表项") public class NavigationCouponItemVo { @ApiModelProperty("用户优惠券主键 id") private Long userCouponId; @ApiModelProperty("券名称") private String couponName; @ApiModelProperty("1=抵扣 2=满减") private Integer couponType; @ApiModelProperty("抵扣/减免金额(展示用)") private Double discountMoney; @ApiModelProperty("满减门槛金额,抵扣券可为空") private Double fullMoney; @ApiModelProperty("门槛文案,如无门槛、满XX元可用") private String thresholdLabel; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ApiModelProperty("过期时间") private LocalDateTime expireTime; @ApiModelProperty("当前订单金额下是否可用") private Boolean available; @ApiModelProperty("不可用原因(置灰时展示)") private String unavailableReason; @ApiModelProperty("是否为系统计算的「最佳优惠」") private Boolean bestOffer; @ApiModelProperty("是否为当前应用中的券(单选)") private Boolean selected; } xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/NavigationCouponPickerVo.java
New file @@ -0,0 +1,34 @@ package com.xiaopeng.api.vo.account; import com.xiaopeng.api.enums.NavigationCouponBubbleType; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data @ApiModel("导航选券弹窗数据") public class NavigationCouponPickerVo { @ApiModelProperty("首页气泡类型") private NavigationCouponBubbleType bubbleType; @ApiModelProperty("已优惠金额(bubbleType=SELECTED 时有值)") private Double selectedDiscountAmount; @ApiModelProperty("可用券张数(bubbleType=MULTI_AVAILABLE 时有值)") private Integer availableCouponCount; @ApiModelProperty("弹窗列表总条数(含不可用置灰,用于「平台优惠券(N张)」)") private Integer listTotalCount; @ApiModelProperty("当前实际应用的券 id;未应用为 null") private Long appliedUserCouponId; @ApiModelProperty("系统自动推荐的最佳券 id(可用时)") private Long recommendedUserCouponId; @ApiModelProperty("排序后的列表:可用在上、最佳优惠置顶于可用段、同等抵扣优先快过期") private List<NavigationCouponItemVo> items; } xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/dto/XpOrderDto.java
@@ -49,7 +49,7 @@ private String placementAddress; @ApiModelProperty("用户优惠券id") @ApiModelProperty("用户优惠券主键:null 或 0 表示不使用券;>0 为具体券 id") private Long userCouponId; @ApiModelProperty("多路线快照 ID(选路线下单时与 selectedRouteId 同时传)") xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java
@@ -269,26 +269,23 @@ ); } // 优惠券 // 优惠券:null=自动最佳;0=不使用;>0=指定券 XpPricingCardVo finalXpPricingCardVo = xpPricingCardVo; R<UserCouponVo> coupon = null; CompletableFuture<R<UserCouponVo>> couponFuture=null; // 判断是否指定优惠券 if(routeAndFareDto.getUserCouponId()!=null){ coupon = xpAppUserCouponClient.getUserCoupon(new GetUserCouponDto(Long.valueOf(userId), finalXpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(),routeAndFareDto.getUserCouponId())); Long ucId = routeAndFareDto.getUserCouponId(); if (Long.valueOf(0L).equals(ucId)) { coupon = R.ok(new UserCouponVo()); } else if (ucId != null && ucId > 0L) { coupon = xpAppUserCouponClient.getUserCoupon(new GetUserCouponDto(Long.valueOf(userId), finalXpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(), ucId)); if(coupon.getCode()==200 && coupon.getData().getStatus()==2){ throw new ServiceException("优惠券不满足条件", I18nKeyConstant.COUPON_NOT_SATISFY); } }else { } else { couponFuture= CompletableFuture.supplyAsync( () -> { // 优惠券 return xpAppUserCouponClient.getUserCoupon( new GetUserCouponDto(Long.valueOf(userId), finalXpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(),null) // new GetUserCouponDto(Long.valueOf(userId), 1d, routeAndFareDto.getOpenCityId()) ); }, () -> xpAppUserCouponClient.getUserCoupon( new GetUserCouponDto(Long.valueOf(userId), finalXpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(),null) ), executorService ); } @@ -304,10 +301,10 @@ // 4. 等待所有并行任务完成并获取结果 R<Boolean> diagnosisResult = diagnosisAreaFuture.join(); R<UserCouponVo> couponResult =null; if(routeAndFareDto.getUserCouponId()!=null){ couponResult =coupon; }else { couponResult =couponFuture.join(); if (Long.valueOf(0L).equals(ucId) || (ucId != null && ucId > 0L)) { couponResult = coupon; } else { couponResult = couponFuture.join(); } // 5. 处理诊断结果 @@ -617,7 +614,8 @@ } final Integer planRuleForQueue = dto.getSelectedRoutePlanRule() != null ? dto.getSelectedRoutePlanRule() : (xpRoutePlan != null ? xpRoutePlan.getRoutePlanRule() : null); xpOrder.setCouponId(dto.getUserCouponId()); Long couponRef = dto.getUserCouponId(); xpOrder.setCouponId(couponRef != null && couponRef > 0L ? couponRef : null); // 产生订单 R<Long> addOrderR = xpOrderClient.addOrder(xpOrder); if (addOrderR.getCode() != R.SUCCESS) { @@ -765,9 +763,12 @@ } R<UserCouponVo> coupon = null; CompletableFuture<R<UserCouponVo>> couponFuture = null; if (routeAndFareDto.getUserCouponId() != null) { Long ucId = routeAndFareDto.getUserCouponId(); if (Long.valueOf(0L).equals(ucId)) { coupon = R.ok(new UserCouponVo()); } else if (ucId != null && ucId > 0L) { coupon = xpAppUserCouponClient.getUserCoupon( new GetUserCouponDto(Long.valueOf(userId), xpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(), routeAndFareDto.getUserCouponId())); new GetUserCouponDto(Long.valueOf(userId), xpPricingCardVo.getTotalPrice(), routeAndFareDto.getOpenCityId(), ucId)); if (coupon.getCode() == 200 && coupon.getData().getStatus() == 2) { throw new ServiceException("优惠券不满足条件", I18nKeyConstant.COUPON_NOT_SATISFY); } @@ -778,7 +779,7 @@ executorService); } R<UserCouponVo> couponResult; if (routeAndFareDto.getUserCouponId() != null) { if (Long.valueOf(0L).equals(ucId) || (ucId != null && ucId > 0L)) { couponResult = coupon; } else { couponResult = couponFuture.join();