xuhy
2026-04-22 fa93c9eefcbfcb7ba8b7a9d0b41ba2b4e3523ae5
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -2,11 +2,13 @@
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.ImmutableMap;
@@ -27,7 +29,9 @@
import com.xiaopeng.api.dto.statistics.OrderStatisticsDTO;
import com.xiaopeng.api.dto.statistics.StatisticsQueryDTO;
import com.xiaopeng.api.dto.vehicleConvoy.BaseVehicleUserDto;
import com.xiaopeng.api.feignClient.account.SysGroupMenuClient;
import com.xiaopeng.api.feignClient.account.UserResourcesClient;
import com.xiaopeng.api.feignClient.account.XpAppUserCollectAddrClient;
import com.xiaopeng.api.feignClient.account.XpAppUserCouponClient;
import com.xiaopeng.api.feignClient.automaticPilot.OrderMissionClient;
import com.xiaopeng.api.feignClient.automaticPilot.VehicleBuriedPointClient;
@@ -35,10 +39,7 @@
import com.xiaopeng.api.feignClient.map.XpMapClient;
import com.xiaopeng.api.feignClient.operation.*;
import com.xiaopeng.api.feignClient.pushMessage.XpPushMessageClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.UserVehicleClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetVehicleClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.*;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.operation.*;
import com.xiaopeng.api.model.order.*;
@@ -49,9 +50,11 @@
import com.xiaopeng.api.vo.automaticPilot.VehiclePosition;
import com.xiaopeng.api.vo.automaticPilot.protobuf.OrderOperationType;
import com.xiaopeng.api.vo.automaticPilot.protobuf.OrderStatus;
import com.xiaopeng.api.vo.operation.NearestNextVo;
import com.xiaopeng.api.vo.operation.RouteVo;
import com.xiaopeng.api.vo.order.*;
import com.xiaopeng.api.vo.vehicleConvoy.AddTaxiPassengerVo;
import com.xiaopeng.api.vo.vehicleConvoy.BluetoothDeliverVo;
import com.xiaopeng.api.vo.vehicleConvoy.CancelTaxiPassengerVo;
import com.xiaopeng.common.core.constant.CacheConstants;
import com.xiaopeng.common.core.constant.Constants;
@@ -70,12 +73,10 @@
import com.xiaopeng.common.security.utils.SecurityUtils;
import com.xiaopeng.order.constant.I18nKeyConstant;
import com.xiaopeng.order.dto.MileageDto;
import com.xiaopeng.order.dto.UserOrderListDto;
import com.xiaopeng.order.job.base.QuartzManager;
import com.xiaopeng.order.job.base.TimeJobType;
import com.xiaopeng.order.job.jobs.AddCruiseMissionJob;
import com.xiaopeng.order.job.jobs.EndTripJob;
import com.xiaopeng.order.job.jobs.OrderCancelJob;
import com.xiaopeng.order.job.jobs.PushChainReactionOrderJob;
import com.xiaopeng.order.job.jobs.*;
import com.xiaopeng.order.mangodb.model.OrderTrajectory;
import com.xiaopeng.order.mangodb.service.OrderTrajectoryService;
import com.xiaopeng.order.mapper.XpCruiseOrderMapper;
@@ -88,16 +89,17 @@
import com.xiaopeng.order.model.query.XpOrderPageQuery;
import com.xiaopeng.order.model.vo.*;
import com.xiaopeng.order.service.*;
import com.xiaopeng.order.utils.GDMapGeocodingUtil;
import com.xiaopeng.order.utils.RegionCodeFilterUtil;
import com.xiaopeng.order.utils.TaxiPricingDemoService;
import com.xiaopeng.order.utils.XpPricingUtil;
import com.xiaopeng.order.utils.*;
import com.xiaopeng.order.utils.holiday.HolidayUtil;
import com.xiaopeng.order.vo.OrderDetailVo;
import com.xiaopeng.order.vo.OrderInfoVo;
import com.xiaopeng.order.vo.UserOrderListVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GlobalCoordinates;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -116,6 +118,7 @@
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -135,6 +138,9 @@
@Service
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class XpOrderServiceImpl extends ServiceImpl<XpOrderMapper, XpOrder> implements IXpOrderService {
    private static final String REASSIGN_CANCEL_CONTEXT_PREFIX = "REASSIGN_CANCEL_CONTEXT:";
    private static final long REASSIGN_CANCEL_CONTEXT_TTL_MINUTES = 5L;
    private final UserResourcesClient userResourcesClient;
    private final XpPointClient xpPointClient;
@@ -166,6 +172,8 @@
    @Resource
    @Lazy
    private BootCommonService bootCommonService;
    @Autowired
    private SysGroupMenuClient sysGroupMenuClient;
    @Autowired
@@ -173,8 +181,17 @@
    @Autowired
    private HolidayUtil holidayUtil;
    @Autowired
    private BluetoothUnlockTokenUtil bluetoothUnlockTokenUtil;
    @Resource
    private BluetoothUnlockClient bluetoothUnlockClient;
    @Value("${robotaxi.appId:xp_robotaxi_api}")
    private String appid;
    @Resource(name = "hutoolSnowflake")
    private Snowflake snowflake;
    @Override
@@ -318,7 +335,7 @@
            XpVehicle vehicle = vehicleMap.get(order.getVehicleId());
            if (Objects.nonNull(vehicle)) {
                vo.setPlateNumber(vehicle.getPlateNumber());
                vo.setVehicleModel(vehicle.getVehicleModel());
                vo.setVehicleModel(vehicle.getVehicleModelName());
            }
        }
@@ -541,14 +558,14 @@
    @Override
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    public void cancelOrCloseOrder(Long orderId, Integer type) {
    public void cancelOrCloseOrder(Long orderId, Integer type,Boolean isFree) {
        // 1. 校验订单是否存在
        XpOrder order = this.getById(orderId);
        if (Objects.isNull(order)) {
            throw new ServiceException("订单不存在");
        }
        // 2. 校验操作类型
        // 2. 校验操作类型 操作类型:1=取消订单,2=关闭订单
        if (type != 1 && type != 2) {
            throw new ServiceException("操作类型错误,type只能为1或2");
        }
@@ -562,6 +579,9 @@
                throw new ServiceException("当前订单状态不允许取消,只有待接单、待出发、接驾中状态的订单可以取消", I18nKeyConstant.ORDER_CANCEL_FAILED);
            }
        } else {
            if(isFree == null){
                throw new ServiceException("是否豁免费用不能为空", I18nKeyConstant.ORDER_CLOSE_FAILED);
            }
            // 关闭订单:0=已取消,1=待接单,2=待出发,3=接驾中,4=待客上车,5=乘客已上车,6=服务中 状态可以关闭
            if (currentStatus == 7 || currentStatus == 8 || currentStatus == 9 || currentStatus == 10) {
                throw new ServiceException("当前订单状态不允许关闭", I18nKeyConstant.ORDER_CLOSE_FAILED);
@@ -575,6 +595,9 @@
        } else {
            // 关闭订单
            closeOrder(order);
            if(isFree){
                this.setFree(order.getId());
            }
        }
    }
@@ -595,19 +618,21 @@
        vo.setEndAddress(order.getEndAddress());
        vo.setOrderMoney(order.getOrderMoney());
        vo.setStatus(order.getStatus());
        vo.setStartPointName(order.getStartPointName());
        vo.setEndPointName(order.getEndPointName());
        // 用户信息
        if (Objects.nonNull(order.getAppUserId()) && userInfoMap != null && userInfoMap.containsKey(order.getAppUserId().toString())) {
            UcUserInfo userInfo = userInfoMap.get(order.getAppUserId().toString());
            vo.setNickName(userInfo.getNickName());
            vo.setPhoneNumber(userInfo.getMobile());
            vo.setPhoneNumber(sysGroupMenuClient.getPhoneDesensitization(userInfo.getMobile()).getData());
        }
        // 车辆信息
        if (Objects.nonNull(order.getVehicleId()) && vehicleMap.containsKey(order.getVehicleId())) {
            XpVehicle vehicle = vehicleMap.get(order.getVehicleId());
            vo.setPlateNumber(vehicle.getPlateNumber());
            vo.setVehicleModel(vehicle.getVehicleModel());
            vo.setVehicleModel(vehicle.getVehicleModelName());
        }
        // 车队信息
@@ -644,9 +669,25 @@
        Long userId = order.getAppUserId();
        if (order.getStatus() == 1) {
            order.setStatus(0);
            Long appUserId = order.getAppUserId();
            order.setAppUserId(null);
            baseMapper.updateById(order);
            order.setAppUserId(appUserId);
            redisService.setCacheObject("ORDER_STATUS:" + order.getId(), 0, 24L, TimeUnit.HOURS);
            //删除蓝牙秘钥
            String token = redisService.getCacheObject("rt:bluetooth:unlock:order:" + order.getId());
            if(StringUtils.isNotBlank(token)){
                redisService.deleteObject("rt:bluetooth:unlock:" + token);
                redisService.deleteObject("rt:bluetooth:unlock:order:" + order.getId());
                XpVehicle xpVehicle = xpVehicleClient.queryById(order.getVehicleId()).getData();
                BluetoothDeliverVo vo = new BluetoothDeliverVo();
                vo.setVim(xpVehicle.getVehicleVin());
                vo.setPassengerUid(order.getAppUserId());
                BaseVehicleUserDto delete = bluetoothUnlockClient.delete(vo);
                if(null == delete || delete.getCode() != 0){
                    log.error("删除蓝牙解锁失败:{} {}", delete.getMsg(), order.getOrderNumber());
                }
            }
            // 消息推送
            Map<String, Object> params = new HashMap<>();
            Map<String, Object> content = new HashMap<>();
@@ -973,6 +1014,8 @@
                    cancelDto.setOperation_type(OrderOperationType.CLOUD_CANCEL_ORDER);
                    cancelDto.setCancelScene(CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE.getCode());
                    orderMissionClient.cancelOrder(cancelDto);
                    cacheReassignCancelContext(orderId, oldVehicleId, newVehicleId,
                            oldVehicle.getVehicleVin(), currentStatus);
                    log.info("已取消原车辆订单指令,订单ID: {}, VIN: {}, taskId: {}, cancelScene: {}",
                            orderId, oldVehicle.getVehicleVin(), cancelDto.getTaskId(), cancelDto.getCancelScene());
@@ -1076,6 +1119,7 @@
            // 11.2 下发订单到新车辆
            PushOrderDto pushOrderDto = new PushOrderDto();
            pushOrderDto.setTaskId(order.getXpuTaskId());
            pushOrderDto.setVin(newVehicle.getVehicleVin());
            pushOrderDto.setOrderId(order.getId().toString());
            pushOrderDto.setStartLatitude(order.getStartLat());
@@ -1118,6 +1162,22 @@
    }
    private void cacheReassignCancelContext(Long orderId, Long oldVehicleId, Long newVehicleId,
                                            String oldVehicleVin, Integer preReassignStatus) {
        JSONObject context = new JSONObject();
        context.put("orderId", orderId);
        context.put("oldVehicleId", oldVehicleId);
        context.put("newVehicleId", newVehicleId);
        context.put("oldVehicleVin", oldVehicleVin);
        context.put("cancelScene", CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE.getCode());
        context.put("operationTime", System.currentTimeMillis());
        context.put("preReassignStatus", preReassignStatus);
        redisService.setCacheObject(REASSIGN_CANCEL_CONTEXT_PREFIX + orderId, context.toJSONString(),
                REASSIGN_CANCEL_CONTEXT_TTL_MINUTES, TimeUnit.MINUTES);
        log.info("记录改派取消上下文成功,orderId: {}, oldVehicleId: {}, newVehicleId: {}, preReassignStatus: {}",
                orderId, oldVehicleId, newVehicleId, preReassignStatus);
    }
    /**
     * 为原车辆创建返程巡游订单
     * <p>
@@ -1154,6 +1214,7 @@
            returnOrder.setLastOrderId(lastCruise.getId());
            returnOrder.setCreateTime(LocalDateTime.now());
            returnOrder.setMissionId(lastCruise.getMissionId());
            returnOrder.setXpuTaskId(snowflake.nextId() + "");
            // 返程起点 = 当前订单的终点(车辆当前位置附近)
            returnOrder.setStartPointId(order.getEndPointId());
@@ -1192,6 +1253,7 @@
            // 下发巡游订单到原车辆
            PushOrderDto pushOrderDto = new PushOrderDto();
            pushOrderDto.setTaskId(returnOrder.getXpuTaskId());
            pushOrderDto.setVin(oldVehicle.getVehicleVin());
            pushOrderDto.setOrderId("XU" + returnOrder.getId());
            pushOrderDto.setStartLatitude(returnOrder.getStartLat());
@@ -1252,6 +1314,59 @@
        }
        return null;
    }
    @Resource
    private XpAppUserCollectAddrClient xpAppUserCollectAddrClient;
    @Override
    public NearestNextVo quickCapsule(String uid, Long startPointId) {
        Long userId = Long.valueOf(uid);
        // - 新用户处理: 历史订单数 < 3单或无匹配数据时,隐藏该胶囊。
        Long validHistoryCount = this.baseMapper.countUserValidHistoryOrders(userId);
        if (validHistoryCount == null || validHistoryCount < 3) return null;
        LocalDateTime now = LocalDateTime.now();
        LocalTime startMoment = now.minusHours(1).toLocalTime();
        LocalTime endMoment = now.plusHours(1).toLocalTime();
        LocalDate startDate = now.minusMonths(1).toLocalDate();
        LocalDateTime endDate = now.plusMonths(1);
        // 时段强相关目的地(场景触发)当前时间点(当前时间 ± 1小时)内,过去30天内前往次数最多的目的地。
        List<Long> endPointId = this.baseMapper.timeSceneOrders(startDate, endDate, startMoment, endMoment, userId);
        // 判定条件: 若当前时段内无明显的规律性目的地(例如:用户在周六下午偶尔出门,且目的地不固定),则推荐用户过去30天内订单总量最高的前1名地址(需排除当前起点)。
        endPointId.addAll(this.baseMapper.frequencyOrders(startDate, endDate, startMoment, endMoment, userId));
        Integer minOrderDistance = getMinOrderDistanceSafely(uid, startPointId);
        if (minOrderDistance == null) return null;//获取用户最短可发单距离失败
        XpPoint start = getPointById(startPointId);
        GlobalCoordinates startCoordinates = new GlobalCoordinates(start.getPointLon(), start.getPointLat());
        List<Long> pointIdsByUserId = Optional.ofNullable(xpAppUserCollectAddrClient.getPointIdsByUserId(Long.valueOf(uid)).getData())
                .orElse(new ArrayList<>());
        for (Long pointId : endPointId) {
            XpPoint endPoint = getPointById(pointId);
            // 计算直线距离,如果endPoint到startPoint的距离大于minOrderDistance,则返回该点
            GlobalCoordinates endCoordinates = new GlobalCoordinates(endPoint.getPointLon(), endPoint.getPointLat());
            double distanceMeter = GeodesyUtil.getDistanceMeter(startCoordinates, endCoordinates, Ellipsoid.WGS84);
            if (distanceMeter > minOrderDistance) {
                NearestNextVo ret = new NearestNextVo();
                ret.setId(endPoint.getId());
                ret.setName(endPoint.getPointName());
                ret.setLatitude(endPoint.getPointLat());
                ret.setLongitude(endPoint.getPointLon());
                ret.setDistance(new BigDecimal(distanceMeter).setScale(2, RoundingMode.HALF_UP).doubleValue());
                ret.setAddress(endPoint.getPointAddress());
                ret.setTime(endPoint.getParkingTimeLimit());
                ret.setElectronicAreaId(endPoint.getElectronicAreaId());
                // 判断是否收藏
                if(pointIdsByUserId.contains(endPoint.getId())){
                    ret.setIsCollect(1);
                }
                return ret;
            }
        }
        return null;
    }
    private RecommendationCandidate buildSceneCandidate(List<XpOrder> recentOrders, LocalDateTime now, XpPoint startPoint, Integer minOrderDistance) {
        DaySceneType currentSceneType = resolveDaySceneType(now.toLocalDate());
@@ -1672,9 +1787,25 @@
            addCruiseMission(xpVehicle.getId());
        }
        order.setStatus(0);
        Long appUserId = order.getAppUserId();
        order.setAppUserId(null);
        baseMapper.updateById(order);
        order.setAppUserId(appUserId);
        redisService.setCacheObject("ORDER_STATUS:" + order.getId(), 0, 24L, TimeUnit.HOURS);
        //删除蓝牙秘钥
        String token = redisService.getCacheObject("rt:bluetooth:unlock:order:" + order.getId());
        if(StringUtils.isNotBlank(token)){
            redisService.deleteObject("rt:bluetooth:unlock:" + token);
            redisService.deleteObject("rt:bluetooth:unlock:order:" + order.getId());
            XpVehicle xpVehicle = xpVehicleClient.queryById(order.getVehicleId()).getData();
            BluetoothDeliverVo vo = new BluetoothDeliverVo();
            vo.setVim(xpVehicle.getVehicleVin());
            vo.setPassengerUid(order.getAppUserId());
            BaseVehicleUserDto delete = bluetoothUnlockClient.delete(vo);
            if(null == delete || delete.getCode() != 0){
                log.error("删除蓝牙解锁失败:{} {}", delete.getMsg(), order.getOrderNumber());
            }
        }
        XpOrderCancel xpOrderCancel = new XpOrderCancel();
        xpOrderCancel.setOrderId(orderId);
@@ -1698,7 +1829,23 @@
        order.setStatus(0);
        order.setAppUserId(null);
        baseMapper.updateById(order);
        order.setAppUserId(appUserId);
        redisService.setCacheObject("ORDER_STATUS:" + order.getId(), 0, 24L, TimeUnit.HOURS);
        //删除蓝牙秘钥
        String token = redisService.getCacheObject("rt:bluetooth:unlock:order:" + order.getId());
        if(StringUtils.isNotBlank(token)){
            redisService.deleteObject("rt:bluetooth:unlock:" + token);
            redisService.deleteObject("rt:bluetooth:unlock:order:" + order.getId());
            XpVehicle xpVehicle = xpVehicleClient.queryById(order.getVehicleId()).getData();
            BluetoothDeliverVo vo = new BluetoothDeliverVo();
            vo.setVim(xpVehicle.getVehicleVin());
            vo.setPassengerUid(order.getAppUserId());
            BaseVehicleUserDto delete = bluetoothUnlockClient.delete(vo);
            if(null == delete || delete.getCode() != 0){
                log.error("删除蓝牙解锁失败:{} {}", delete.getMsg(), order.getOrderNumber());
            }
        }
        XpOrderCancel xpOrderCancel = new XpOrderCancel();
        xpOrderCancel.setOrderId(Long.valueOf(orderId));
        xpOrderCancel.setUserId(1l);
@@ -1788,7 +1935,7 @@
    }
    @Override
    public R<String> updateOrderStatus(Long orderId, Integer status) {
    public R<String> updateOrderStatus(Long orderId, Integer status, Boolean orderException) {
        XpOrder xpOrder = this.getById(orderId);
        if (Objects.isNull(xpOrder)) {
            return R.fail("订单不存在");
@@ -1843,6 +1990,12 @@
            case 6:
                // 行程开始时间
                xpOrder.setTripBegin(LocalDateTime.now());
                //添加预估到达时间
                String cacheObject = redisService.getCacheObject("ORDER_NAVIGATION_END_DATA:" + xpOrder.getAppUserId());
                if(StringUtils.isNotBlank(cacheObject)){
                    RouteVo routeVo = JSONObject.parseObject(cacheObject, RouteVo.class);
                    xpOrder.setEstimatedArrivalTime(LocalDateTime.now().plusSeconds(routeVo.getDuration()));
                }
                redisService.deleteObject("ORDER_NAVIGATION_START_DATA:" + xpOrder.getAppUserId());
                break;
            case 7:
@@ -1897,7 +2050,6 @@
                    });
                }
                //创建定时结束订单和等待结束后下发巡游单的定时任务
                Integer arriveTime = xpSystemSettingClient.getOrderFinishTime().getData();
                // 添加自动结束行程定时任务job
                Map<String, ? extends Object> endTripJobMaps =
@@ -1924,6 +2076,18 @@
                                .put("appid", appid)
                                .put("orderType", OrderType.USER_ORDER)
                                .build();
                //如果是异常订单,需要定时创建客服工单
                if(orderException){
                    XpSystemSetting xpSystemSetting = xpSystemSettingClient.getSystemSettingOne().getData();
                    QuartzManager.addJob(
                            OrderExceptionJob.class,
                            (OrderExceptionJob.name + xpOrder.getId()).toUpperCase(),
                            TimeJobType.ADD_CRUISE_MISSION,
                            DateUtils.localDateTimeToDate(LocalDateTime.now().plusMinutes(xpSystemSetting.getOrderExceptionalTimeout())),
                            jobMaps
                    );
                }
                //判断是否有连环单
                Long id = redisService.getCacheObject(CacheConstants.nextOrder(xpOrder.getId().toString()));
@@ -2002,15 +2166,25 @@
        this.endTrip(xpOrder.getId(),appUserId.toString());
        //将支付金额修改成0 并将状态修改成 订单状态9
        setFree(xpOrder.getId());
        return R.ok();
    }
    /**
     * 豁免
     */
    public void setFree(Long id){
        //将支付金额修改成0 并将状态修改成 订单状态9
        XpOrder xpOrder1 = new XpOrder();
        xpOrder1.setId(xpOrder.getId());
        xpOrder1.setId(id);
        xpOrder1.setIsFree( true);//豁免
        xpOrder1.setPayMoney(BigDecimal.ZERO);
        xpOrder1.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
        xpOrder1.setUpdateTime(LocalDateTime.now());
        baseMapper.updateById(xpOrder1);
        redisService.setCacheObject("ORDER_STATUS:" + xpOrder.getId(), xpOrder1.getStatus(), 24L, TimeUnit.HOURS);
        redisService.setCacheObject("ORDER_STATUS:" + id, xpOrder1.getStatus(), 24L, TimeUnit.HOURS);
        return R.ok();
    }
@@ -2035,10 +2209,26 @@
            throw new RuntimeException("订单状态错误");
        }
        order.setStatus(8);
        Long appUserId = order.getAppUserId();
        order.setAppUserId(null);
        order.setTripEnd(LocalDateTime.now());
        baseMapper.updateById(order);
        order.setAppUserId(appUserId);
        redisService.setCacheObject("ORDER_STATUS:" + order.getId(), 8, 24L, TimeUnit.HOURS);
        //删除蓝牙秘钥
        String token = redisService.getCacheObject("rt:bluetooth:unlock:order:" + orderId);
        if(StringUtils.isNotBlank(token)){
            redisService.deleteObject("rt:bluetooth:unlock:" + token);
            redisService.deleteObject("rt:bluetooth:unlock:order:" + orderId);
            XpVehicle xpVehicle = xpVehicleClient.queryById(order.getVehicleId()).getData();
            BluetoothDeliverVo vo = new BluetoothDeliverVo();
            vo.setVim(xpVehicle.getVehicleVin());
            vo.setPassengerUid(order.getAppUserId());
            BaseVehicleUserDto delete = bluetoothUnlockClient.delete(vo);
            if(null == delete || delete.getCode() != 0){
                log.error("删除蓝牙解锁失败:{} {}", delete.getMsg(), order.getOrderNumber());
            }
        }
        // 根据订单所属开通城市id 查询定价
        XpPricing pricing = bootCommonService.getXpPricing(order.getCityId(),order.getVehicleId());
@@ -2454,6 +2644,7 @@
        xpCruiseOrder.setStartPointName(order.getEndPointName());
        xpCruiseOrder.setStartLon(order.getEndLon());
        xpCruiseOrder.setStartLat(order.getEndLat());
        xpCruiseOrder.setXpuTaskId(snowflake.nextId() + "");
        // 完成的直接走终点   终止的走起点
        if(null != xpCruiseOrders){
@@ -2514,6 +2705,7 @@
        log.info("添加巡游单成功 {}", xpCruiseOrder.getId());
        //下发XUP巡游订单报文
        PushOrderDto pushOrderDto = new PushOrderDto();
        pushOrderDto.setTaskId(xpCruiseOrder.getXpuTaskId());
        pushOrderDto.setVin(xpCruiseOrder.getVehicleVin());
        pushOrderDto.setOrderId("XU" + xpCruiseOrder.getId());
        pushOrderDto.setStartLatitude(xpCruiseOrder.getStartLat());
@@ -2561,6 +2753,7 @@
        xpCruiseOrder.setFleetId(vehicle.getFleetId());
        xpCruiseOrder.setVehicleVin(vehicle.getVehicleVin());
        xpCruiseOrder.setOrderType(2);
        xpCruiseOrder.setXpuTaskId(snowflake.nextId() + "");
        // 返程起点是当前正常订单的终点
        xpCruiseOrder.setStartPointId(null);
        xpCruiseOrder.setStartPointName(null);
@@ -2630,6 +2823,7 @@
        //下发XPU消息报文
        PushOrderDto pushOrderDto = new PushOrderDto();
        pushOrderDto.setTaskId(xpCruiseOrder.getXpuTaskId());
        pushOrderDto.setVin(xpCruiseOrder.getVehicleVin());
        pushOrderDto.setOrderId("XU" + xpCruiseOrder.getId());
        pushOrderDto.setStartLatitude(xpCruiseOrder.getStartLat());
@@ -2886,6 +3080,10 @@
            this.updateById(order);
        }
        res.setTime(new BigDecimal(order.getDuration() == null ? 0 : order.getDuration()).multiply(new BigDecimal(60)).setScale(0, RoundingMode.HALF_UP));
        XpSystemSetting xpSystemSetting = xpSystemSettingClient.getSystemSettingOne().getData();
        if(null != xpSystemSetting){
            res.setOperateFree(xpSystemSetting.getOperateFree());
        }
        return R.ok(res);
    }
@@ -3255,16 +3453,19 @@
                //生成前往上车点的导航数据
                //查询车辆位置
                VehiclePosition vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("vin").is(dto.getXpVehicleVin())), VehiclePosition.class);
                if (Objects.isNull(vehiclePosition)) {
                    throw new RuntimeException("车辆位置信息不存在");
                }
                //TODO 本地环境默认写死
                // VehiclePosition vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("vin").is(dto.getXpVehicleVin())), VehiclePosition.class);
                // if (Objects.isNull(vehiclePosition)) {
                //     throw new RuntimeException("车辆位置信息不存在");
                // }
                LonLatDto destination = new LonLatDto();
                LonLatDto origin = new LonLatDto();
                //查询司机当前位置到起点的线路
                origin.setLongitude(vehiclePosition.getPoint().getX());
                origin.setLatitude(vehiclePosition.getPoint().getY());
                // origin.setLongitude(vehiclePosition.getPoint().getX());
                // origin.setLatitude(vehiclePosition.getPoint().getY());
                origin.setLongitude(104.055499);
                origin.setLatitude(30.586747);
                destination.setLongitude(order.getStartLon());
                destination.setLatitude(order.getStartLat());
@@ -3284,6 +3485,45 @@
                //推送乘客车辆到达预约点的导航数据
                sendRoute(addTaxiPassengerVo.getUid(), orderId, "ORDER_NAVIGATION_START_DATA", dto.getXpVehicleVin());
                //生成蓝牙token
                //生成过期时间 (接单时间+到达上车点预估时间+上车点等待时间+行程预估时间+下车点等待时间+30分钟)
                XpPoint startPoint = xpPointClient.queryById(order.getStartPointId()).getData();
                Object cacheObject = redisService.getCacheObject("ORDER_NAVIGATION_END_DATA:" + appUserId);
                RouteVo routeVo1 = JSON.parseObject(cacheObject.toString(), RouteVo.class);
                XpPoint endPoint = xpPointClient.queryById(order.getEndPointId()).getData();
                LocalDateTime localDateTime = order.getAcceptanceTime().plusMinutes(routeVo.getDuration()).plusMinutes(startPoint.getParkingTimeLimit())
                        .plusMinutes(routeVo1.getDuration()).plusMinutes(endPoint.getParkingTimeLimit()).plusMinutes(30);
                long expirationTime = localDateTime.toEpochSecond(ZoneOffset.of("+8"));
                //生成token
                String token = bluetoothUnlockTokenUtil.generateBluetoothUnlockToken(order.getAppUserId(),
                        order.getId(), order.getVehicleId(), expirationTime);
                log.info("生成蓝牙token:{} 有效期:{} 订单号: {}", token, expirationTime, order.getOrderNumber());
                //存储缓存
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("userId", order.getAppUserId());
                jsonObject.put("orderId", order.getId());
                jsonObject.put("vehicleId", order.getVehicleId());
                jsonObject.put("expirationTime", expirationTime);
                jsonObject.put("orderNumber", order.getOrderNumber());
                jsonObject.put("vehicleVin", vehicle.getVehicleVin());
                jsonObject.put("createTime", System.currentTimeMillis());
                redisService.setCacheObject("rt:bluetooth:unlock:" + token, jsonObject.toJSONString(), expirationTime, TimeUnit.SECONDS);
                redisService.setCacheObject("rt:bluetooth:unlock:order:" + order.getId(), token, expirationTime, TimeUnit.SECONDS);
                //开始下发蓝牙token
                BluetoothDeliverVo vo = new BluetoothDeliverVo();
                vo.setCredential(token);
                vo.setVim(vehicle.getVehicleVin());
                vo.setPassengerUid(order.getAppUserId());
                BaseVehicleUserDto deliver = bluetoothUnlockClient.deliver(vo);
                if(null == deliver){
                    throw new ServiceException("下发蓝牙token失败");
                }
                if(deliver.getCode() != 0){
                    log.error("下发蓝牙token失败:{} {}", deliver.getMsg(), order.getOrderNumber());
                    throw new ServiceException(deliver.getMsg());
                }
                log.info("下发蓝牙token成功:{} {}", deliver.getData(), order.getOrderNumber());
            }
            if(order.getStatus() == 2){
@@ -3462,7 +3702,7 @@
            R<XpVehicle> xpVehicleR = xpVehicleClient.queryById(xpOrder.getVehicleId());
            XpVehicle vehicle = xpVehicleR.getData();
            xpOrderDetailVO.setVehicleVin(vehicle.getVehicleVin());
            xpOrderDetailVO.setVehicleModel(vehicle.getVehicleModel());
            xpOrderDetailVO.setVehicleModel(vehicle.getVehicleModelName());
            xpOrderDetailVO.setVehicleColor(vehicle.getVehicleColor());
            xpOrderDetailVO.setPlateNumber(vehicle.getPlateNumber());
            xpOrderDetailVO.setVehiclePictures(vehicle.getVehiclePictures());
@@ -3490,7 +3730,7 @@
            R<XpVehicle> xpVehicleR = xpVehicleClient.queryById(xpOrder.getVehicleId());
            XpVehicle vehicle = xpVehicleR.getData();
            xpOrderDetailVO.setVehicleVin(vehicle.getVehicleVin());
            xpOrderDetailVO.setVehicleModel(vehicle.getVehicleModel());
            xpOrderDetailVO.setVehicleModel(vehicle.getVehicleModelName());
            xpOrderDetailVO.setVehicleColor(vehicle.getVehicleColor());
            xpOrderDetailVO.setPlateNumber(vehicle.getPlateNumber());
@@ -3510,21 +3750,28 @@
     */
    private boolean isCancelShow(XpOrder xpOrder,String vehicleVin) {
        boolean isCancel = false;
        //获取系统配置
        XpSystemSetting systemSetting = xpSystemSettingClient.getSystemSettingOne().getData();
        if(systemSetting == null){
            throw new ServiceException("获取行程配置错误");
        }
        //获取配置文件
        int sylc = 800;//单位米 todo yyy 需要读取后台配置文件
        int sysj = 5;//单位分钟 todo yyy 需要读取后台配置文件
        int sylc = systemSetting.getXczSyDistance();  // 行程中剩余里程单位米  需要读取后台配置文件
        int sysj = systemSetting.getXczSyTime();//行程中剩余时间 单位分钟  需要读取后台配置文件
        // 减少3分钟 获取3分账内的 行程数据
        long minute =3;//todo yyy 需要读取后台配置文件
        Integer xczSpeedPerHour = systemSetting.getXczHmSpeedPerHour();//行程中豁免时速 km/h
        Integer xczHmDuration = systemSetting.getXczHmDuration();//行程中豁免持续时间 分钟  需要读取后台配置文件
        List<OrderTrajectory> orderTrajectories = queryOrderTrajectories(xpOrder.getId());
        if(!orderTrajectories.isEmpty()){// 减少3分钟 获取3分账内的 行程数据
            //3千米/小时 转换成分钟 50米/分钟
            BigDecimal suduDefault = new BigDecimal("3");//3千米/小时
            BigDecimal suduDefault = new BigDecimal(xczSpeedPerHour);//3千米/小时
            BigDecimal suduDefault1 = suduDefault.multiply(BigDecimal.valueOf(1000)) ;
            BigDecimal suduDefaultNew =suduDefault1.divide(BigDecimal.valueOf(60),2,RoundingMode.HALF_EVEN);
            LocalDateTime minutesBefore = LocalDateTime.now().minusMinutes(minute);
            LocalDateTime minutesBefore = LocalDateTime.now().minusMinutes(xczHmDuration);
            long milliseconds = DateUtils.getMilliseconds(minutesBefore);
            List<OrderTrajectory> orderTrajectoriesNew = new ArrayList<>();
            for (OrderTrajectory orderTrajectory : orderTrajectories) {
@@ -3537,7 +3784,7 @@
             */
            //计算行驶总距离和时长
            Double reduce = orderTrajectoriesNew.stream().map(OrderTrajectory::getDistance).reduce(0.0, Double::sum);
            BigDecimal xcsudu = new BigDecimal(reduce).divide(new BigDecimal(minute),2, RoundingMode.HALF_EVEN);//米/分钟
            BigDecimal xcsudu = new BigDecimal(reduce).divide(new BigDecimal(xczHmDuration),2, RoundingMode.HALF_EVEN);//米/分钟
            //特殊豁免:若系统检测到车辆在当前坐标时速 < 3km/h 且持续时间 > 3分钟(后台配置)(判定为极度拥堵或异常停滞),此时解除限制,允许用户修改目的地或新增途径点以寻找更近的下车位置。
            if(xcsudu.compareTo(suduDefaultNew)<0){
@@ -3678,4 +3925,79 @@
        );
        return num.intValue();
    }
    /**
     * 根据 UID 查询用户是否有 RT 订单 & 订单列表
     * @param dto
     * @return
     */
    @Override
    public List<UserOrderListVo> getUserOrderList(UserOrderListDto dto) {
        List<XpOrder> list = this.list(new QueryWrapper<XpOrder>().eq("app_user_id", dto.getUid())
                .eq("is_deleted", 0)
                .eq(dto.getOrderStatus() != null, "status", dto.getOrderStatus())
                .eq(dto.getOrderId() != null, "id", dto.getOrderId())
                .last(" order by create_time desc limit " + dto.getPageNo() + "," + dto.getPageSize())
        );
        List<UserOrderListVo> collect = list.stream().map(order -> {
            UserOrderListVo vo = new UserOrderListVo();
            vo.setOrderId(order.getId());
            vo.setOrderStatus(order.getStatus());
            vo.setOrderCreatedAt(order.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            XpPoint startPoint = xpPointClient.queryById(order.getStartPointId()).getData();
            vo.setPickupAddress(null != startPoint ? startPoint.getPointAddress() : null);
            XpPoint endPoint = xpPointClient.queryById(order.getEndPointId()).getData();
            vo.setDestAddress(null != endPoint ? endPoint.getPointAddress() : null);
            vo.setTotalPrice(order.getOrderMoney());
            return vo;
        }).collect(Collectors.toList());
        return collect;
    }
    /**
     * 获取订单详情
     * @param orderId
     * @return
     */
    @Override
    public OrderInfoVo getOrderInfo(Long orderId) {
        XpOrder xpOrder = this.getById(orderId);
        OrderInfoVo vo = new OrderInfoVo();
        vo.setOrderId(xpOrder.getId());
        vo.setOrderStatus(xpOrder.getStatus());
        XpPoint startPoint = xpPointClient.queryById(xpOrder.getStartPointId()).getData();
        vo.setPickupAddress(startPoint.getPointAddress());
        XpPoint endPoint = xpPointClient.queryById(xpOrder.getEndPointId()).getData();
        vo.setDestAddress(endPoint.getPointAddress());
        vo.setPullOverAddress(xpOrder.getGetoffAddress());
        if(null != xpOrder.getEstimatedArrivalTime()){
            vo.setEstimatedArrivalTime(xpOrder.getEstimatedArrivalTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
        if(null != xpOrder.getGetoffTime()){
            vo.setActualArrivalTime(xpOrder.getGetoffTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
        if(null == xpOrder.getMileageKilometers()){
            vo.setActualMileage(BigDecimal.valueOf(xpOrder.getMileageKilometers()));
        }
        vo.setTotalPrice(xpOrder.getOrderMoney());
        vo.setStartPrice(xpOrder.getStartMoney());
        vo.setWaitFee(xpOrder.getWaitMoney());
        vo.setMileageFee(xpOrder.getMileageMoney());
        vo.setDurationFee(xpOrder.getDurationMoney());
        vo.setLongDistanceFee(xpOrder.getLongDistancemoney());
        vo.setCouponDiscount(xpOrder.getCouponMoney());
        vo.setActualPayment(xpOrder.getPayMoney());
        XpVehicle xpVehicle = xpVehicleClient.queryById(xpOrder.getVehicleId()).getData();
        if(null != xpVehicle){
            vo.setVin(xpVehicle.getVehicleVin());
        }
        XpOrderCancel orderCancel = xpOrderCancelService.getOne(new QueryWrapper<XpOrderCancel>().eq("order_id", orderId));
        if(null != orderCancel){
            vo.setCancelReasonType(orderCancel.getCancelReason());
        }
        return vo;
    }
}