liujie
2026-04-18 b92a2bf0fdb56b51066c2d484f8880d1b05941de
Merge remote-tracking branch 'origin/1.0' into 1.0
9个文件已添加
34个文件已修改
2117 ■■■■■ 已修改文件
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpAppUserCouponController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/mapper/XpAppUserCouponMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/model/vo/XpAppUserCouponListVo.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/IXpAppUserCouponService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpAppUserCouponServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/resources/mapper/account/XpAppUserCouponMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/account/UcUserInfo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpActicityClientFallbackFactory.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpActicityClient.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/vehicleConvoy/BluetoothUnlockClient.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpWhitelist.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/query/account/XpAppUserCouponPageListQuery.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/automaticPilot/VehiclePosition.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/vehicleConvoy/BluetoothDeliverVo.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/service/KafkaConsumerService.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/controller/ActiveOrderController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/ActiveOrderServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpActicityController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpWhitelistController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/dto/RoutePlanAddDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/dto/RoutePlanEditDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/exportExcle/XpWhitelistExportExcel.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/query/WhitelistPageListQuery.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/IXpActicityService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpActicityServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpWhitelistServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/utils/TaskOrderUtil.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpWhitelistMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/utils/BluetoothUnlockTokenUtil.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/utils/TaskOrderCancelUtil.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketConfig.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketServer.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketServerConfig.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/controller/WebSocketController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/PushMessageWebSocketHandler.java 412 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/WebSocketAuthHandler.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/WebSocketTextFrameHandler.java 386 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/service/WebSocketUserConnectionManager.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpVehicleController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/util/BluetoothUnlockTokenUtil.java 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpAppUserCouponController.java
@@ -1,6 +1,7 @@
package com.xiaopeng.account.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.account.model.dto.ReceiveCouponDto;
import com.xiaopeng.account.model.dto.SendCouponDto;
@@ -11,19 +12,23 @@
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.XpActicityClient;
import com.xiaopeng.api.feignClient.operation.XpCouponClient;
import com.xiaopeng.api.feignClient.operation.XpOpenCityClient;
import com.xiaopeng.api.model.account.XpAppUserCoupon;
import com.xiaopeng.api.model.operation.XpCoupon;
import com.xiaopeng.api.query.account.XpAppUserCouponPageListQuery;
import com.xiaopeng.api.vo.account.NavigationCouponPickerVo;
import com.xiaopeng.api.vo.account.UserCouponVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.unifiedverification.interceptor.XpClientCheck;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.math.BigDecimal;
@@ -51,6 +56,9 @@
    private final UserResourcesClient userResourcesClient;
    private final XpCouponClient xpCouponClient;
    @Resource
    private XpActicityClient xpActicityClient;
    public XpAppUserCouponController(IXpAppUserCouponService xpAppUserCouponService, XpOpenCityClient xpOpenCityClient, UserResourcesClient userResourcesClient, XpCouponClient xpCouponClient) {
        this.xpAppUserCouponService = xpAppUserCouponService;
@@ -59,6 +67,18 @@
        this.xpCouponClient = xpCouponClient;
    }
    @PostMapping("/v1/api/appUserCoupon/pageList")
    @ApiOperation(value = "用户优惠券列表",tags = {"管理后台-用户优惠券列表"},response = XpAppUserCouponListVo.class)
    public R<PageInfo<XpAppUserCouponListVo>> pageList(@RequestBody XpAppUserCouponPageListQuery query) {
        if(StrUtil.isNotBlank(query.getActicityName()) ){
            List<String> acticityIds = xpActicityClient.findListToIds(query.getActicityName()).getData();
            query.setActicityIds(acticityIds);
        }
        PageInfo<XpAppUserCouponListVo> pageList = xpAppUserCouponService.pageList(query);
        return R.ok(pageList);
    }
    @PostMapping("/logan/inner/appUserCoupon/getUserCoupon")
    public R<UserCouponVo> getUserCoupon(@RequestBody GetUserCouponDto dto) {
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/mapper/XpAppUserCouponMapper.java
@@ -5,7 +5,11 @@
import com.xiaopeng.account.model.query.XpUserCouponListQuery;
import com.xiaopeng.account.model.vo.XpAppUserCouponListVo;
import com.xiaopeng.api.model.account.XpAppUserCoupon;
import com.xiaopeng.api.query.account.XpAppUserCouponPageListQuery;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -19,4 +23,7 @@
    Page<XpAppUserCouponListVo> userCouponList(@Param("page") Page<XpAppUserCouponListVo> page, @Param("query") XpUserCouponListQuery query, @Param("uid") Long uid);
    List<XpAppUserCouponListVo> pageList(@Param("pageInfo") PageInfo<XpAppUserCouponListVo> pageInfo, @Param("query") XpAppUserCouponPageListQuery query);
}
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/model/vo/XpAppUserCouponListVo.java
@@ -1,5 +1,7 @@
package com.xiaopeng.account.model.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -12,8 +14,14 @@
@ApiModel("用户优惠券列表Vo")
public class XpAppUserCouponListVo {
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;
    @ApiModelProperty(value = "Id")
    private String couponId;
    @ApiModelProperty(value = "用户id 根据手机号获取")
    private Long appUserId;
    @ApiModelProperty(value = "优惠券名称")
    private String title;
@@ -45,4 +53,8 @@
    @ApiModelProperty(value = "优惠券说明")
    private String couponDesc;
    @ApiModelProperty(value = "活动名称")
    private String acticityName;
}
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/IXpAppUserCouponService.java
@@ -4,12 +4,15 @@
import com.xiaopeng.account.model.dto.ReceiveCouponDto;
import com.xiaopeng.account.model.dto.SendCouponDto;
import com.xiaopeng.account.model.query.XpUserCouponListQuery;
import com.xiaopeng.account.model.vo.XpAppUserCouponListVo;
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.query.account.XpAppUserCouponPageListQuery;
import com.xiaopeng.api.vo.account.NavigationCouponPickerVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import java.util.List;
@@ -23,6 +26,8 @@
 */
public interface IXpAppUserCouponService extends IService<XpAppUserCoupon> {
    PageInfo<XpAppUserCouponListVo> pageList(XpAppUserCouponPageListQuery query);
    /**
     * 获取用户优惠券列表
     * @param query
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpAppUserCouponServiceImpl.java
@@ -5,7 +5,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.xiaopeng.account.mapper.XpAppUserCouponMapper;
import com.xiaopeng.account.model.dto.ReceiveCouponDto;
import com.xiaopeng.account.model.dto.SendCouponDto;
@@ -18,6 +17,7 @@
import com.xiaopeng.api.dto.account.NavigationCouponPickerRequest;
import com.xiaopeng.api.dto.account.UcUserInfo;
import com.xiaopeng.api.enums.NavigationCouponBubbleType;
import com.xiaopeng.api.query.account.XpAppUserCouponPageListQuery;
import com.xiaopeng.api.vo.account.NavigationCouponItemVo;
import com.xiaopeng.api.vo.account.NavigationCouponPickerVo;
import com.xiaopeng.api.feignClient.account.UserResourcesClient;
@@ -27,6 +27,7 @@
import com.xiaopeng.api.model.operation.XpCoupon;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.utils.DateUtils;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -63,6 +64,15 @@
    private UserResourcesClient userResourcesClient;
    @Override
    public PageInfo<XpAppUserCouponListVo> pageList(XpAppUserCouponPageListQuery query) {
        PageInfo<XpAppUserCouponListVo> pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize());
        List<XpAppUserCouponListVo> list = this.baseMapper.pageList(pageInfo,query);
        pageInfo.setRecords(list);
        return pageInfo;
    }
    /**
     * 获取用户优惠券列表
     *
xp-robotaxi-account-boot/src/main/resources/mapper/account/XpAppUserCouponMapper.xml
@@ -7,7 +7,27 @@
        id, full_money, money, expiration_time, status, coupon_type, app_user_id, coupon_id, activity_id, activity_type, is_deleted, create_time, update_time, create_by, update_by
    </sql>
    <select id="pageList" resultType="com.xiaopeng.account.model.vo.XpAppUserCouponListVo">
        select a.*
        from xp_app_user_coupon as a
        where a.is_deleted = 0 and a.app_user_id = #{appUserId}
        <if test="query.createTimeStart != null ">
            a.create_time >= #{query.createTimeStart}
        </if>
        <if test="query.createTimeEnd != null ">
            a.create_time &lt;= #{query.createTimeEnd}
        </if>
        <if test="query.status != null">
            and a.status = #{query.status}
        </if>
        <if test="query.acticityIds != null">
            and a.activity_id in
            <foreach item="item" collection="query.acticityIds" separator="," open="(" close=")" index="">
                #{item}
            </foreach>
        </if>
        order by a.create_time desc
    </select>
    <select id="userCouponList" resultType="com.xiaopeng.account.model.vo.XpAppUserCouponListVo">
        select t.*,t.status  from (
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/account/UcUserInfo.java
@@ -12,13 +12,13 @@
    private String appId;
    private String headImg;
    private Long id;
    private String mobile;
    private String nickName;
    private String mobile;//用户手机号
    private String nickName;//用户名称
    private List<String> orderList;
    private Integer orderType;
    private Integer state;
    private Integer type;
    private Long uid;
    private Long uid; //用户id
    private List<String> bindCars; // 空时为 null
    private Integer musicVip;      // 0-非音频会员,1-音频会员
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpActicityClientFallbackFactory.java
New file
@@ -0,0 +1,26 @@
package com.xiaopeng.api.factory.operation;
import com.xiaopeng.api.feignClient.operation.XpActicityClient;
import com.xiaopeng.common.core.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class XpActicityClientFallbackFactory implements FallbackFactory<XpActicityClient> {
    @Override
    public XpActicityClient create(Throwable cause) {
        return new XpActicityClient() {
            @Override
            public R<List<String>> findListToIds(String acticityName) {
                return R.ok();
            }
        };
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpActicityClient.java
New file
@@ -0,0 +1,22 @@
package com.xiaopeng.api.feignClient.operation;
import com.xiaopeng.api.factory.operation.XpCouponClientFallbackFactory;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient(contextId = "XpActicityClient",
        value = ServiceNameConstantsEnum.XP_OPERATION_SERVICE,
        fallbackFactory = XpCouponClientFallbackFactory.class)
public interface XpActicityClient {
    @PostMapping("/logan/inner/acticity/findListToIds")
    R<List<String>> findListToIds(@RequestParam("acticityName") String acticityName);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/vehicleConvoy/BluetoothUnlockClient.java
New file
@@ -0,0 +1,37 @@
package com.xiaopeng.api.feignClient.vehicleConvoy;
import com.xiaopeng.api.dto.vehicleConvoy.BaseVehicleUserDto;
import com.xiaopeng.api.factory.vehicleConvoy.UserVehicleClientFallbackFactory;
import com.xiaopeng.api.vo.vehicleConvoy.BluetoothDeliverVo;
import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
 * 蓝牙解锁车门
 */
// todo 需要修改蓝牙微服务调用名
@FeignClient(contextId = "BluetoothUnlockClient",
        value = ServiceNameConstantsEnum.XP_VEHICLE_USER_SERVICE,
        fallbackFactory = UserVehicleClientFallbackFactory.class)
public interface BluetoothUnlockClient {
    /**
     * 下发凭证
     * @param vo
     */
    @PostMapping("/logan/rt/credential/deliver")
    BaseVehicleUserDto deliver(@RequestBody BluetoothDeliverVo vo);
    /**
     * 删除凭证
     * @param vo
     */
    @PostMapping("/logan/rt/credential/delete")
    BaseVehicleUserDto delete(@RequestBody BluetoothDeliverVo vo);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpWhitelist.java
@@ -60,9 +60,15 @@
    @Max(value = 99999, message = "最大不能超过99999")
    private Integer maxOrderNum;
    @ApiModelProperty(value = "用户id 根据手机号获取")
    private Long appUserId;
    public Boolean getStatus() {
        if (LocalDate.now().isAfter(expireTime)) {
            return false;
        if(expireTime!= null){
            if (LocalDate.now().isAfter(expireTime)) {
                return false;
            }
            return status;
        }
        return status;
    }
xp-robotaxi-api/src/main/java/com/xiaopeng/api/query/account/XpAppUserCouponPageListQuery.java
New file
@@ -0,0 +1,35 @@
package com.xiaopeng.api.query.account;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xiaopeng.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class XpAppUserCouponPageListQuery extends BasePage {
    @ApiModelProperty(value = "用户id 根据手机号获取")
    private Long appUserId;
    @ApiModelProperty(value = "状态 1-未使用 2-已使用 3-已过期 0待领取")
    private Integer status;
    @ApiModelProperty(value = "活动名称")
    private String acticityName;
    @ApiModelProperty(value = "领取时间开始")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDateTime createTimeStart;
    @ApiModelProperty(value = "领取时间结束")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDateTime createTimeEnd;
    private List<String> acticityIds;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/automaticPilot/VehiclePosition.java
@@ -6,6 +6,8 @@
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
@@ -32,6 +34,7 @@
    /**
     * 车辆实时位置信息
     */
    @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
    private GeoJsonPoint point;
    /**
     * 航向,单位:度,北零顺时针。范围:[0-360)。0 => 北, 90 => 东, 180 => 南, 270 => 西
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/vehicleConvoy/BluetoothDeliverVo.java
New file
@@ -0,0 +1,20 @@
package com.xiaopeng.api.vo.vehicleConvoy;
import lombok.Data;
@Data
public class BluetoothDeliverVo {
    /**
     * VIM码
     */
    private String vim;
    /**
     * 乘客用户 ID
     */
    private Long passengerUid;
    /**
     * 鉴权凭证
     */
    private String credential;
}
xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -56,4 +56,5 @@
com.xiaopeng.api.factory.map.XpStationClientFallbackFactory
com.xiaopeng.api.factory.pushMessage.SubscribeMessageClientFallbackFactory
com.xiaopeng.api.factory.account.UserSubscribeMessageClientFallbackFactory
com.xiaopeng.api.factory.operation.XpTradingStrategyClientFallbackFactory
com.xiaopeng.api.factory.operation.XpTradingStrategyClientFallbackFactory
com.xiaopeng.api.factory.operation.XpActicityClientFallbackFactory
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/service/KafkaConsumerService.java
@@ -6,9 +6,7 @@
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.xiaopeng.api.dto.automaticPilot.MqttPublishDto;
import com.xiaopeng.api.dto.automaticPilot.PushOrderDto;
import com.xiaopeng.api.dto.automaticPilot.VehicleEventDTO;
import com.xiaopeng.api.dto.map.HistoryPointDto;
import com.xiaopeng.api.dto.map.OrderRouteAndFareDto;
import com.xiaopeng.api.dto.operation.CruisePointDto;
import com.xiaopeng.api.dto.operation.LonLatDto;
@@ -50,7 +48,6 @@
import com.xiaopeng.common.redis.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.geo.Point;
@@ -63,9 +60,10 @@
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
@@ -556,12 +554,12 @@
                    reassignOrderDTO.setUid(xpOrder.getAppUserId().toString());
                    reassignOrderDTO.setOrderNo(xpOrder.getOrderNumber());
                    reassignOrderDTO.setCount(10);
                    RealtimePose pose = systemRuntimeStatus.getPose();
                    Wgs naviPose = pose.getNaviPose();
                    Double latitude = naviPose.getLatitude();
                    Double longitude = naviPose.getLongitude();
                    reassignOrderDTO.setLongitude(longitude);
                    reassignOrderDTO.setLatitude(latitude);
//                    RealtimePose pose = systemRuntimeStatus.getPose();
//                    Wgs naviPose = pose.getNaviPose();
//                    Double latitude = naviPose.getLatitude();
//                    Double longitude = naviPose.getLongitude();
                    reassignOrderDTO.setLongitude(xpOrder.getStartLon());
                    reassignOrderDTO.setLatitude(xpOrder.getStartLat());
                    reassignOrderClient.reassignOrder(reassignOrderDTO);
                }
            }
@@ -1343,8 +1341,22 @@
                                buildPushOrderStatus(cmd, orderId, OrderStatusEnum.SEE_OFF_GUESTS, sequence);
                                xpOrderClient.updateOrderStatus(Long.parseLong(orderId), OrderStatusEnum.SEE_OFF_GUESTS.getCode(), true);
                                redisService.setCacheObject("ORDER_STATUS:" + orderId, OrderStatusEnum.SEE_OFF_GUESTS.getCode(), 24L, TimeUnit.HOURS);
                                // todo 判断当前订单的车辆是否有连环派单,然后判断是都还满足连环派单的条件,不满足则需要重新派单
                                // 判断当前订单的车辆是否有连环派单,然后判断是都还满足连环派单的条件,不满足则需要重新派单
                                Long nextOrderId = redisService.getCacheObject(CacheConstants.nextOrder(orderId));
                                if(null != nextOrderId){
                                    XpOrder nextOrder = xpOrderClient.queryOrderById(nextOrderId).getData();
                                    if(null != nextOrder && nextOrder.getStatus() == OrderStatusEnum.PICK_UP.getCode()){
                                        redisService.setCacheObject(CacheConstants.nextOrder(orderId),null);
                                        ReassignOrderDTO reassignOrderDTO = new ReassignOrderDTO();
                                        reassignOrderDTO.setUid(xpOrder.getAppUserId().toString());
                                        reassignOrderDTO.setOrderNo(xpOrder.getOrderNumber());
                                        reassignOrderDTO.setCount(10);
                                        reassignOrderDTO.setLongitude(xpOrder.getStartLon());
                                        reassignOrderDTO.setLatitude(xpOrder.getStartLon());
                                        reassignOrderClient.reassignOrder(reassignOrderDTO);
                                    }
                                }
                                redisService.getCacheObject("ORDER_CONNECT_ORDER:" + orderId);
                            }else{
                                buildPushOrderStatus(cmd, orderId, OrderStatusEnum.CANCELED, sequence);
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/controller/ActiveOrderController.java
@@ -2,6 +2,7 @@
import com.xiaopeng.api.dto.map.RouteAndFareDto;
import com.xiaopeng.api.dto.order.OrderChangeDto;
import com.xiaopeng.api.vo.automaticPilot.VehiclePosition;
import com.xiaopeng.api.vo.operation.RouteAndFareVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.itinerary.service.ActiveOrderService;
@@ -10,11 +11,14 @@
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@@ -73,6 +77,26 @@
        return R.ok();
    }
    /**
     * 周围车辆点位查询
     */
    @ApiOperation(value = "周围车辆点位查询", notes = "", tags = {"小程序-周围车辆点位查询"})
    @GetMapping("/api/v1/mini-program/nearby/cars")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<List<VehiclePosition>> carAround(@RequestParam double lon,@RequestParam double lat,@RequestParam double distanceMeters,HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        Point point = new Point(lon, lat);
        Distance distance = new Distance(distanceMeters / 1000.0, Metrics.KILOMETERS);
        Criteria criteria = Criteria.where("point")
                .nearSphere(point)
                .maxDistance(distance.getNormalizedValue());
        Query query = new Query(criteria);
        return R.ok(mongoTemplate.find(query, VehiclePosition.class));
    }
    @Resource
    private MongoTemplate mongoTemplate;
}
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/ActiveOrderServiceImpl.java
@@ -2,18 +2,23 @@
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.dto.operation.ReassignOrderDTO;
import com.xiaopeng.api.dto.order.OrderChangeDto;
import com.xiaopeng.api.feignClient.map.XpMapClient;
import com.xiaopeng.api.feignClient.operation.ReassignOrderClient;
import com.xiaopeng.api.feignClient.operation.XpPointClient;
import com.xiaopeng.api.feignClient.operation.XpSystemSettingClient;
import com.xiaopeng.api.feignClient.order.XpOrderClient;
import com.xiaopeng.api.model.operation.*;
import com.xiaopeng.api.model.operation.XpElectronicFence;
import com.xiaopeng.api.model.operation.XpElectronicFenceArea;
import com.xiaopeng.api.model.operation.XpPoint;
import com.xiaopeng.api.model.operation.XpSystemSetting;
import com.xiaopeng.api.model.order.XpOrder;
import com.xiaopeng.api.vo.operation.RouteOptionVo;
import com.xiaopeng.api.vo.operation.RouteVo;
import com.xiaopeng.common.core.constant.CacheConstants;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.enums.OrderStatusEnum;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.redis.service.RedisService;
import com.xiaopeng.itinerary.constant.I18nKeyConstant;
@@ -32,7 +37,7 @@
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
@@ -183,10 +188,24 @@
        xpOrder.setQueueName(key);
        xpOrderClient.updateOrder(xpOrder);
        // todo 判断当前订单的车辆是否有连环派单,然后判断是都还满足连环派单的条件,不满足则需要重新派单
        // 判断当前订单的车辆是否有连环派单,然后判断是都还满足连环派单的条件,不满足则需要重新派单
        Long nextOrderId = redisService.getCacheObject(CacheConstants.nextOrder(order.getId().toString()));
        if(null != nextOrderId){
            XpOrder nextOrder = xpOrderClient.queryOrderById(nextOrderId).getData();
            if(null != nextOrder && nextOrder.getStatus() == OrderStatusEnum.PICK_UP.getCode()){
                redisService.setCacheObject(CacheConstants.nextOrder(order.getId().toString()),null);
                ReassignOrderDTO reassignOrderDTO = new ReassignOrderDTO();
                reassignOrderDTO.setUid(xpOrder.getAppUserId().toString());
                reassignOrderDTO.setOrderNo(xpOrder.getOrderNumber());
                reassignOrderDTO.setCount(10);
                reassignOrderDTO.setLongitude(xpOrder.getStartLon());
                reassignOrderDTO.setLatitude(xpOrder.getStartLon());
                reassignOrderClient.reassignOrder(reassignOrderDTO);
            }
        }
    }
    @Resource
    private ReassignOrderClient reassignOrderClient;
    /**
     * 获取距离和时间
     * @param uid
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpActicityController.java
@@ -1,9 +1,17 @@
package com.xiaopeng.operation.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.operation.service.IXpActicityService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
import java.util.List;
/**
 * <p>
@@ -13,8 +21,24 @@
 * @author mitao
 * @since 2026-02-03
 */
@Controller
@RequestMapping("/xp-acticity")
@Api(tags = "活动管理")
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class XpActicityController {
    @Resource
    private IXpActicityService xpActicityService;
    /**
     * 获取所有白名单手机号
     * @return
     */
    @PostMapping("/logan/inner/acticity/findListToIds")
    @ApiOperation(value = "获取活动信息ids", tags = {"管理后台-获取活动信息ids"})
    public R<List<String>> findListToIds(@RequestParam String acticityName){
        if(acticityName == null){
            return R.ok();
        }
        return R.ok(xpActicityService.findListToIds(acticityName));
    }
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpWhitelistController.java
@@ -82,6 +82,14 @@
        return R.ok(xpWhitelistService.getAllWhiteList());
    }
    @ApiOperation(value = "白名单管理详情", tags = {"管理后台-白名单管理详情"})
    @GetMapping("/api/v1/whitelist/detail/{id}")
    public R<XpWhitelist> detail(@PathVariable("id") Long id) {
        return R.ok(xpWhitelistService.getById(id));
    }
    @ApiOperation(value = "白名单管理分页列表", response = XpWhitelist.class)
    @PostMapping("/api/v1/whitelist/pageList")
    public R<PageInfo<XpWhitelist>> pageList(@RequestBody WhitelistPageListQuery query) {
@@ -412,6 +420,9 @@
            xpWhitelistExportExcel.setNickName(xpWhitelist.getNickName());
            xpWhitelistExportExcel.setStatus(xpWhitelist.getStatus());
            xpWhitelistExportExcel.setIsPriorityDispatch(xpWhitelist.getIsPriorityDispatch());
            xpWhitelistExportExcel.setExpireTime(xpWhitelist.getExpireTime().toString());
            xpWhitelistExportExcel.setMaxOrderNum(xpWhitelist.getMaxOrderNum());
            xpWhitelistExportExcel.setCreateBy(xpWhitelist.getCreateBy());
            LocalDateTime createTime = xpWhitelist.getCreateTime();
            // 将LocalDateTime转化为yyyy-MM-dd HH:mm:ss格式字符串
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/dto/RoutePlanAddDTO.java
@@ -6,6 +6,7 @@
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
 * <p>
@@ -21,9 +22,11 @@
    @ApiModelProperty(value = "名称", required = true)
    @NotBlank(message = "名称不能为空")
    @Size(max = 50, message = "名称长度不能超过50")
    private String planName;
    @ApiModelProperty(value = "备注", required = true)
    @Size(max = 200, message = "备注长度不能超过200")
    private String remarks;
    @ApiModelProperty(value = "路线规划规则", required = true)
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/dto/RoutePlanEditDTO.java
@@ -6,6 +6,7 @@
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
 * <p>
@@ -29,9 +30,11 @@
    @ApiModelProperty(value = "名称", required = true)
    @NotBlank(message = "名称不能为空")
    @Size(max = 50, message = "名称长度不能超过50")
    private String planName;
    @ApiModelProperty(value = "备注", required = true)
    @Size(max = 200, message = "备注长度不能超过200")
    private String remarks;
    @ApiModelProperty(value = "路线规划规则", required = true)
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/exportExcle/XpWhitelistExportExcel.java
@@ -1,6 +1,6 @@
package com.xiaopeng.operation.exportExcle;
import com.xiaopeng.common.core.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -39,9 +39,16 @@
    @ApiModelProperty(value = "优先派单 1=是 0=否")
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "优先派单",width = 20,replace = {"是_true","否_false"})
    private Boolean isPriorityDispatch;
    @ApiModelProperty(value = "有限期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "有限期",width = 20)
    private String expireTime;
    @ApiModelProperty(value = "最大下单数量")
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "最大下单数量",width = 20)
    private Integer maxOrderNum;
    @ApiModelProperty(value = "记录创建人,前端忽略")
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "创建人",width = 20)
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/query/WhitelistPageListQuery.java
@@ -19,4 +19,7 @@
    @ApiModelProperty(value = "选中的记录id集合")
    private List<Long> ids;
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/IXpActicityService.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.api.model.operation.XpActicity;
import java.util.List;
/**
 * <p>
 * 活动管理 服务类
@@ -13,4 +15,6 @@
 */
public interface IXpActicityService extends IService<XpActicity> {
    List<String> findListToIds(String acticityName);
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpActicityServiceImpl.java
@@ -1,10 +1,13 @@
package com.xiaopeng.operation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.model.operation.XpActicity;
import com.xiaopeng.operation.mapper.XpActicityMapper;
import com.xiaopeng.operation.service.IXpActicityService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * <p>
@@ -17,4 +20,15 @@
@Service
public class XpActicityServiceImpl extends ServiceImpl<XpActicityMapper, XpActicity> implements IXpActicityService {
    @Override
    public List<String> findListToIds(String acticityName){
        LambdaQueryWrapper<XpActicity > query = new LambdaQueryWrapper<>();
        query.select(XpActicity::getId);
        query.eq(XpActicity::getDeleted, 0);
        query.like(XpActicity::getActicityName, acticityName);
        return listObjs(query, Object::toString);
    }
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpWhitelistServiceImpl.java
@@ -2,6 +2,9 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.PhoneUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.dto.account.UcUserInfo;
import com.xiaopeng.api.feignClient.account.UserResourcesClient;
import com.xiaopeng.api.model.account.SysUser;
import com.xiaopeng.api.model.operation.XpWhitelist;
import com.xiaopeng.api.model.operation.XpWhitelistOperlog;
@@ -15,7 +18,6 @@
import com.xiaopeng.operation.mapper.XpWhitelistOperlogMapper;
import com.xiaopeng.operation.query.WhitelistPageListQuery;
import com.xiaopeng.operation.service.IXpWhitelistService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.beans.factory.annotation.Autowired;
@@ -24,15 +26,7 @@
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -48,6 +42,10 @@
    @Autowired
    private XpWhitelistOperlogMapper xpWhitelistOperlogMapper;
    @Autowired
    private UserResourcesClient userResourcesClient;
    @Override
    public PageInfo<XpWhitelist> pageList(WhitelistPageListQuery query) {
        PageInfo<XpWhitelist> pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize());
@@ -60,6 +58,15 @@
    public List<XpWhitelist> exportList(WhitelistPageListQuery query) {
        List<XpWhitelist> list = this.baseMapper.exportList(query);
        return list;
    }
    //根据用户账号 通过接口获取用户id
    public UcUserInfo getUserByAccount (String account){
        UcUserInfo userByMobile = userResourcesClient.getUserByMobile(account);
        if(userByMobile == null){
            throw new ServiceException(account+"未获取到用户信息");
        }
        return userByMobile;
    }
    @Override
@@ -83,6 +90,7 @@
        Map<String, Long> reasonCountMap = new HashMap<>();
        Map<String, LinkedHashSet<String>> reasonSampleMap = new HashMap<>();
        Map<String, UcUserInfo> userMap = new HashMap<>();
        Set<String> seenAccounts = new HashSet<>();
        List<String> candidates = new ArrayList<>();
        for (String account : accounts) {
@@ -94,7 +102,12 @@
                recordFail(reasonCountMap, reasonSampleMap, "文件内重复", account, sampleLimitPerReason);
                continue;
            }
            candidates.add(account);
            //根据用户账号 通过接口获取用户id
            UcUserInfo userByAccount = this.getUserByAccount(account);
            if(userByAccount != null){
                candidates.add(account);
                userMap.put(account, userByAccount);
            }
        }
        if (CollUtil.isEmpty(candidates)) {
            return R.fail("所有帐号添加失败,请检查帐号格式或唯一性");
@@ -120,6 +133,8 @@
            }
            XpWhitelist xpWhitelist = new XpWhitelist();
            xpWhitelist.setAccount(account);
            xpWhitelist.setNickName(userMap.get(account).getNickName());
            xpWhitelist.setAppUserId(userMap.get(account).getUid());
            xpWhitelist.setStatus(true);
            xpWhitelist.setIsPriorityDispatch(dto.getIsPriorityDispatch());
            xpWhitelist.setWhitelistSource(1);
@@ -240,6 +255,7 @@
        if (!PhoneUtil.isMobile(dto.getAccount())) {
            throw new ServiceException("以下帐号格式不正确:", I18nKeyConstant.ACCOUNT_FORMAT_ERROR, new Object[]{dto.getAccount()});
        }
        UcUserInfo userByAccount = this.getUserByAccount(dto.getAccount());
        XpWhitelist old = this.lambdaQuery()
                .eq(XpWhitelist::getAccount, dto.getAccount())
                .ne(XpWhitelist::getId, dto.getId())
@@ -249,13 +265,15 @@
        }
        XpWhitelist whitelist = this.getById(dto.getId());
        whitelist.setAppUserId(userByAccount.getUid());
        StringBuilder operationContent = new StringBuilder("编辑账号:");
        StringBuilder accountBuilder = new StringBuilder();
        if (!whitelist.getAccount().equals(dto.getAccount())) {
            accountBuilder.append("白名单账号" + "\"" + whitelist.getAccount() + "\"" + "为" + "\"" + dto.getAccount() + "\"");
        }
        if (StringUtils.hasLength(whitelist.getNickName()) || StringUtils.hasLength(dto.getNickName())) {
            if (!StringUtils.hasLength(whitelist.getNickName())) {
            if (StringUtils.hasLength(whitelist.getNickName())) {
                if (!whitelist.getNickName().equals(dto.getNickName())) {
                    accountBuilder.append("用户名称" + "\"" + whitelist.getNickName() + "\"" + "为" + "\"" + dto.getNickName() + "\"");
                }
@@ -271,6 +289,15 @@
        if (whitelist.getIsPriorityDispatch() != dto.getIsPriorityDispatch()) {
            accountBuilder.append("优先派单" + "\"" + (whitelist.getIsPriorityDispatch() ? "是" : "否") + "\"" + "为" + "\"" + (dto.getIsPriorityDispatch() ? "是" : "否") + "\"");
        }
        if ( !dto.getMaxOrderNum().equals(whitelist.getMaxOrderNum()) ) {
            accountBuilder.append("最大下单数量" + "\"" + whitelist.getMaxOrderNum() + "\"" + "为" + "\"" + dto.getMaxOrderNum() + "\"");
            whitelist.setMaxOrderNum(dto.getMaxOrderNum());
        }
        if (!whitelist.getExpireTime().isEqual(dto.getExpireTime())) {
            accountBuilder.append("有限期" + "\"" + whitelist.getExpireTime() + "\"" + "为" + "\"" + dto.getExpireTime() + "\"");
            whitelist.setExpireTime(dto.getExpireTime());
        }
        XpWhitelistOperlog xpWhitelistOperlog = new XpWhitelistOperlog();
        xpWhitelistOperlog.setOperationName(sysUser.getNickName());
        xpWhitelistOperlog.setOperationAccount(sysUser.getPhoneNumber());
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/utils/TaskOrderUtil.java
@@ -1089,11 +1089,11 @@
            log.info("没有找到空闲车辆 {},尝试连环派单给即将服务结束的车辆", orderNo);
            // 查询状态为1的车辆vin
            R<List<String>> servingVehicles = xpVehicleClient.queryVehiclePositionByVin(new VehiclePositionByVinDto(xpElectronicFence.getId(), vinList, xpTradingStrategy.getSimultaneouslyOrderCount(),1));
            if (vehiclePositionVos.getCode() != 200) {
            if (servingVehicles.getCode() != 200) {
                continue;
            }
            LonLatDto current = new LonLatDto(Double.parseDouble(split[0]), Double.parseDouble(split[1]));//当前点位
            LonLatDto userPostion = new LonLatDto(Double.parseDouble(split[0]), Double.parseDouble(split[1]));
            XpTradingStrategyDetailVO strategyDetailVO = tradingStrategyClient.queryById().getData();
@@ -1114,21 +1114,15 @@
                    .map(R::getData)
                    .filter(o -> {
                        XpVehicle xpVehicle = xpVehicleClient.queryById(o.getVehicleId()).getData();
                        Object cacheObject = redisService.getCacheObject(CacheConstants.navigationEnd(o.getAppUserId().toString()));
                        RouteVo routeRemain = JSON.parseObject(cacheObject.toString(), RouteVo.class);
                        if (routeRemain.getDuration() / 60 > timeRegardAsEnding) {
                            return false;
                        }
                        RouteAndFareDto remain = new RouteAndFareDto();
                        remain.setOrigin(current);
                        LonLatDto ll = new LonLatDto(o.getStartLon(), o.getStartLat());
                        remain.setDestination(ll);
                        RouteVo r = xpMapClient.getDistanceAndDuration(remain).getData();
                        if (r.getDistance() > distanceRegardAsEnding) {
                        if (routeRemain.getDistance() > distanceRegardAsEnding) {
                            return false;
                        }
                        if (r.getDuration() / 60 > timeRegardAsEnding) {
                        if (routeRemain.getDuration() / 60 > timeRegardAsEnding) {
                            return false;
                        }
                        RouteAndFareDto next = new RouteAndFareDto();
@@ -1137,7 +1131,7 @@
                        RouteVo route = xpMapClient.getDistanceAndDuration(next).getData();
                        if (route.getDistance() > backupPower + xpVehicle.getMileageRange())
                            return false;
                        return r.getDistance() < destToPassenger
                        return routeRemain.getDistance() < destToPassenger
                                || now.plusSeconds(routeRemain.getDuration())
                                .plusMinutes(endingTimeToUseTime)
                                .isBefore(rideTime);
@@ -1150,7 +1144,7 @@
                XpVehicle xpVehicle = xpVehicleClient.queryById(luckyOder.getVehicleId()).getData();
                // 设置路线信息
                RouteAndFareDto routeAndFareDto = new RouteAndFareDto();
                routeAndFareDto.setDestination(current);
                routeAndFareDto.setDestination(userPostion);
                routeAndFareDto.setOrigin(new LonLatDto(luckyOder.getEndLon(),luckyOder.getEndLat()));
                RouteVo routeToOrigin = xpMapClient.getDistanceAndDuration(routeAndFareDto).getData();
                Object cacheObject = redisService.getCacheObject(CacheConstants.navigationEnd(luckyOder.getAppUserId().toString()));
@@ -1166,6 +1160,7 @@
                redisService.setCacheObject(CacheConstants.navigationStart(xpOrder.getAppUserId().toString()), routeToOrigin);
                redisService.removeByPattern(quoteName,xpOrder.getOrderNumber());
                log.info("将订单 {} 从队列 {} 弹出去", orderNo, quoteName);
                break;
            }
        }
        return false;
@@ -1236,7 +1231,81 @@
            // 获取车辆的区域信息
            if (positionList.isEmpty()) {
                continue;
                log.info("没有找到空闲车辆 {},尝试连环派单给即将服务结束的车辆", orderNo);
                // 查询状态为1的车辆vin
                Set<String> vinList = positionList.stream().map(VehiclePosition::getVin).collect(Collectors.toSet());
                R<List<String>> servingVehicles = xpVehicleClient.queryVehiclePositionByVin(new VehiclePositionByVinDto(data.getFleetId(), vinList, xpTradingStrategy.getSimultaneouslyOrderCount(),1));
                if (servingVehicles.getCode() != 200) {
                    continue;
                }
                LonLatDto userPosition = new LonLatDto(data.getStartLon(), data.getStartLat());//人的位置
                XpTradingStrategyDetailVO strategyDetailVO = tradingStrategyClient.queryById().getData();
                Integer distanceRegardAsEnding = strategyDetailVO.getDistanceRegardAsEnding();
                Integer timeRegardAsEnding = strategyDetailVO.getTimeRegardAsEnding();
                Integer backupPower = strategyDetailVO.getBackupPower();
                Integer destToPassenger = strategyDetailVO.getDestToPassenger();
                Integer endingTimeToUseTime = strategyDetailVO.getEndingTimeToUseTime();
                LocalDateTime rideTime = data.getRideTime();
                LocalDateTime now = LocalDateTime.now();
                Optional<XpOrder> luckyOrderOpt = servingVehicles.getData().stream()
                        //获取位置
                        .map(vin -> mongoTemplate.findOne(new Query(Criteria.where("vin").is(vin)), VehiclePosition.class))
                        .map(VehiclePosition::getOrderId)
                        .map(xpOrderClient::queryOrderById)
                        .filter(R::isSuccess)
                        .map(R::getData)
                        .filter(o -> {
                            XpVehicle xpVehicle = xpVehicleClient.queryById(o.getVehicleId()).getData();
                            Object cacheObject = redisService.getCacheObject(CacheConstants.navigationEnd(o.getAppUserId().toString()));
                            RouteVo routeRemain = JSON.parseObject(cacheObject.toString(), RouteVo.class);
                            if (routeRemain.getDuration() / 60 > timeRegardAsEnding) {
                                return false;
                            }
                            if (routeRemain.getDistance() > distanceRegardAsEnding) {
                                return false;
                            }
                            if (routeRemain.getDuration() / 60 > timeRegardAsEnding) {
                                return false;
                            }
                            RouteAndFareDto next = new RouteAndFareDto();
                            next.setOrigin(new LonLatDto(o.getStartLon(),o.getStartLat()));
                            next.setDestination(new LonLatDto(o.getEndLon(),o.getEndLat()));
                            RouteVo route = xpMapClient.getDistanceAndDuration(next).getData();
                            if (route.getDistance() > backupPower + xpVehicle.getMileageRange())
                                return false;
                            return routeRemain.getDistance() < destToPassenger
                                    || now.plusSeconds(routeRemain.getDuration())
                                    .plusMinutes(endingTimeToUseTime)
                                    .isBefore(rideTime);
                        })
                        .findAny();
                if (luckyOrderOpt.isPresent()) {
                    XpOrder luckyOder = luckyOrderOpt.get();
                    log.info("有空闲车辆 {} 订单 {}", luckyOder.getOrderNumber(), orderNo);
                    XpVehicle xpVehicle = xpVehicleClient.queryById(luckyOder.getVehicleId()).getData();
                    // 设置路线信息
                    RouteAndFareDto pickUpRoute = new RouteAndFareDto();
                    pickUpRoute.setDestination(userPosition);
                    pickUpRoute.setOrigin(new LonLatDto(luckyOder.getEndLon(),luckyOder.getEndLat()));
                    RouteVo routeToOrigin = xpMapClient.getDistanceAndDuration(pickUpRoute).getData();
                    Object cacheObject = redisService.getCacheObject(CacheConstants.navigationEnd(luckyOder.getAppUserId().toString()));
                    RouteVo routeRemain = JSON.parseObject(cacheObject.toString(), RouteVo.class);
                    routeToOrigin.getPolyline().addAll(0, routeRemain.getPolyline());
                    routeToOrigin.setDuration(routeToOrigin.getDuration() + routeRemain.getDuration());
                    routeToOrigin.setDistance(routeToOrigin.getDistance() + routeRemain.getDistance());
                    PushOrderDto pushOrderDto = buildPushOrderDto(data);
                    pushOrderDto.setVin(xpVehicle.getVehicleVin());
                    // 订单流转
                    orderMissionClient.pushOrder(pushOrderDto);
                    redisService.setCacheObject(CacheConstants.nextOrder(luckyOder.getId().toString()), data.getId());
                    redisService.setCacheObject(CacheConstants.navigationStart(data.getAppUserId().toString()), routeToOrigin);
                    break;
                }
            }
            // 直接派单  定义一个锁来锁定车辆
xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpWhitelistMapper.xml
@@ -4,11 +4,12 @@
    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, nick_name, account, status, is_priority_dispatch, whitelist_source,expire_time, is_deleted, create_time, update_time, create_by, update_by
        id, nick_name, account, status, is_priority_dispatch, whitelist_source,expire_time,max_order_num, is_deleted, create_time, update_time, create_by, update_by
    </sql>
    <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpWhitelist">
        select
            id,nick_name as nickName,account,status,is_priority_dispatch as isPriorityDispatch,whitelist_source as whitelistSource,expire_time as expireTime
            id,nick_name as nickName,account,status,is_priority_dispatch as isPriorityDispatch,whitelist_source as whitelistSource,
            expire_time as expireTime,max_order_num as maxOrderNum
        ,create_time as createTime,update_time as updateTime,create_by as createBy,update_by as updateBy
        from xp_whitelist
        where is_deleted = 0
@@ -25,7 +26,8 @@
        order by create_time desc
    </select>
    <select id="exportList" resultType="com.xiaopeng.api.model.operation.XpWhitelist">
        select id,nick_name as nickName,account,status,is_priority_dispatch as isPriorityDispatch,whitelist_source as whitelistSource,expire_time as expireTime
        select id,nick_name as nickName,account,status,is_priority_dispatch as isPriorityDispatch,whitelist_source as whitelistSource,
        expire_time as expireTime,max_order_num as maxOrderNum
        ,create_time as createTime,update_time as updateTime,create_by as createBy,update_by as updateBy
        from xp_whitelist
        where is_deleted = 0
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java
@@ -19,10 +19,13 @@
import com.xiaopeng.api.vo.operation.RouteVo;
import com.xiaopeng.api.vo.order.*;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.utils.StringUtils;
import com.xiaopeng.common.core.utils.WebUtils;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.common.redis.service.RedisService;
import com.xiaopeng.order.mangodb.model.OrderTrajectory;
import com.xiaopeng.order.mangodb.service.OrderTrajectoryService;
import com.xiaopeng.order.model.dto.XpOrderCommentDto;
import com.xiaopeng.order.model.dto.XpOrderOperateDTO;
import com.xiaopeng.order.model.dto.XpOrderPayDto;
@@ -62,6 +65,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
 * <p>
@@ -82,6 +86,7 @@
    private MongoTemplate mongoTemplate;
    @Resource
    private XpVehicleClient xpVehicleClient;
    private OrderTrajectoryService orderTrajectoryService;
    /**
     * 获取订单统计数据(内部接口)
@@ -206,7 +211,7 @@
     */
    @ApiOperation(value = "订单分页列表", tags = {"管理后台-交易管理-订单管理"})
    @PostMapping("/api/v1/order/pageList")
    public R<PageInfo<XpOrderPageVO>> pageList(@Validated @RequestBody XpOrderPageQuery query) {
    public R<PageInfo<XpOrderPageVO>> pageList( @RequestBody XpOrderPageQuery query) {
        PageInfo<XpOrderPageVO> pageInfo = xpOrderService.pageList(query);
        return R.ok(pageInfo);
    }
@@ -346,9 +351,9 @@
        String uid = request.getHeader("XP-Uid");
        OrderDetailVo orderDetailVo = xpOrderService.orderDetail(orderId, uid);
        BigDecimal orderMoney = orderDetailVo.getOrderMoney();
        if(orderMoney==null){
        if (orderMoney == null) {
            orderDetailVo.setOrderMoneyStr("0.00");
        }else {
        } else {
            orderDetailVo.setOrderMoneyStr(orderMoney.setScale(2, RoundingMode.HALF_UP).toString());
        }
        return R.ok(orderDetailVo);
@@ -361,10 +366,10 @@
    @ApiOperation(value = "获取历史下单成功起点10条数据列表", tags = {"小程序-获取历史下单成功起点10条数据列表"})
    @GetMapping("/base/v1/mini-program/history/startPoints")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<List<XpHistoryStartPointsVo>> getHistoryStartPoints(double lon, double lat,Long electronicAreaId, HttpServletRequest request) {
    public R<List<XpHistoryStartPointsVo>> getHistoryStartPoints(double lon, double lat, Long electronicAreaId, HttpServletRequest request) {
        // uid已通过拦截器otp校验,可以直接使用
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpOrderService.getHistoryStartPoints(uid, lon, lat,electronicAreaId));
        return R.ok(xpOrderService.getHistoryStartPoints(uid, lon, lat, electronicAreaId));
    }
    /**
@@ -373,10 +378,10 @@
    @ApiOperation(value = "获取历史下单成功终点10条数据列表", tags = {"小程序-获取历史下单成功终点10条数据列表"})
    @GetMapping("/base/v1/mini-program/history/endPoints")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<List<XpHistoryEndPointsVo>> getHistoryEndPoints(double lon, double lat,Long electronicAreaId, HttpServletRequest request) {
    public R<List<XpHistoryEndPointsVo>> getHistoryEndPoints(double lon, double lat, Long electronicAreaId, HttpServletRequest request) {
        // uid已通过拦截器otp校验,可以直接使用
        String uid = request.getHeader("XP-Uid");
        return R.ok(xpOrderService.getHistoryEndPoints(uid, lon, lat,electronicAreaId));
        return R.ok(xpOrderService.getHistoryEndPoints(uid, lon, lat, electronicAreaId));
    }
@@ -464,7 +469,6 @@
    }
    /**
     * +
     * 获取行程费用明细
@@ -532,7 +536,7 @@
     */
    @ApiOperation(value = "行驶中车辆异常,订单状态修改- 修改状态=7 再修改 金额-再将状态修改成8")
    @PostMapping("/logan/inner/v1/order/carAbnormalUpdateOrderStatus")
    public R<String> carAbnormalUpdateOrderStatus(@RequestParam Long orderId ) {
    public R<String> carAbnormalUpdateOrderStatus(@RequestParam Long orderId) {
//        Integer status = OrderStatusEnum.SEE_OFF_GUESTS.getCode();
        return xpOrderService.carAbnormalUpdateOrderStatus(orderId);
    }
@@ -567,7 +571,7 @@
    @PostMapping("/api/v1/mini-program/order/queryOrderRoute")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<RouteVo> queryOrderRoute(@Validated @RequestBody QueryOrderRouteDTO dto, HttpServletRequest request) {
        if(dto.getOrderId() == null){
        if (dto.getOrderId() == null) {
            return R.fail("参数不能为空");
        }
        String uid = request.getHeader("XP-Uid");
@@ -592,6 +596,7 @@
        routeVo.setDirection(null != vehiclePosition ? vehiclePosition.getCourse() : 90);
        return R.ok(routeVo);
    }
    /**
     * 订单查询轨迹路线
     */
@@ -599,7 +604,7 @@
    @GetMapping("/api/v1/mini-program/order/queryOrderTrajectory")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<RouteVo> queryOrderTrajectory(@RequestParam String orderId, HttpServletRequest request) {
        if(orderId == null){
        if (orderId == null) {
            return R.fail("参数不能为空");
        }
        List<OrderTrajectory> trajectories = mongoTemplate.find(
@@ -610,7 +615,7 @@
        // 封装信息
        RouteVo routeVo = new RouteVo();
        List<LonLatDto> polyline = new ArrayList<>();
        if(!CollectionUtils.isEmpty(trajectories)){
        if (!CollectionUtils.isEmpty(trajectories)) {
            for (OrderTrajectory trajectory : trajectories) {
                LonLatDto lonLatDto = new LonLatDto();
                String[] split = trajectory.getLocation().split(",");
@@ -624,7 +629,6 @@
    }
    /**
     * 当日取消订单次数
     *
@@ -632,13 +636,30 @@
     * @return 订单数量
     */
    @PostMapping("/inner/order/todayCancellationCount")
    public R<Integer> todayCancellationCount(@RequestParam Long userId){
    public R<Integer> todayCancellationCount(@RequestParam Long userId) {
        long count = xpOrderService.count(new QueryWrapper<XpOrder>().eq("app_user_id", userId).eq("status", 0)
                .eq("is_deleted", 0).last(" and DATE_FORMAT(create_time, '%Y-%m-%d') = DATE_FORMAT(NOW(), '%Y-%m-%d')"));
        return R.ok((int)count);
        return R.ok((int) count);
    }
}
    @ApiOperation(value = "通过订单id获取蓝牙开锁秘钥", tags = {"小程序-业务流程"})
    @GetMapping("/api/v1/mini-program/bluetooth/getUnlocktToken/{orderId}")
    @XpClientCheck(checkToken = false, checkOtpToken = true)
    public R<String> getBluetoothToken(@PathVariable("orderId") Long orderId, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        XpOrder xpOrder = xpOrderService.getById(orderId);
        if (!Objects.equals(xpOrder.getAppUserId(), Long.valueOf(uid))) {
            return R.ok();
        }
        if(!Arrays.asList(4, 5, 6, 7, 8).contains(xpOrder.getStatus())){
            return R.ok();
        }
        String token = redisService.getCacheObject("rt:bluetooth:unlock:order:" + orderId);
        if (StringUtils.isBlank(token)) {
            return R.ok();
        }
        return R.ok(token);
    }
}
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -27,6 +27,7 @@
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.XpAppUserCouponClient;
import com.xiaopeng.api.feignClient.automaticPilot.OrderMissionClient;
@@ -35,10 +36,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.*;
@@ -52,6 +50,7 @@
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;
@@ -85,10 +84,7 @@
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 lombok.RequiredArgsConstructor;
@@ -163,12 +159,20 @@
    @Resource
    @Lazy
    private BootCommonService bootCommonService;
    @Autowired
    private SysGroupMenuClient sysGroupMenuClient;
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private HolidayUtil holidayUtil;
    @Autowired
    private BluetoothUnlockTokenUtil bluetoothUnlockTokenUtil;
    @Resource
    private BluetoothUnlockClient bluetoothUnlockClient;
    @Value("${robotaxi.appId:xp_robotaxi_api}")
    private String appid;
@@ -597,7 +601,7 @@
        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());
        }
        // 车辆信息
@@ -641,9 +645,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<>();
@@ -1669,9 +1689,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);
@@ -1695,7 +1731,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);
@@ -2043,10 +2095,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());
@@ -3295,6 +3363,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){
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/utils/BluetoothUnlockTokenUtil.java
New file
@@ -0,0 +1,129 @@
package com.xiaopeng.order.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
 * 蓝牙解锁Token生成工具类
 * 根据设计方案实现Token的生成
 */
@Component
public class BluetoothUnlockTokenUtil {
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final int RANDOM_BYTES_LENGTH = 16; // 16字节随机数
    private static final int HMAC_BYTES_LENGTH = 16;   // 16字节HMAC (取前16字节)
    private static final int LONG_BYTE_SIZE = 8;       // Long类型占用8字节
    @Value("${bluetooth.unlock.secret:123456}")
    private String secret;
    /**
     * 生成蓝牙解锁Token
     *
     * @param userId 用户ID
     * @param orderId 订单ID
     * @param vehicleId 车辆ID
     * @param expirationTime 订单预估结束时间
     * @return 64字符的Token (32字符随机数 + 32字符HMAC)
     */
    public String generateBluetoothUnlockToken(long userId, long orderId,
                                                     long vehicleId, long expirationTime) {
        try {
            // 1. 使用安全随机数生成器生成16字节随机数
            SecureRandom secureRandom = new SecureRandom();
            byte[] randomBytes = new byte[RANDOM_BYTES_LENGTH];
            secureRandom.nextBytes(randomBytes);
            // 2. 拼接二进制数据:随机数(16字节) + userId(8字节) + orderId(8字节) + vehicleId(8字节) + expirationTime(8字节)
            byte[] dataToSign = new byte[RANDOM_BYTES_LENGTH + 4 * LONG_BYTE_SIZE];
            // 复制随机数
            System.arraycopy(randomBytes, 0, dataToSign, 0, RANDOM_BYTES_LENGTH);
            // 复制userId (转换为字节数组)
            longToBytes(userId, dataToSign, RANDOM_BYTES_LENGTH);
            // 复制orderId (转换为字节数组)
            longToBytes(orderId, dataToSign, RANDOM_BYTES_LENGTH + LONG_BYTE_SIZE);
            // 复制vehicleId (转换为字节数组)
            longToBytes(vehicleId, dataToSign, RANDOM_BYTES_LENGTH + 2 * LONG_BYTE_SIZE);
            // 复制expirationTime (转换为字节数组)
            longToBytes(expirationTime, dataToSign, RANDOM_BYTES_LENGTH + 3 * LONG_BYTE_SIZE);
            // 3. 使用服务器端密钥对上述48字节计算HMAC-SHA256
            byte[] hmacFull = calculateHmac(secret, dataToSign);
            // 4. 取HMAC结果的前16字节
            byte[] hmacPrefix = new byte[HMAC_BYTES_LENGTH];
            System.arraycopy(hmacFull, 0, hmacPrefix, 0, HMAC_BYTES_LENGTH);
            // 5. 最终Token = 十六进制(随机数) + 十六进制(HMAC前16字节)
            String randomHex = bytesToHex(randomBytes);
            String hmacHex = bytesToHex(hmacPrefix);
            return randomHex + hmacHex;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("生成Token失败", e);
        }
    }
    /**
     * 将long值转换为8字节,并存入指定位置
     *
     * @param value 要转换的long值
     * @param dest 目标字节数组
     * @param offset 偏移量
     */
    private static void longToBytes(long value, byte[] dest, int offset) {
        for (int i = 0; i < LONG_BYTE_SIZE; i++) {
            dest[offset + i] = (byte) ((value >> (8 * (LONG_BYTE_SIZE - 1 - i))) & 0xFF);
        }
    }
    /**
     * 计算HMAC-SHA256
     *
     * @param secret 密钥
     * @param data 待计算的数据
     * @return HMAC-SHA256结果
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    private static byte[] calculateHmac(String secret, byte[] data) throws NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), HMAC_ALGORITHM);
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(secretKeySpec);
        return mac.doFinal(data);
    }
    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param bytes 字节数组
     * @return 十六进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(b & 0xFF);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/utils/TaskOrderCancelUtil.java
@@ -2,7 +2,13 @@
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.dto.vehicleConvoy.BaseVehicleUserDto;
import com.xiaopeng.api.feignClient.vehicleConvoy.BluetoothUnlockClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.order.XpOrder;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.api.vo.vehicleConvoy.BluetoothDeliverVo;
import com.xiaopeng.common.core.utils.StringUtils;
import com.xiaopeng.common.redis.service.RedisService;
import com.xiaopeng.order.service.IXpOrderService;
import lombok.extern.slf4j.Slf4j;
@@ -10,6 +16,7 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -24,6 +31,12 @@
    @Autowired
    private RedisService redisService;
    @Resource
    private XpVehicleClient xpVehicleClient;
    @Resource
    private BluetoothUnlockClient bluetoothUnlockClient;
    /**
     * 30秒执行一次
@@ -59,7 +72,22 @@
        order.setAppUserId(null);
        order.setStatus(0);
        xpOrderService.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());
            }
        }
    }
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketConfig.java
@@ -1,27 +1,27 @@
package com.xiaopeng.pushMessage.webSocket.config;
import com.xiaopeng.pushMessage.webSocket.handler.PushMessageWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
 * Spring WebSocket 配置
 * 注册 /websocket 端点,共用 Spring Boot HTTP 端口
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private PushMessageWebSocketHandler pushMessageWebSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(pushMessageWebSocketHandler, "/websocket")
                // 允许所有跨域来源,生产环境可按需收紧
                .setAllowedOrigins("*");
    }
}
//package com.xiaopeng.pushMessage.webSocket.config;
//
//import com.xiaopeng.pushMessage.webSocket.handler.PushMessageWebSocketHandler;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.web.socket.config.annotation.EnableWebSocket;
//import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
//import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
//
///**
// * Spring WebSocket 配置
// * 注册 /websocket 端点,共用 Spring Boot HTTP 端口
// */
//@Configuration
//@EnableWebSocket
//public class WebSocketConfig implements WebSocketConfigurer {
//
//    @Autowired
//    private PushMessageWebSocketHandler pushMessageWebSocketHandler;
//
//    @Override
//    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//        registry.addHandler(pushMessageWebSocketHandler, "/websocket")
//                // 允许所有跨域来源,生产环境可按需收紧
//                .setAllowedOrigins("*");
//    }
//}
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketServer.java
New file
@@ -0,0 +1,89 @@
package com.xiaopeng.pushMessage.webSocket.config;
import com.xiaopeng.pushMessage.webSocket.handler.WebSocketTextFrameHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class WebSocketServer {
    private final int port;
    @Autowired
    private WebSocketTextFrameHandler webSocketTextFrameHandler;
    public WebSocketServer() {
        this.port = 8888; // 默认端口
    }
    public WebSocketServer(int port) {
        this.port = port;
    }
    public void run() throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new LoggingHandler(LogLevel.INFO))  // 在BossGroup中增加一个日志处理器 日志级别为INFO
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 因为是基于Http协议的所以采用Http的编解码器
                            pipeline.addLast(new HttpServerCodec());
                            // 是以块的方式写, 添加ChunkedWriteHandler(分块写入处理程序)
                            pipeline.addLast(new ChunkedWriteHandler());
                            /*
                             * http 数据在传输过程中是分段的 http Object aggregator 就是可以将多个段聚合
                             * 这就是为什么 当浏览器发送大量数据时, 就会出现多次http请求
                             * 参数为 : 最大内容长度
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /*
                             * 对应WebSocket 他的数据时以桢(frame) 形式传递
                             * 可以看到WebSocketFrame下面有6个子类
                             * 浏览器请求时: ws://localhost:7000/xxx 请求的url
                             * 核心功能是将http协议升级为ws协议 保持长链接
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
                            // 自定义Handler, 处理业务逻辑
                            // 使用@Sharable注解的Handler,可以被多个连接共享
                            pipeline.addLast(webSocketTextFrameHandler);
                        }
                    });
            log.info("WebSocket服务器启动中,端口: {}", port);
            ChannelFuture sync = serverBootstrap.bind(port).sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("WebSocket服务器启动失败", e);
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
//    public static void main(String[] args) throws InterruptedException {
//        new WebSocketServer(7000).run();
//    }
}
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/config/WebSocketServerConfig.java
New file
@@ -0,0 +1,32 @@
package com.xiaopeng.pushMessage.webSocket.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
 * WebSocket服务器配置类
 * 在Spring Boot应用启动时自动启动WebSocket服务器
 */
@Component
@Slf4j
public class WebSocketServerConfig implements CommandLineRunner {
    @Autowired
    private WebSocketServer webSocketServer;
    @Override
    public void run(String... args) throws Exception {
        // 在新线程中启动WebSocket服务器,避免阻塞主线程
        new Thread(() -> {
            try {
                log.info("正在启动WebSocket服务器...");
                webSocketServer.run();
            } catch (InterruptedException e) {
                log.error("WebSocket服务器启动失败", e);
                Thread.currentThread().interrupt();
            }
        }, "WebSocket-Server-Thread").start();
    }
}
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/controller/WebSocketController.java
@@ -1,6 +1,5 @@
package com.xiaopeng.pushMessage.webSocket.controller;
import com.alibaba.fastjson.JSONObject;
import com.xiaopeng.api.dto.pushMessage.SendMessageDTO;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.controller.BaseController;
@@ -41,7 +40,7 @@
     * @return 操作结果
     */
    @ApiOperation(value = "发送订单状态")
    @PostMapping("/logan/inner/websocket/order/sendStatus")
    @PostMapping("/inner/websocket/order/sendStatus")
    public R<String> sendStatus(@RequestBody SendMessageDTO dto) {
        try {
            String message = dto.getMessage();
@@ -99,7 +98,7 @@
     * @return 操作结果
     */
    @ApiOperation(value = "发送车辆路线、时间")
    @PostMapping("/logan/inner/websocket/order/sendRoute")
    @PostMapping("/inner/websocket/order/sendRoute")
    public R<String> sendRoute(@RequestBody SendMessageDTO dto) {
        try {
            String message = dto.getMessage();
@@ -133,7 +132,7 @@
     * @return 操作结果
     */
    @ApiOperation(value = "发送车辆实时定位")
    @PostMapping("/logan/inner/websocket/order/sendVehicleLocation")
    @PostMapping("/inner/websocket/order/sendVehicleLocation")
    public R<String> sendVehicleLocation(@RequestBody SendMessageDTO dto) {
        try {
            String message = dto.getMessage();
@@ -167,7 +166,7 @@
     * @return 操作结果
     */
    @ApiOperation(value = "发送是否开启倒计时")
    @PostMapping("/logan/inner/websocket/order/sendOpenTrunk")
    @PostMapping("/inner/websocket/order/sendOpenTrunk")
    public R<String> sendOpenTrunk(@RequestBody SendMessageDTO dto) {
        try {
            String message = dto.getMessage();
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/PushMessageWebSocketHandler.java
@@ -1,226 +1,186 @@
package com.xiaopeng.pushMessage.webSocket.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.xiaopeng.api.dto.account.XpOperationsUserPositionDTO;
import com.xiaopeng.pushMessage.webSocket.service.WebSocketUserConnectionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.UUID;
/**
 * Spring WebSocket 消息处理器
 * 路径: /websocket,共用 Spring Boot HTTP 端口
 * 替代原 Netty 独立端口方案
 */
@Component
@Slf4j
public class PushMessageWebSocketHandler extends TextWebSocketHandler {
    @Autowired
    private WebSocketUserConnectionManager userConnectionManager;
    @Autowired
    private WebSocketAuthHandler authHandler;
    private ObjectMapper om;
    @PostConstruct
    void setupObjectMapper() {
        // 允许对象忽略json中不存在的属性
        om = new ObjectMapper();
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        om.registerModule(new JavaTimeModule());
    }
    // ==================== 连接生命周期 ====================
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("WebSocket 连接建立,sessionId: {}", session.getId());
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("WebSocket 连接关闭,sessionId: {}, status: {}", session.getId(), status);
        userConnectionManager.removeUserConnection(session);
    }
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.error("WebSocket 传输异常,sessionId: {}, error: {}", session.getId(), exception.getMessage(), exception);
        if (session.isOpen()) {
            session.close(CloseStatus.SERVER_ERROR);
        }
    }
    // ==================== 消息处理 ====================
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("[服务器] 收到消息 -> {}", payload);
        try {
            if (payload.startsWith("pos:")) {
                if (log.isDebugEnabled())
                    log.debug("收到运维人员位置上报消息:{}",payload);
                XpOperationsUserPositionDTO xpOperationsUserPositionDTO = om.readValue(payload.substring(4), XpOperationsUserPositionDTO.class);
                saveToMongo(xpOperationsUserPositionDTO);
            }
            JSONObject messageObj = JSON.parseObject(payload);
            String messageType = messageObj.getString("type");
            if ("auth".equals(messageType)) {
                handleUserAuth(session, messageObj);
            } else if ("ping".equals(messageType)) {
                handlePing(session);
            } else {
                handleBusinessMessage(session, messageObj);
            }
        } catch (Exception e) {
            log.debug("消息非 JSON 格式,按普通文本处理: {}", payload);
            sendText(session, "服务器时间: " + LocalDateTime.now() + "->" + payload);
        }
    }
    @Autowired
    private MongoTemplate mongoTemplate;
    private void saveToMongo(XpOperationsUserPositionDTO dto) {
        XpOperationsUserPositionDTO vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("id").is(dto.getId())), XpOperationsUserPositionDTO.class);
        if (null == vehiclePosition) {
            mongoTemplate.save(dto);
        }else{
            Update update = new Update()
                    .set("id",dto.getId())
                    .set("latitude", dto.getLatitude())
                    .set("latitude", dto.getLongitude());
            mongoTemplate.updateFirst(new Query(Criteria.where("id").is(dto.getId())), update, XpOperationsUserPositionDTO.class);
        }
    }
    // ==================== 业务处理 ====================
    /**
     * 处理用户认证消息
     */
    private void handleUserAuth(WebSocketSession session, JSONObject messageObj) {
        try {
            WebSocketAuthHandler.UserAuthInfo authInfo = authHandler.extractAuthInfo(messageObj);
            String appUserId = authInfo.getAppUserId();
            String token = authInfo.getToken();
            if (!StringUtils.hasText(appUserId)) {
                sendAuthFailure(session, "用户ID不能为空");
                return;
            }
            WebSocketAuthHandler.AuthResult authResult = authHandler.authenticateUser(appUserId, token);
            if (authResult.isSuccess()) {
                userConnectionManager.addUserConnection(appUserId, session);
                // TODO
                String uuid = UUID.randomUUID().toString();
                JSONObject response = new JSONObject();
                response.put("type", "auth_success");
                response.put("appUserId", appUserId);
                response.put("nickName", uuid);
                response.put("message", "认证成功");
                response.put("timestamp", LocalDateTime.now().toString());
                sendText(session, response.toJSONString());
                log.info("用户 {} ({}) 认证成功,sessionId: {}", appUserId, uuid, session.getId());
            } else {
                sendAuthFailure(session, authResult.getMessage());
                log.warn("用户 {} 认证失败: {}, sessionId: {}", appUserId, authResult.getMessage(), session.getId());
            }
        } catch (Exception e) {
            log.error("处理用户认证时发生错误", e);
            sendAuthFailure(session, "认证过程中发生错误: " + e.getMessage());
        }
    }
    private void sendAuthFailure(WebSocketSession session, String message) {
        JSONObject response = new JSONObject();
        response.put("type", "auth_failed");
        response.put("message", message);
        response.put("timestamp", LocalDateTime.now().toString());
        sendText(session, response.toJSONString());
    }
    /**
     * 处理心跳消息
     */
    private void handlePing(WebSocketSession session) {
        String appUserId = userConnectionManager.getUserIdBySessionId(session.getId());
        JSONObject response = new JSONObject();
        response.put("type", "pong");
        response.put("appUserId", appUserId);
        response.put("timestamp", LocalDateTime.now().toString());
        log.info("心跳用户id:{}", appUserId);
        sendText(session, response.toJSONString());
    }
    /**
     * 处理业务消息
     */
    private void handleBusinessMessage(WebSocketSession session, JSONObject messageObj) {
        String appUserId = userConnectionManager.getUserIdBySessionId(session.getId());
        if (appUserId != null) {
            log.info("收到用户 {} 的业务消息: {}", appUserId, messageObj.toJSONString());
            JSONObject response = new JSONObject();
            response.put("type", "message_response");
            response.put("appUserId", appUserId);
            response.put("originalMessage", messageObj);
            response.put("serverTime", LocalDateTime.now().toString());
            sendText(session, response.toJSONString());
        } else {
            log.warn("收到未认证连接的业务消息: {}", messageObj.toJSONString());
            JSONObject response = new JSONObject();
            response.put("type", "auth_required");
            response.put("message", "请先进行用户认证");
            response.put("timestamp", LocalDateTime.now().toString());
            sendText(session, response.toJSONString());
        }
    }
    // ==================== 工具方法 ====================
    /**
     * 发送文本消息(线程安全)
     */
    private void sendText(WebSocketSession session, String text) {
        try {
            if (session.isOpen()) {
                synchronized (session) {
                    session.sendMessage(new TextMessage(text));
                }
            }
        } catch (Exception e) {
            log.error("发送消息失败,sessionId: {}, error: {}", session.getId(), e.getMessage());
        }
    }
}
//package com.xiaopeng.pushMessage.webSocket.handler;
//
//import com.alibaba.fastjson.JSON;
//import com.alibaba.fastjson.JSONObject;
//import com.xiaopeng.pushMessage.webSocket.service.WebSocketUserConnectionManager;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.stereotype.Component;
//import org.springframework.util.StringUtils;
//import org.springframework.web.socket.CloseStatus;
//import org.springframework.web.socket.TextMessage;
//import org.springframework.web.socket.WebSocketSession;
//import org.springframework.web.socket.handler.TextWebSocketHandler;
//
//import java.time.LocalDateTime;
//import java.util.UUID;
//
///**
// * Spring WebSocket 消息处理器
// * 路径: /websocket,共用 Spring Boot HTTP 端口
// * 替代原 Netty 独立端口方案
// */
//@Component
//@Slf4j
//public class PushMessageWebSocketHandler extends TextWebSocketHandler {
//
//    @Autowired
//    private WebSocketUserConnectionManager userConnectionManager;
//
//    @Autowired
//    private WebSocketAuthHandler authHandler;
//
//    // ==================== 连接生命周期 ====================
//
//    @Override
//    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//        log.info("WebSocket 连接建立,sessionId: {}", session.getId());
//    }
//
//    @Override
//    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//        log.info("WebSocket 连接关闭,sessionId: {}, status: {}", session.getId(), status);
//        userConnectionManager.removeUserConnection(session);
//    }
//
//    @Override
//    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
//        log.error("WebSocket 传输异常,sessionId: {}, error: {}", session.getId(), exception.getMessage(), exception);
//        if (session.isOpen()) {
//            session.close(CloseStatus.SERVER_ERROR);
//        }
//    }
//
//    // ==================== 消息处理 ====================
//
//    @Override
//    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//        String payload = message.getPayload();
//        log.info("[服务器] 收到消息 -> {}", payload);
//
//        try {
//            JSONObject messageObj = JSON.parseObject(payload);
//            String messageType = messageObj.getString("type");
//
//            if ("auth".equals(messageType)) {
//                handleUserAuth(session, messageObj);
//            } else if ("ping".equals(messageType)) {
//                handlePing(session);
//            } else {
//                handleBusinessMessage(session, messageObj);
//            }
//        } catch (Exception e) {
//            log.debug("消息非 JSON 格式,按普通文本处理: {}", payload);
//            sendText(session, "服务器时间: " + LocalDateTime.now() + "->" + payload);
//        }
//    }
//
//    // ==================== 业务处理 ====================
//
//    /**
//     * 处理用户认证消息
//     */
//    private void handleUserAuth(WebSocketSession session, JSONObject messageObj) {
//        try {
//            WebSocketAuthHandler.UserAuthInfo authInfo = authHandler.extractAuthInfo(messageObj);
//            String appUserId = authInfo.getAppUserId();
//            String token = authInfo.getToken();
//
//            if (!StringUtils.hasText(appUserId)) {
//                sendAuthFailure(session, "用户ID不能为空");
//                return;
//            }
//
//            WebSocketAuthHandler.AuthResult authResult = authHandler.authenticateUser(appUserId, token);
//            if (authResult.isSuccess()) {
//                userConnectionManager.addUserConnection(appUserId, session);
//
//                // TODO
//                String uuid = UUID.randomUUID().toString();
//                JSONObject response = new JSONObject();
//                response.put("type", "auth_success");
//                response.put("appUserId", appUserId);
//                response.put("nickName", uuid);
//                response.put("message", "认证成功");
//                response.put("timestamp", LocalDateTime.now().toString());
//
//                sendText(session, response.toJSONString());
//                log.info("用户 {} ({}) 认证成功,sessionId: {}", appUserId, uuid, session.getId());
//            } else {
//                sendAuthFailure(session, authResult.getMessage());
//                log.warn("用户 {} 认证失败: {}, sessionId: {}", appUserId, authResult.getMessage(), session.getId());
//            }
//        } catch (Exception e) {
//            log.error("处理用户认证时发生错误", e);
//            sendAuthFailure(session, "认证过程中发生错误: " + e.getMessage());
//        }
//    }
//
//    private void sendAuthFailure(WebSocketSession session, String message) {
//        JSONObject response = new JSONObject();
//        response.put("type", "auth_failed");
//        response.put("message", message);
//        response.put("timestamp", LocalDateTime.now().toString());
//        sendText(session, response.toJSONString());
//    }
//
//    /**
//     * 处理心跳消息
//     */
//    private void handlePing(WebSocketSession session) {
//        String appUserId = userConnectionManager.getUserIdBySessionId(session.getId());
//
//        JSONObject response = new JSONObject();
//        response.put("type", "pong");
//        response.put("appUserId", appUserId);
//        response.put("timestamp", LocalDateTime.now().toString());
//        log.info("心跳用户id:{}", appUserId);
//
//        sendText(session, response.toJSONString());
//    }
//
//    /**
//     * 处理业务消息
//     */
//    private void handleBusinessMessage(WebSocketSession session, JSONObject messageObj) {
//        String appUserId = userConnectionManager.getUserIdBySessionId(session.getId());
//
//        if (appUserId != null) {
//            log.info("收到用户 {} 的业务消息: {}", appUserId, messageObj.toJSONString());
//
//            JSONObject response = new JSONObject();
//            response.put("type", "message_response");
//            response.put("appUserId", appUserId);
//            response.put("originalMessage", messageObj);
//            response.put("serverTime", LocalDateTime.now().toString());
//
//            sendText(session, response.toJSONString());
//        } else {
//            log.warn("收到未认证连接的业务消息: {}", messageObj.toJSONString());
//
//            JSONObject response = new JSONObject();
//            response.put("type", "auth_required");
//            response.put("message", "请先进行用户认证");
//            response.put("timestamp", LocalDateTime.now().toString());
//
//            sendText(session, response.toJSONString());
//        }
//    }
//
//    // ==================== 工具方法 ====================
//
//    /**
//     * 发送文本消息(线程安全)
//     */
//    private void sendText(WebSocketSession session, String text) {
//        try {
//            if (session.isOpen()) {
//                synchronized (session) {
//                    session.sendMessage(new TextMessage(text));
//                }
//            }
//        } catch (Exception e) {
//            log.error("发送消息失败,sessionId: {}, error: {}", session.getId(), e.getMessage());
//        }
//    }
//}
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/WebSocketAuthHandler.java
@@ -3,7 +3,6 @@
import com.xiaopeng.api.dto.account.UserResourcesDto;
import com.xiaopeng.api.feignClient.account.UserResourcesClient;
import com.xiaopeng.api.feignClient.account.XpAppUserClient;
import com.xiaopeng.api.model.account.XpAppUser;
import com.xiaopeng.api.vo.account.LoginOperationUser;
import com.xiaopeng.common.core.constant.CacheConstants;
import com.xiaopeng.common.core.constant.Constants;
@@ -12,12 +11,10 @@
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.Objects;
/**
 * WebSocket认证处理器
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/handler/WebSocketTextFrameHandler.java
@@ -1,195 +1,195 @@
package com.xiaopeng.pushMessage.webSocket.handler;
//import com.alibaba.fastjson.JSON;
//import com.alibaba.fastjson.JSONObject;
//import com.xiaopeng.pushMessage.webSocket.service.WebSocketUserConnectionManager;
//import io.netty.channel.ChannelHandler;
//import io.netty.channel.ChannelHandlerContext;
//import io.netty.channel.SimpleChannelInboundHandler;
//import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.stereotype.Component;
//import org.springframework.util.StringUtils;
//
//import java.time.LocalDateTime;
//import java.util.Objects;
//import java.util.UUID;
//
///**
// * TextWebSocketFrame 表示一个文本桢
// * 使用@Sharable注解使其可以被多个连接共享
// * 支持基于AppUser的连接管理
// */
//@Component
//@Slf4j
//@ChannelHandler.Sharable
//public class WebSocketTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
//
//
//    @Autowired
//    private WebSocketUserConnectionManager userConnectionManager;
//
//    @Autowired
//    private WebSocketAuthHandler authHandler;
//
//    @Override
//    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//        String message = msg.text();
//        log.info("[服务器] : 收到消息 -> {}", message);
//
//        try {
//            // 尝试解析消息,检查是否包含用户认证信息
//            JSONObject messageObj = JSON.parseObject(message);
//            String messageType = messageObj.getString("type");
//
//            if ("auth".equals(messageType)) {
//                // 处理用户认证消息
//                handleUserAuth(ctx, messageObj);
//            } else if ("ping".equals(messageType)) {
//                // 处理心跳消息
//                handlePing(ctx, messageObj);
//            } else {
//                // 处理普通业务消息
//                handleBusinessMessage(ctx, messageObj);
//            }
//        } catch (Exception e) {
//            // 如果不是JSON格式,按普通文本处理
//            log.debug("消息不是JSON格式,按普通文本处理: {}", message);
//            ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间: " + LocalDateTime.now() + "->" + message));
//        }
//    }
//
//    /**
//     * 处理用户认证消息
//     */
//    private void handleUserAuth(ChannelHandlerContext ctx, JSONObject messageObj) {
//        try {
//            // 提取认证信息
//            WebSocketAuthHandler.UserAuthInfo authInfo = authHandler.extractAuthInfo(messageObj);
//            String appUserId = authInfo.getAppUserId();
//            String token = authInfo.getToken();
//
//            if (!StringUtils.hasText(appUserId)) {
//                sendAuthFailureResponse(ctx, "用户ID不能为空");
//                return;
//            }
//
//            // 进行用户认证
//            WebSocketAuthHandler.AuthResult authResult = authHandler.authenticateUser(appUserId, token);
//            if (authResult.isSuccess()) {
//                // 认证成功,将连接与用户关联
//                userConnectionManager.addUserConnection(appUserId, ctx.channel());
//
//                // 发送认证成功响应
//                JSONObject response = new JSONObject();
//                response.put("type", "auth_success");
//                response.put("appUserId", appUserId);
//                // TODO
//                String uuid = UUID.randomUUID().toString();
//                response.put("nickName", uuid);
////                response.put("nickName", authResult.getAppUser().getNickName());
//                response.put("message", "认证成功");
//                response.put("timestamp", LocalDateTime.now().toString());
//
//                ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
//                log.info("用户 {} ({}) 认证成功,ChannelId: {}",
//                        appUserId, uuid, ctx.channel().id().asLongText());
//            } else {
//                // 认证失败
//                sendAuthFailureResponse(ctx, authResult.getMessage());
//                log.warn("用户 {} 认证失败: {}, ChannelId: {}",
//                        appUserId, authResult.getMessage(), ctx.channel().id().asLongText());
//            }
//        } catch (Exception e) {
//            log.error("处理用户认证时发生错误", e);
//            sendAuthFailureResponse(ctx, "认证过程中发生错误: " + e.getMessage());
//        }
//    }
//
//    /**
//     * 发送认证失败响应
//     */
//    private void sendAuthFailureResponse(ChannelHandlerContext ctx, String message) {
//        JSONObject response = new JSONObject();
//        response.put("type", "auth_failed");
//        response.put("message", message);
//        response.put("timestamp", LocalDateTime.now().toString());
//
//        ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
//    }
//
//    /**
//     * 处理心跳消息
//     */
//    private void handlePing(ChannelHandlerContext ctx, JSONObject messageObj) {
//        String appUserId = userConnectionManager.getUserIdByChannelId(ctx.channel().id().asLongText());
//
//        JSONObject response = new JSONObject();
//        response.put("type", "pong");
//        log.info("心跳用户id:{}",appUserId);
//        response.put("appUserId", appUserId);
//        response.put("timestamp", LocalDateTime.now().toString());
//
//        ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
//    }
//
//    /**
//     * 处理业务消息
//     */
//    private void handleBusinessMessage(ChannelHandlerContext ctx, JSONObject messageObj) {
//        String appUserId = userConnectionManager.getUserIdByChannelId(ctx.channel().id().asLongText());
//
//        if (appUserId != null) {
//            log.info("收到用户 {} 的业务消息: {}", appUserId, messageObj.toJSONString());
//
//            // 回复消息
//            JSONObject response = new JSONObject();
//            response.put("type", "message_response");
//            response.put("appUserId", appUserId);
//            response.put("originalMessage", messageObj);
//            response.put("serverTime", LocalDateTime.now().toString());
//
//            ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
//        } else {
//            log.warn("收到未认证连接的业务消息: {}", messageObj.toJSONString());
//
//            // 要求先认证
//            JSONObject response = new JSONObject();
//            response.put("type", "auth_required");
//            response.put("message", "请先进行用户认证");
//            response.put("timestamp", LocalDateTime.now().toString());
//
//            ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
//        }
//    }
//
//    @Override
//    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//        // id 表示唯一的值 LongText是唯一的
//        log.info("handlerAdded 被调用: {}", ctx.channel().id().asLongText());
//        // shortText 可能会重复
//        log.info("handlerAdded 被调用: {}", ctx.channel().id().asShortText());
//
//        // 注意:用户连接管理在认证时添加,这里不添加
//    }
//
//    @Override
//    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//        // id 表示唯一的值 LongText是唯一的
//        log.info("handlerRemoved 被调用: {}", ctx.channel().id().asLongText());
//        // shortText 可能会重复
//        log.info("handlerRemoved 被调用: {}", ctx.channel().id().asShortText());
//
//        // 从用户连接管理器中移除连接
//        if (userConnectionManager != null) {
//            userConnectionManager.removeUserConnection(ctx.channel());
//        }
//    }
//
//    @Override
//    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//        log.error("WebSocket连接异常: {}", cause.getMessage(), cause);
//        ctx.channel().close();
//    }
//}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xiaopeng.pushMessage.webSocket.service.WebSocketUserConnectionManager;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
/**
 * TextWebSocketFrame 表示一个文本桢
 * 使用@Sharable注解使其可以被多个连接共享
 * 支持基于AppUser的连接管理
 */
@Component
@Slf4j
@ChannelHandler.Sharable
public class WebSocketTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Autowired
    private WebSocketUserConnectionManager userConnectionManager;
    @Autowired
    private WebSocketAuthHandler authHandler;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String message = msg.text();
        log.info("[服务器] : 收到消息 -> {}", message);
        try {
            // 尝试解析消息,检查是否包含用户认证信息
            JSONObject messageObj = JSON.parseObject(message);
            String messageType = messageObj.getString("type");
            if ("auth".equals(messageType)) {
                // 处理用户认证消息
                handleUserAuth(ctx, messageObj);
            } else if ("ping".equals(messageType)) {
                // 处理心跳消息
                handlePing(ctx, messageObj);
            } else {
                // 处理普通业务消息
                handleBusinessMessage(ctx, messageObj);
            }
        } catch (Exception e) {
            // 如果不是JSON格式,按普通文本处理
            log.debug("消息不是JSON格式,按普通文本处理: {}", message);
            ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间: " + LocalDateTime.now() + "->" + message));
        }
    }
    /**
     * 处理用户认证消息
     */
    private void handleUserAuth(ChannelHandlerContext ctx, JSONObject messageObj) {
        try {
            // 提取认证信息
            WebSocketAuthHandler.UserAuthInfo authInfo = authHandler.extractAuthInfo(messageObj);
            String appUserId = authInfo.getAppUserId();
            String token = authInfo.getToken();
            if (!StringUtils.hasText(appUserId)) {
                sendAuthFailureResponse(ctx, "用户ID不能为空");
                return;
            }
            // 进行用户认证
            WebSocketAuthHandler.AuthResult authResult = authHandler.authenticateUser(appUserId, token);
            if (authResult.isSuccess()) {
                // 认证成功,将连接与用户关联
                userConnectionManager.addUserConnection(appUserId, ctx.channel());
                // 发送认证成功响应
                JSONObject response = new JSONObject();
                response.put("type", "auth_success");
                response.put("appUserId", appUserId);
                // TODO
                String uuid = UUID.randomUUID().toString();
                response.put("nickName", uuid);
//                response.put("nickName", authResult.getAppUser().getNickName());
                response.put("message", "认证成功");
                response.put("timestamp", LocalDateTime.now().toString());
                ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
                log.info("用户 {} ({}) 认证成功,ChannelId: {}",
                        appUserId, uuid, ctx.channel().id().asLongText());
            } else {
                // 认证失败
                sendAuthFailureResponse(ctx, authResult.getMessage());
                log.warn("用户 {} 认证失败: {}, ChannelId: {}",
                        appUserId, authResult.getMessage(), ctx.channel().id().asLongText());
            }
        } catch (Exception e) {
            log.error("处理用户认证时发生错误", e);
            sendAuthFailureResponse(ctx, "认证过程中发生错误: " + e.getMessage());
        }
    }
    /**
     * 发送认证失败响应
     */
    private void sendAuthFailureResponse(ChannelHandlerContext ctx, String message) {
        JSONObject response = new JSONObject();
        response.put("type", "auth_failed");
        response.put("message", message);
        response.put("timestamp", LocalDateTime.now().toString());
        ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
    }
    /**
     * 处理心跳消息
     */
    private void handlePing(ChannelHandlerContext ctx, JSONObject messageObj) {
        String appUserId = userConnectionManager.getUserIdByChannelId(ctx.channel().id().asLongText());
        JSONObject response = new JSONObject();
        response.put("type", "pong");
        log.info("心跳用户id:{}",appUserId);
        response.put("appUserId", appUserId);
        response.put("timestamp", LocalDateTime.now().toString());
        ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
    }
    /**
     * 处理业务消息
     */
    private void handleBusinessMessage(ChannelHandlerContext ctx, JSONObject messageObj) {
        String appUserId = userConnectionManager.getUserIdByChannelId(ctx.channel().id().asLongText());
        if (appUserId != null) {
            log.info("收到用户 {} 的业务消息: {}", appUserId, messageObj.toJSONString());
            // 回复消息
            JSONObject response = new JSONObject();
            response.put("type", "message_response");
            response.put("appUserId", appUserId);
            response.put("originalMessage", messageObj);
            response.put("serverTime", LocalDateTime.now().toString());
            ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
        } else {
            log.warn("收到未认证连接的业务消息: {}", messageObj.toJSONString());
            // 要求先认证
            JSONObject response = new JSONObject();
            response.put("type", "auth_required");
            response.put("message", "请先进行用户认证");
            response.put("timestamp", LocalDateTime.now().toString());
            ctx.channel().writeAndFlush(new TextWebSocketFrame(response.toJSONString()));
        }
    }
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        log.info("handlerAdded 被调用: {}", ctx.channel().id().asLongText());
        // shortText 可能会重复
        log.info("handlerAdded 被调用: {}", ctx.channel().id().asShortText());
        // 注意:用户连接管理在认证时添加,这里不添加
    }
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        log.info("handlerRemoved 被调用: {}", ctx.channel().id().asLongText());
        // shortText 可能会重复
        log.info("handlerRemoved 被调用: {}", ctx.channel().id().asShortText());
        // 从用户连接管理器中移除连接
        if (userConnectionManager != null) {
            userConnectionManager.removeUserConnection(ctx.channel());
        }
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("WebSocket连接异常: {}", cause.getMessage(), cause);
        ctx.channel().close();
    }
}
xp-robotaxi-push-message-boot/src/main/java/com/xiaopeng/pushMessage/webSocket/service/WebSocketUserConnectionManager.java
@@ -1,9 +1,9 @@
package com.xiaopeng.pushMessage.webSocket.service;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -18,138 +18,159 @@
public class WebSocketUserConnectionManager {
    /**
     * 存储用户的所有连接
     * Key: 用户ID (appUserId)
     * Value: 该用户的所有 WebSocketSession 集合
     * Value: 该用户的所有连接Channel列表
     */
    private static final ConcurrentHashMap<String, Set<WebSocketSession>> userConnections = new ConcurrentHashMap<>();
    private static final ConcurrentHashMap<String, Set<Channel>> userConnections = new ConcurrentHashMap<>();
    /**
     * Key: session.getId()
     * 存储连接与用户的映射关系
     * Key: ChannelId.asLongText()
     * Value: 用户ID (appUserId)
     */
    private static final ConcurrentHashMap<String, String> sessionToUser = new ConcurrentHashMap<>();
    private static final ConcurrentHashMap<String, String> channelToUser = new ConcurrentHashMap<>();
    /**
     * Key: session.getId()
     * Value: WebSocketSession
     * 存储所有连接
     * Key: ChannelId.asLongText()
     * Value: Channel
     */
    private static final ConcurrentHashMap<String, WebSocketSession> allConnections = new ConcurrentHashMap<>();
    // ==================== 连接管理 ====================
    private static final ConcurrentHashMap<String, Channel> allConnections = new ConcurrentHashMap<>();
    /**
     * 添加用户连接
     * @param appUserId 用户ID
     * @param channel WebSocket连接通道
     */
    public void addUserConnection(String appUserId, WebSocketSession session) {
        String sessionId = session.getId();
    public void addUserConnection(String appUserId, Channel channel) {
        String channelId = channel.id().asLongText();
        userConnections.computeIfAbsent(appUserId, k -> ConcurrentHashMap.newKeySet()).add(session);
        sessionToUser.put(sessionId, appUserId);
        allConnections.put(sessionId, session);
        // 添加到用户连接映射
        userConnections.computeIfAbsent(appUserId, k -> ConcurrentHashMap.newKeySet())
                .add(channel);
        log.info("用户连接已添加,用户ID: {}, sessionId: {}, 该用户连接数: {}",
                appUserId, sessionId, userConnections.get(appUserId).size());
        // 添加到连接用户映射
        channelToUser.put(channelId, appUserId);
        // 添加到所有连接
        allConnections.put(channelId, channel);
        log.info("用户连接已添加,用户ID: {}, ChannelId: {}, 该用户连接数: {}",
                appUserId, channelId, userConnections.get(appUserId).size());
    }
    /**
     * 移除用户连接
     * @param channel WebSocket连接通道
     */
    public void removeUserConnection(WebSocketSession session) {
        String sessionId = session.getId();
        String appUserId = sessionToUser.get(sessionId);
    public void removeUserConnection(Channel channel) {
        String channelId = channel.id().asLongText();
        String appUserId = channelToUser.get(channelId);
        if (appUserId != null) {
            Set<WebSocketSession> userSessions = userConnections.get(appUserId);
            if (userSessions != null) {
                userSessions.remove(session);
                if (userSessions.isEmpty()) {
            // 从用户连接中移除
            Set<Channel> userChannels = userConnections.get(appUserId);
            if (userChannels != null) {
                userChannels.remove(channel);
                // 如果用户没有连接了,删除用户映射
                if (userChannels.isEmpty()) {
                    userConnections.remove(appUserId);
                }
            }
            sessionToUser.remove(sessionId);
            log.info("用户连接已移除,用户ID: {}, sessionId: {}, 该用户剩余连接数: {}",
                    appUserId, sessionId, userSessions != null ? userSessions.size() : 0);
            // 从连接用户映射中移除
            channelToUser.remove(channelId);
            log.info("用户连接已移除,用户ID: {}, ChannelId: {}, 该用户剩余连接数: {}",
                    appUserId, channelId, userChannels != null ? userChannels.size() : 0);
        }
        allConnections.remove(sessionId);
        // 从所有连接中移除
        allConnections.remove(channelId);
    }
    // ==================== 消息发送 ====================
    /**
     * 向指定用户的所有设备发送消息
     * @param appUserId 用户ID
     * @param message 消息内容
     * @return 成功发送的设备数
     */
    public int sendMessageToUser(String appUserId, String message) {
        Set<WebSocketSession> sessions = userConnections.get(appUserId);
        if (sessions == null || sessions.isEmpty()) {
        Set<Channel> userChannels = userConnections.get(appUserId);
        if (userChannels == null || userChannels.isEmpty()) {
            log.warn("用户 {} 没有活跃连接", appUserId);
            return 0;
        }
        int successCount = 0;
        for (WebSocketSession session : sessions) {
            if (session != null && session.isOpen()) {
        for (Channel channel : userChannels) {
            if (channel != null && channel.isActive()) {
                try {
                    synchronized (session) {
                        session.sendMessage(new TextMessage(message));
                    }
                    channel.writeAndFlush(new TextWebSocketFrame(message));
                    successCount++;
                } catch (Exception e) {
                    log.error("向用户 {} 发送消息失败,sessionId: {}, 错误: {}",
                            appUserId, session.getId(), e.getMessage());
                    log.error("向用户 {} 发送消息失败,连接ID: {}, 错误: {}",
                            appUserId, channel.id().asLongText(), e.getMessage());
                }
            }
        }
        log.info("向用户 {} 发送消息完成,成功发送到 {} 个设备", appUserId, successCount);
        log.info("向用户 {} 发送消息完成,成功发送到 {} 个设备,消息内容: {}",
                appUserId, successCount, message);
        return successCount;
    }
    /**
     * 向指定 session 发送消息
     * 向指定连接发送消息
     * @param channelId 连接ID
     * @param message 消息内容
     * @return 是否发送成功
     */
    public boolean sendMessageToChannel(String sessionId, String message) {
        WebSocketSession session = allConnections.get(sessionId);
        if (session != null && session.isOpen()) {
    public boolean sendMessageToChannel(String channelId, String message) {
        Channel channel = allConnections.get(channelId);
        if (channel != null && channel.isActive()) {
            try {
                synchronized (session) {
                    session.sendMessage(new TextMessage(message));
                }
                log.info("消息已发送到连接: {}", sessionId);
                channel.writeAndFlush(new TextWebSocketFrame(message));
                log.info("消息已发送到连接: {}, 内容: {}", channelId, message);
                return true;
            } catch (Exception e) {
                log.error("发送消息失败,sessionId: {}, 错误: {}", sessionId, e.getMessage());
                log.error("发送消息失败,连接ID: {}, 错误: {}", channelId, e.getMessage());
                return false;
            }
        } else {
            log.warn("连接不存在或已关闭,sessionId: {}", sessionId);
            log.warn("连接不存在或已关闭,ChannelId: {}", channelId);
            return false;
        }
    }
    /**
     * 向所有连接广播消息
     * @param message 消息内容
     * @return 成功发送的连接数
     */
    public int broadcastMessage(String message) {
        int successCount = 0;
        for (WebSocketSession session : allConnections.values()) {
            if (session != null && session.isOpen()) {
        for (Channel channel : allConnections.values()) {
            if (channel != null && channel.isActive()) {
                try {
                    synchronized (session) {
                        session.sendMessage(new TextMessage(message));
                    }
                    channel.writeAndFlush(new TextWebSocketFrame(message));
                    successCount++;
                } catch (Exception e) {
                    log.error("广播消息失败,sessionId: {}, 错误: {}", session.getId(), e.getMessage());
                    log.error("广播消息失败,连接ID: {}, 错误: {}",
                            channel.id().asLongText(), e.getMessage());
                }
            }
        }
        log.info("广播消息完成,成功发送到 {} 个连接", successCount);
        log.info("广播消息完成,成功发送到 {} 个连接,消息内容: {}", successCount, message);
        return successCount;
    }
    /**
     * 向多个用户发送消息
     * @param appUserIds 用户ID列表
     * @param message 消息内容
     * @return 成功发送的用户数
     */
    public int sendMessageToUsers(List<String> appUserIds, String message) {
        int successUserCount = 0;
@@ -163,57 +184,95 @@
        return successUserCount;
    }
    // ==================== 查询 ====================
    public Set<WebSocketSession> getUserConnections(String appUserId) {
    /**
     * 获取用户的所有连接
     * @param appUserId 用户ID
     * @return 连接列表
     */
    public Set<Channel> getUserConnections(String appUserId) {
        return userConnections.getOrDefault(appUserId, Collections.emptySet());
    }
    /**
     * 获取用户连接数
     * @param appUserId 用户ID
     * @return 连接数
     */
    public int getUserConnectionCount(String appUserId) {
        Set<WebSocketSession> sessions = userConnections.get(appUserId);
        return sessions != null ? sessions.size() : 0;
        Set<Channel> userChannels = userConnections.get(appUserId);
        return userChannels != null ? userChannels.size() : 0;
    }
    /**
     * 获取总连接数
     * @return 总连接数
     */
    public int getTotalConnectionCount() {
        return allConnections.size();
    }
    /**
     * 获取在线用户数
     * @return 在线用户数
     */
    public int getOnlineUserCount() {
        return userConnections.size();
    }
    /**
     * 获取所有在线用户ID
     * @return 用户ID集合
     */
    public Set<String> getOnlineUserIds() {
        return userConnections.keySet();
    }
    /**
     * 检查用户是否在线
     * @param appUserId 用户ID
     * @return 是否在线
     */
    public boolean isUserOnline(String appUserId) {
        Set<WebSocketSession> sessions = userConnections.get(appUserId);
        if (sessions == null || sessions.isEmpty()) {
        Set<Channel> userChannels = userConnections.get(appUserId);
        if (userChannels == null || userChannels.isEmpty()) {
            return false;
        }
        return sessions.stream().anyMatch(WebSocketSession::isOpen);
    }
    public boolean isConnectionExists(String sessionId) {
        WebSocketSession session = allConnections.get(sessionId);
        return session != null && session.isOpen();
        // 检查是否有活跃连接
        return userChannels.stream().anyMatch(Channel::isActive);
    }
    /**
     * 根据 sessionId 获取用户ID
     * 检查连接是否存在
     * @param channelId 连接ID
     * @return 是否存在
     */
    public String getUserIdBySessionId(String sessionId) {
        return sessionToUser.get(sessionId);
    public boolean isConnectionExists(String channelId) {
        Channel channel = allConnections.get(channelId);
        return channel != null && channel.isActive();
    }
    /**
     * 根据连接ID获取用户ID
     * @param channelId 连接ID
     * @return 用户ID
     */
    public String getUserIdByChannelId(String channelId) {
        return channelToUser.get(channelId);
    }
    /**
     * 获取连接统计信息
     * @return 统计信息
     */
    public Map<String, Object> getConnectionStats() {
        Map<String, Object> stats = new HashMap<>();
        stats.put("totalConnections", getTotalConnectionCount());
        stats.put("onlineUsers", getOnlineUserCount());
        stats.put("onlineUserIds", getOnlineUserIds());
        // 统计每个用户的连接数
        Map<String, Integer> userConnectionCounts = new HashMap<>();
        for (Map.Entry<String, Set<WebSocketSession>> entry : userConnections.entrySet()) {
        for (Map.Entry<String, Set<Channel>> entry : userConnections.entrySet()) {
            userConnectionCounts.put(entry.getKey(), entry.getValue().size());
        }
        stats.put("userConnectionCounts", userConnectionCounts);
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpVehicleController.java
@@ -1092,4 +1092,12 @@
        Boolean vehicleTrunkOpenStatus = xpVehicleService.getVehicleTrunkOpenStatus(String.valueOf(uid), id);
        return R.ok(vehicleTrunkOpenStatus);
    }
    public R<String> generateBluetoothUnlockToken(){
    }
}
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/util/BluetoothUnlockTokenUtil.java