kong
2026-04-12 37c866f665fc1d2255f0442f92516c686c6c9edf
add 优惠券切换
4个文件已添加
6个文件已修改
336 ■■■■■ 已修改文件
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpAppUserCouponController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/IXpAppUserCouponService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpAppUserCouponServiceImpl.java 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/account/NavigationCouponPickerRequest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/map/RouteAndFareDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/enums/NavigationCouponBubbleType.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/NavigationCouponItemVo.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/NavigationCouponPickerVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/dto/XpOrderDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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();