puzhibing
2026-04-21 e4282457d992fd59acdaf0de9f150105a7015b9c
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/Robotaxi into 1.0

# Conflicts:
# xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpClockIn.java
# xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/service/KafkaConsumerService.java
# xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/util/XPUUtil.java
# xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpClockInService.java
# xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
# xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/job/jobs/ElectronicEffectJob.java
# xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpVehicleController.java
# xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java
4个文件已添加
25个文件已修改
968 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpClockIn.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/service/KafkaConsumerService.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/util/XPUUtil.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/resources/application-local.yml 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/resources/i18n/messages.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-itinerary-boot/src/main/resources/i18n/messages_zh_CN.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/ClockInScheduleTask.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpClockInService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/constant/I18nKeyConstant.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/schedule/ElectronicFenceFailureCompensationTask.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpElectronicFenceService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/i18n/messages.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/i18n/messages_en_US.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/i18n/messages_zh_CN.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/utils/mango/VehiclePositionService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/exportExcel/XpOrderExportExcel.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/vo/XpOrderPageVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-traffic-control-boot/src/main/java/com/xiaopeng/trafficControl/TrafficControlApplication.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-traffic-control-boot/src/main/resources/application-local.yml 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-traffic-control-boot/src/main/resources/bootstrap.yml 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpClockIn.java
@@ -3,6 +3,12 @@
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xiaopeng.common.core.web.domain.BaseModel;
import io.swagger.annotations.ApiModel;
@@ -10,6 +16,7 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import net.bytebuddy.dynamic.loading.InjectionClassLoader;
import java.time.LocalDateTime;
@@ -34,6 +41,7 @@
    private Long id;
    @ApiModelProperty(value = "用户id")
    @TableField(updateStrategy = FieldStrategy.NEVER)
    private Long appUserId;
    @ApiModelProperty(value = "上班打卡日期")
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/service/KafkaConsumerService.java
@@ -73,6 +73,8 @@
@Service
public class KafkaConsumerService {
    private static final String REASSIGN_CANCEL_CONTEXT_PREFIX = "REASSIGN_CANCEL_CONTEXT:";
    private static final String REASSIGN_CANCEL_CONTEXT_PREFIX = "REASSIGN_CANCEL_CONTEXT:";
    private static final String CANCEL_ORDER_CONTEXT_PREFIX = "CANCEL_ORDER_CONTEXT:";
    @Resource
@@ -1527,6 +1529,25 @@
        }
    }
    /**
     * 兼容按订单维度缓存的改派取消上下文。
     * 该键在历史实现中使用,保留读取能力避免线上旧数据失效。
     */
    private JSONObject getReassignCancelContext(String orderId) {
        if (StringUtils.isEmpty(orderId)) {
            return null;
        }
        Object cacheObject = redisService.getCacheObject(REASSIGN_CANCEL_CONTEXT_PREFIX + orderId);
        if (cacheObject == null) {
            return null;
        }
        return JSON.parseObject(cacheObject.toString());
    }
    /**
     * 当前 cancelOrder 指令是按 taskId 记录取消上下文。
     * 改派旧车取消回调优先读取该上下文,避免依赖订单当前车/状态造成竞态误判。
     */
    private JSONObject getReassignCancelContext(String orderId) {
        if (StringUtils.isEmpty(orderId)) {
            return null;
@@ -1542,8 +1563,39 @@
        if (OrderStateChangeSource.ORDER_STATE_CHANGE_SOURCE_VEHICLE != orderStatusInfo.getChangeSource()) {
            return null;
        }
        JSONObject cancelContext = getReassignCancelContext(orderId);
        String taskId = orderStatusInfo.getTaskId();
        // 先按 taskId 读取当前生效的取消上下文,再兼容历史按 orderId 存储的改派上下文。
        JSONObject cancelContext = getCancelOrderContext(taskId);
        if (cancelContext == null) {
            cancelContext = getReassignCancelContext(orderId);
        }
        if (cancelContext == null) {
            log.info("改派取消回调未命中专用上下文,orderId: {}, taskId: {}, missReason: context_not_found",
                    orderId, taskId);
            return null;
        }
        String cancelScene = cancelContext.getString("cancelScene");
        if (CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE != CancelOrderSceneEnum.fromCode(cancelScene)) {
            log.info("改派取消回调未命中专用上下文,orderId: {}, taskId: {}, missReason: scene_not_match, cancelScene: {}, context: {}",
                    orderId, taskId, cancelScene, cancelContext);
            return null;
        }
        String contextOrderId = cancelContext.getString("orderId");
        if (StringUtils.isNotEmpty(contextOrderId) && !Objects.equals(contextOrderId, orderId)) {
            log.info("改派取消回调未命中专用上下文,orderId: {}, taskId: {}, missReason: order_not_match, contextOrderId: {}, context: {}",
                    orderId, taskId, contextOrderId, cancelContext);
            return null;
        }
        return cancelContext;
    }
    private String resolveReassignCancelResponseVin(JSONObject cancelContext) {
        if (cancelContext == null) {
            return null;
        }
        String vin = cancelContext.getString("vin");
        if (StringUtils.isNotEmpty(vin)) {
            return vin;
            return null;
        }
        if (CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE != CancelOrderSceneEnum.fromCode(
@@ -1563,6 +1615,17 @@
        if (vehicleMatched && statusMatched) {
            return cancelContext;
        }
        String oldVehicleVin = cancelContext.getString("oldVehicleVin");
        if (StringUtils.isNotEmpty(oldVehicleVin)) {
            return oldVehicleVin;
        }
        Long oldVehicleId = cancelContext.getLong("oldVehicleId");
        if (Objects.isNull(oldVehicleId)) {
            return null;
        }
        R<XpVehicle> vehicleResult = xpVehicleClient.queryById(oldVehicleId);
        XpVehicle xpVehicle = vehicleResult == null ? null : vehicleResult.getData();
        return xpVehicle == null ? null : xpVehicle.getVehicleVin();
        log.info("改派取消回调未命中专用上下文,orderId: {}, currentVehicleId: {}, newVehicleId: {}, currentStatus: {}",
                orderId, xpOrder.getVehicleId(), newVehicleId, currentStatus);
        return null;
xp-robotaxi-automatic-pilot-boot/src/main/java/com/xiaopeng/automaticPilot/util/XPUUtil.java
@@ -11,6 +11,7 @@
import com.xiaopeng.api.vo.automaticPilot.MqttPublishVo;
import com.xiaopeng.api.vo.automaticPilot.protobuf.*;
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.uuid.UUID;
import lombok.extern.slf4j.Slf4j;
@@ -225,7 +226,7 @@
        log.info("getMqttPublishTopic vin,{},xpuIdMapping,{}", vin, JSON.toJSONString(xpuIdMapping));
        if (xpuIdMapping == null || StringUtils.isEmpty(xpuIdMapping.get(XPU_ID_TYPE))) {
            log.error("根据vin查询车辆xpuid信息为空,vin={}", vin);
            throw new RuntimeException("根据vin查询车辆xpuid信息为空");
            throw new ServiceException("根据vin查询车辆xpuid信息为空");
        }
        return xpuIdMapping.get(XPU_ID_TYPE);
    }
xp-robotaxi-itinerary-boot/src/main/java/com/xiaopeng/itinerary/service/impl/XpPricingServiceImpl.java
@@ -236,12 +236,12 @@
        // 1. 基础数据查询
        Map<String, String> geocode = gdMapGeocodingUtil.geocode(routeAndFareDto.getOrigin().getLongitude().toString(), routeAndFareDto.getOrigin().getLatitude().toString());
        if(null == geocode){
            throw new ServiceException("获取城市信息失败", I18nKeyConstant.CITY_NOT_FOUND);
            throw new ServiceException("城市暂未开通服务", I18nKeyConstant.CITY_NOT_FOUND);
        }
        Long cityCode = routeAndFareDto.getOpenCityId();
        XpOpenCity openCities = xpOpenCityMapper.selectById(cityCode);
        if (openCities == null ) {
            throw new ServiceException("获取城市信息失败", I18nKeyConstant.CITY_NOT_FOUND);
            throw new ServiceException("城市暂未开通服务", I18nKeyConstant.CITY_NOT_FOUND);
        }
        routeAndFareDto.setOpenCityId(openCities.getId());
        //根据订单获取车型 获取定价策略
@@ -654,7 +654,7 @@
        XpOpenCity openCities = xpOpenCityMapper.selectById(dto.getOpenCityId());
        if (openCities == null) {
            throw new ServiceException("获取城市信息失败", I18nKeyConstant.CITY_NOT_FOUND);
            throw new ServiceException("城市暂未开通服务", I18nKeyConstant.CITY_NOT_FOUND);
        }
        XpOpenCity xpOpenCity = openCities;
        dto.setOpenCityId(xpOpenCity.getId());
xp-robotaxi-itinerary-boot/src/main/resources/application-local.yml
New file
@@ -0,0 +1,108 @@
spring:
  config:
    activate:
      on-profile: local
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  redis:
    database: 99
    host: r-bp1ip8die60uuo3ovypd.redis.rds.aliyuncs.com
    password: syAktm2x9uDusoF_
    port: 6379
  data:
    mongodb:
      host: dds-bp1f4a38f3cccad4-pub.mongodb.rds.aliyuncs.com
      port: 3717
      database: xp_robotaxi
      username: xp_robotaxi
      password: hDXSBe_hs0d5KxUf
  shardingsphere:
    datasource:
      names: xp0
      xp0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://pc-bp1o444934f546768po.rwlb.rds.aliyuncs.com:3306/xp_rt_order?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: xp_rt_order
        password: JLZtAI_BL4Lv5mbE
        druid:
          initial-size: 10
          min-idle: 10
          max-active: 100
          max-wait: 60000
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          filters: stat,wall,log4j
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          remove-abandoned: true
          remove-abandoned-timeout: 1800
          log-abandoned: true
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            login-username: admin
            login-password: admin
            reset-enable: false
          wall:
            config:
              multi-statement-allow: false
    rules:
      sharding:
        tables:
          sys_oper_log:
            actual-data-nodes: xp0.sys_oper_log_${0..9}
            table-strategy:
              standard:
                sharding-column: userId
                sharding-algorithm-name: sys-oper-log-inline
            key-generate-strategy:
              column: oper_id
              key-generator-name: snowflake
          xp_agreement_app_user:
            actual-data-nodes: xp0.xp_agreement_app_user_${0..9}
            table-strategy:
              standard:
                sharding-column: app_user_id
                sharding-algorithm-name: xp-agreement-app-user-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
          xp_point:
            actual-data-nodes: xp0.xp_point_${0..9}
            table-strategy:
              standard:
                sharding-column: electronic_area_id
                sharding-algorithm-name: xp-point-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
        sharding-algorithms:
          sys-oper-log-inline:
            type: INLINE
            props:
              algorithm-expression: sys_oper_log_${userId % 10}
          xp-agreement-app-user-inline:
            type: INLINE
            props:
              algorithm-expression: xp_agreement_app_user_${app_user_id % 10}
          xp-point-inline:
            type: INLINE
            props:
              algorithm-expression: xp_point_${electronic_area_id % 10}
        key-generators:
          snowflake:
            type: SNOWFLAKE
      single-table:
        default-data-source-name: xp0
    props:
      sql-show: true
      sql-simple: true
seata:
  enabled: false
xp-robotaxi-itinerary-boot/src/main/resources/i18n/messages.properties
@@ -1,5 +1,5 @@
# 中文
city.not.found=获取城市信息失败
city.not.found=城市暂未开通服务
coupon.not.satisfy=券不满足条件
no.current.point=该站点已停止运营
no.current.region=当前区域未开通服务
xp-robotaxi-itinerary-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -1,5 +1,5 @@
# 中文
city.not.found=获取城市信息失败
city.not.found=城市暂未开通服务
coupon.not.satisfy=券不满足条件
no.current.point=该站点已停止运营
no.current.region=当前区域未开通服务
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/ClockInScheduleTask.java
@@ -90,39 +90,30 @@
                        List<Long> fleetIdIn = clockOutPlans.stream().map(XpSchedulingDetail::getFleetId).collect(Collectors.toList());
                        List<Long> operationsUserIds = xpFleetOperations.stream().filter(x -> fleetIdIn.contains(x.getFleetId()))
                                .map(XpFleetOperations::getOperationsUserId).distinct().collect(Collectors.toList());
                        // 批量查询人员打卡信息
                        List<XpClockIn> clockInList = xpClockInService.list(Wrappers.lambdaQuery(XpClockIn.class)
                                .in(XpClockIn::getAppUserId, operationsUserIds).in(XpClockIn::getShiftId, ids));
                        // 判断有哪些人员没有打卡信息
                        List<Long> noClockInUserIds = operationsUserIds.stream().filter(id -> clockInList.stream().noneMatch(clockIn -> clockIn.getAppUserId().equals(id))).collect(Collectors.toList());
                        if(!CollectionUtils.isEmpty(noClockInUserIds)){
                            log.info("========== 获取到{}个人员没有打卡信息 ==========", noClockInUserIds.size());
                            // 添加上班缺卡信息
                            List<XpClockIn> noClockInList = new ArrayList<>();
                            for (Long noClockInUserId : noClockInUserIds) {
                                // 通过用户id找到车队id
                                List<XpFleetOperations> operations = xpFleetOperations.stream().filter(x -> x.getOperationsUserId().equals(noClockInUserId)).collect(Collectors.toList());
                                if(!CollectionUtils.isEmpty(operations)){
                                    Long fleetId = operations.get(0).getFleetId();
                                    // 通过车队id和已过期的班次获取排班明细
                                    List<XpSchedulingDetail> clockOutPlanExpiredList = clockOutPlanList.stream()
                                            .filter(xpSchedulingDetail -> xpSchedulingDetail.getFleetId().equals(fleetId)
                                                    && ids.contains(xpSchedulingDetail.getShiftId())).collect(Collectors.toList());
                                    if(!CollectionUtils.isEmpty(clockOutPlanExpiredList)){
                                        Long shiftId = clockOutPlanExpiredList.get(0).getShiftId();
                                        XpClockIn xpClockIn = new XpClockIn();
                                        xpClockIn.setAppUserId(noClockInUserId);
                                        xpClockIn.setClockInTime(LocalDateTime.now());
                                        xpClockIn.setClockInResult(3);
                                        xpClockIn.setShiftId(shiftId);
                                        noClockInList.add(xpClockIn);
                                    }
                        if(!CollectionUtils.isEmpty(operationsUserIds)){
                            int createdCount = 0;
                            for (Long operationsUserId : operationsUserIds) {
                                List<XpFleetOperations> operations = xpFleetOperations.stream()
                                        .filter(x -> x.getOperationsUserId().equals(operationsUserId))
                                        .collect(Collectors.toList());
                                if(CollectionUtils.isEmpty(operations)){
                                    continue;
                                }
                                Long fleetId = operations.get(0).getFleetId();
                                List<XpSchedulingDetail> clockOutPlanExpiredList = clockOutPlanList.stream()
                                        .filter(xpSchedulingDetail -> xpSchedulingDetail.getFleetId().equals(fleetId)
                                                && ids.contains(xpSchedulingDetail.getShiftId()))
                                        .collect(Collectors.toList());
                                if(CollectionUtils.isEmpty(clockOutPlanExpiredList)){
                                    continue;
                                }
                                Long shiftId = clockOutPlanExpiredList.get(0).getShiftId();
                                boolean created = xpClockInService.createMissingClockInIfAbsent(operationsUserId, shiftId, LocalDate.now());
                                if (created) {
                                    createdCount++;
                                }
                            }
                            if (!CollectionUtils.isEmpty(noClockInList)){
                                log.info("添加上班缺卡信息:{}", noClockInList);
                                xpClockInService.saveBatch(noClockInList);
                            }
                            log.info("========== 新增上班缺卡记录{}条 ==========", createdCount);
                        }
                    }
@@ -154,7 +145,6 @@
                                for (XpClockIn xpClockIn : clockInListFive) {
                                    xpClockIn.setClockOutTime(LocalDateTime.now());
                                    xpClockIn.setClockOutResult(3);
                                    xpClockIn.setAppUserId(null);
                                }
                                xpClockInService.updateBatchById(clockInListFive);
                                log.info("========== 修改为下班缺卡 ==========");
@@ -198,7 +188,6 @@
                        log.info("========== 获取到{}个人员没有下班打卡信息 ==========", clockInListFive.size());
                        if(!CollectionUtils.isEmpty(clockInListFive)){
                            for (XpClockIn xpClockIn : clockInListFive) {
                                xpClockIn.setAppUserId(null);
                                xpClockIn.setClockOutTime(LocalDateTime.now());
                                xpClockIn.setClockOutResult(3);
                            }
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpClockInService.java
@@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
/**
 * <p>
@@ -43,7 +44,18 @@
     * @param dto
     * @return
     */
    R<Integer> clockOut(ClockDTO dto);
    R<String> clockOut(ClockDTO dto);
    /**
     * 补充上班缺卡记录
     *
     * @param appUserId 用户ID
     * @param shiftId 班次ID
     * @param attendanceDate 考勤日期
     * @return 是否新增成功
     */
    boolean createMissingClockInIfAbsent(Long appUserId, Long shiftId, LocalDate attendanceDate);
    /**
     * 检查用户是否上班中
     * @param appUserId 用户ID
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
@@ -11,6 +11,7 @@
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.account.XpOperationUserNotificationClient;
import com.xiaopeng.api.feignClient.pushMessage.XpAliPushMessageClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetOperationsClient;
import com.xiaopeng.api.model.account.SysRole;
import com.xiaopeng.api.model.account.XpOperationsRole;import com.xiaopeng.api.model.account.XpOperationsUser;
@@ -19,6 +20,7 @@
import com.xiaopeng.api.model.maintenance.XpClockIn;
import com.xiaopeng.api.model.maintenance.XpSchedulingDetail;
import com.xiaopeng.api.model.maintenance.XpShift;
import com.xiaopeng.api.model.vehicleConvoy.XpFleet;
import com.xiaopeng.api.vo.account.XpOperationsUserVO;
import com.xiaopeng.api.vo.account.XpOperationsUserWithRoleVO;import com.xiaopeng.common.core.constant.Constants;
import com.xiaopeng.common.core.domain.R;
@@ -37,6 +39,8 @@
import com.xiaopeng.maintenance.vo.ClockInStatisticsDetailsVo;
import com.xiaopeng.maintenance.vo.ClockInStatisticsVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -72,6 +76,10 @@
@Service
public class XpClockInServiceImpl extends ServiceImpl<XpClockInMapper, XpClockIn> implements IXpClockInService {
    private static final String CLOCK_ATTENDANCE_LOCK_PREFIX = "CLOCK_ATTENDANCE_";
    private static final long CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS = 1L;
    private static final long CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS = 10L;
    @Resource
    private XpCheckLocationMapper xpCheckLocationMapper;
    @Resource
@@ -86,11 +94,16 @@
    private IXpSchedulingDetailService xpSchedulingDetailService;
    @Resource
    private XpFleetClient xpFleetClient;
    @Resource
    private XpFleetOperationsClient fleetOperationsClient;
    @Resource
    private XpOperationUserNotificationClient xpOperationUserNotificationClient;
    @Resource
    private XpAliPushMessageClient xpAliPushMessageClient;
    @Resource
    private RedissonClient redissonClient;
@@ -183,30 +196,124 @@
            return R.fail("打卡失败");
        }
        // 查询上班打卡信息
        XpClockIn xpClockIn = xpClockInMapper.selectOne(Wrappers.lambdaQuery(XpClockIn.class)
                .eq(XpClockIn::getAppUserId, dto.getOperationsUserId())
                .likeRight(XpClockIn::getClockInTime, LocalDate.now())
        LocalDate attendanceDate = LocalDate.now();
        RLock lock = redissonClient.getLock(buildAttendanceLockKey(dto.getOperationsUserId(), attendanceDate));
        try {
            boolean isLocked = lock.tryLock(CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS,
                    CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
            if (!isLocked) {
                return R.fail("系统繁忙,请稍后重试");
            }
            LocalDateTime now = LocalDateTime.now();
            XpClockIn xpClockIn = findTodayClockInRecord(dto.getOperationsUserId(), attendanceDate);
            boolean isNewRecord = false;
            if (Objects.isNull(xpClockIn)) {
                Long shiftId = resolveClockOutShiftId(dto.getOperationsUserId(), dto.getShiftId(), attendanceDate);
                if (Objects.isNull(shiftId)) {
                    return R.fail("班次不存在");
                }
                xpClockIn = buildMissingClockInRecord(dto.getOperationsUserId(), shiftId, now);
                isNewRecord = true;
            }
            XpShift xpShift = xpShiftMapper.selectById(xpClockIn.getShiftId());
            if (null == xpShift) {
                return R.fail("班次不存在");
            }
            fillClockOutInfo(xpClockIn, dto, xpShift, now, attendanceDate);
            if (isNewRecord) {
                this.save(xpClockIn);
            } else {
                this.updateById(xpClockIn);
            }
            sendAgreementUpdateNotification(now);
            return R.ok();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("下班打卡获取考勤锁被中断, appUserId={}", dto.getOperationsUserId(), e);
            return R.fail("系统繁忙,请稍后重试");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    @Override
    public boolean createMissingClockInIfAbsent(Long appUserId, Long shiftId, LocalDate attendanceDate) {
        if (Objects.isNull(appUserId) || Objects.isNull(shiftId) || Objects.isNull(attendanceDate)) {
            return false;
        }
        RLock lock = redissonClient.getLock(buildAttendanceLockKey(appUserId, attendanceDate));
        try {
            boolean isLocked = lock.tryLock(CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS,
                    CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
            if (!isLocked) {
                log.info("补上班缺卡获取锁失败, appUserId={}, attendanceDate={}", appUserId, attendanceDate);
                return false;
            }
            XpClockIn existingClockIn = findTodayClockInRecord(appUserId, attendanceDate);
            if (Objects.nonNull(existingClockIn)) {
                return false;
            }
            XpClockIn missingClockIn = buildMissingClockInRecord(appUserId, shiftId, LocalDateTime.now());
            return this.save(missingClockIn);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("补上班缺卡获取考勤锁被中断, appUserId={}, attendanceDate={}", appUserId, attendanceDate, e);
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    private XpClockIn findTodayClockInRecord(Long appUserId, LocalDate attendanceDate) {
        LocalDateTime startTime = attendanceDate.atStartOfDay();
        LocalDateTime endTime = attendanceDate.plusDays(1).atStartOfDay();
        return xpClockInMapper.selectOne(Wrappers.lambdaQuery(XpClockIn.class)
                .eq(XpClockIn::getAppUserId, appUserId)
                .ge(XpClockIn::getClockInTime, startTime)
                .lt(XpClockIn::getClockInTime, endTime)
                .orderByDesc(XpClockIn::getClockInTime)
                .last("LIMIT 1"));
        if (Objects.isNull(xpClockIn)) {
            return R.fail("请先上班打卡");
        }
    }
        // 查询班次
        XpShift xpShift = xpShiftMapper.selectById(xpClockIn.getShiftId());
        if (null == xpShift) {
            return R.fail("班次不存在");
    private Long resolveClockOutShiftId(Long appUserId, Long fallbackShiftId, LocalDate attendanceDate) {
        R<XpFleet> fleetResult = xpFleetClient.getFleetByOperationsUserId(appUserId);
        XpFleet xpFleet = Objects.isNull(fleetResult) ? null : fleetResult.getData();
        if (Objects.nonNull(xpFleet)) {
            XpSchedulingDetail schedulingDetail = xpSchedulingDetailService.getOne(Wrappers.lambdaQuery(XpSchedulingDetail.class)
                    .eq(XpSchedulingDetail::getFleetId, xpFleet.getId())
                    .eq(XpSchedulingDetail::getPlanTime, attendanceDate)
                    .last("LIMIT 1"));
            if (Objects.nonNull(schedulingDetail) && Objects.nonNull(schedulingDetail.getShiftId())) {
                return schedulingDetail.getShiftId();
            }
        }
        // 开始打卡时间
        String endTime = xpShift.getEndTime();
        LocalDateTime endLocalDateTime = DateUtils.stringToLocalDateTime(LocalDate.now() + " " + endTime + ":00");
        return fallbackShiftId;
    }
        LocalDateTime now = LocalDateTime.now();
    private XpClockIn buildMissingClockInRecord(Long appUserId, Long shiftId, LocalDateTime now) {
        XpClockIn xpClockIn = new XpClockIn();
        xpClockIn.setAppUserId(appUserId);
        xpClockIn.setClockInTime(now);
        xpClockIn.setClockInResult(3);
        xpClockIn.setShiftId(shiftId);
        return xpClockIn;
    }
    private void fillClockOutInfo(XpClockIn xpClockIn, ClockDTO dto, XpShift xpShift,
                                  LocalDateTime now, LocalDate attendanceDate) {
        LocalDateTime endLocalDateTime = DateUtils.stringToLocalDateTime(attendanceDate + " " + xpShift.getEndTime() + ":00");
        xpClockIn.setClockOutTime(now);
        xpClockIn.setClockOutLon(dto.getUserLon());
        xpClockIn.setClockOutLat(dto.getUserLat());
        xpClockIn.setClockOutAddress(dto.getClockOutAddress());
        if (xpClockIn.getClockOutTime().isBefore(endLocalDateTime)) {
        if (now.isBefore(endLocalDateTime)) {
            xpClockIn.setClockOutResult(2);
        } else {
            xpClockIn.setClockOutResult(1);
@@ -217,6 +324,10 @@
        return R.ok(xpClockIn.getClockOutResult());
    }
    private String buildAttendanceLockKey(Long appUserId, LocalDate attendanceDate) {
        return CLOCK_ATTENDANCE_LOCK_PREFIX + appUserId + "_" + attendanceDate;
    }
    @Override
    public Integer checkWorkStatus(Long appUserId) {
        if (appUserId == null) {
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -245,24 +245,24 @@
        if (Objects.isNull(vehicle)) {
            throw new ServiceException("上报失败,未获取到车辆信息", I18nKeyConstant.TASK_VEHICLE_NOT_EXIST);
        }
        VehiclePosition vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("vin").is(vehicle.getVehicleVin())), VehiclePosition.class);
        if (Objects.isNull(vehiclePosition)) {
            log.error("事故上报未获取到车辆当前定位");
            throw new ServiceException("获取车辆实时定位信息失败,请稍后重试", I18nKeyConstant.TASK_SAVE_ERROR);
        }
        xpTask.setVehicleLon(new BigDecimal(vehiclePosition.getPoint().getX()));
        xpTask.setVehicleLat(new BigDecimal(vehiclePosition.getPoint().getY()));
        Map<String, String> geocode = gdMapGeocodingUtil.geocode(xpTask.getVehicleLon().toString(), xpTask.getVehicleLat().toString());
        String address = geocode == null ? null : geocode.get("address");
        if (StringUtils.isBlank(address)) {
            log.error("事故上报通过车辆定位从高德获取地址信息失败");
            throw new ServiceException("获取车辆实时定位信息失败,请稍后重试", I18nKeyConstant.TASK_SAVE_ERROR);
        }
        xpTask.setVehicleLocation(address);
        // VehiclePosition vehiclePosition = mongoTemplate.findOne(new Query(Criteria.where("vin").is(vehicle.getVehicleVin())), VehiclePosition.class);
        // if (Objects.isNull(vehiclePosition)) {
        //     log.error("事故上报未获取到车辆当前定位");
        //     throw new ServiceException("获取车辆实时定位信息失败,请稍后重试", I18nKeyConstant.TASK_SAVE_ERROR);
        // }
        // xpTask.setVehicleLon(new BigDecimal(vehiclePosition.getPoint().getX()));
        // xpTask.setVehicleLat(new BigDecimal(vehiclePosition.getPoint().getY()));
        // Map<String, String> geocode = gdMapGeocodingUtil.geocode(xpTask.getVehicleLon().toString(), xpTask.getVehicleLat().toString());
        // String address = geocode == null ? null : geocode.get("address");
        // if (StringUtils.isBlank(address)) {
        //     log.error("事故上报通过车辆定位从高德获取地址信息失败");
        //     throw new ServiceException("获取车辆实时定位信息失败,请稍后重试", I18nKeyConstant.TASK_SAVE_ERROR);
        // }
        // xpTask.setVehicleLocation(address);
// 临时处理
//         xpTask.setVehicleLon(new BigDecimal("104.055683"));
//         xpTask.setVehicleLat(new BigDecimal("30.585991"));
//         xpTask.setVehicleLocation("四川省成都市武侯区石羊街道天府新谷");
        xpTask.setVehicleLon(new BigDecimal("104.055683"));
        xpTask.setVehicleLat(new BigDecimal("30.585991"));
        xpTask.setVehicleLocation("四川省成都市武侯区石羊街道天府新谷");
        xpTask.setVehicleId(dto.getId());
        xpTask.setReportUserId(xpOperationsUser.getId());
        xpTask.setReportTime(LocalDateTime.now());
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/constant/I18nKeyConstant.java
@@ -31,6 +31,8 @@
    String FENCE_STRUCTURE_NOT_EXIST = "fence.structure.not.exist";
    // 围栏不存在
    String FENCE_NOT_EXIST = "fence.not.exist";
    // 围栏名称已存在
    String FENCE_NAME_EXISTS = "fence.name.exists";
    // 围栏启用状态不可复制
    String FENCE_STATUS_CANNOT_COPIED = "fence.status.cannot.copied";
@@ -97,6 +99,8 @@
    String POINT_NUMBER_LOCK_FAIL = "point.number.lock.fail";
    // 点位-编号解析失败
    String POINT_NUMBER_PARSE_FAIL = "point.number.parse.fail";
    // 点位-名称已存在
    String POINT_NAME_EXISTS = "point.name.exists";
    // 点位删除-存在关联巡游点位
    String POINT_DELETE_HAS_CRUISE_PLAN_POINT = "point.delete.has.cruise.plan.point";
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java
@@ -199,6 +199,7 @@
        if (xpPoint == null) {
            throw new ServiceException("系统异常", I18nKeyConstant.SYSTEM_EXCEPTION);
        }
        xpPointService.validatePointNameUnique(pointAddDto.getPointName(), pointAddDto.getId());
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        if (Integer.valueOf(2).equals(pointAddDto.getOperationStatus()) && Integer.valueOf(5).equals(pointAddDto.getVerificationStatus())) {
            Long electronicAreaId = xpPoint.getElectronicAreaId();
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/schedule/ElectronicFenceFailureCompensationTask.java
New file
@@ -0,0 +1,81 @@
package com.xiaopeng.map.schedule;
import com.xiaopeng.api.model.operation.XpElectronicFence;
import com.xiaopeng.map.service.XpElectronicFenceService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
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;
import java.util.stream.Collectors;
/**
 * 电子围栏失效补偿定时任务
 */
@Slf4j
@Component
public class ElectronicFenceFailureCompensationTask {
    private static final String LOCK_KEY = "MAP:ELECTRONIC_FENCE:FAILURE_COMPENSATE";
    private static final int LOCK_WAIT_SECONDS = 0;
    private static final int LOCK_LEASE_SECONDS = 15;
    @Resource
    private XpElectronicFenceService xpElectronicFenceService;
    @Resource
    private RedissonClient redissonClient;
    @Scheduled(fixedDelay = 10 * 1000L)
    public void compensateExpiredFence() {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        boolean locked = false;
        try {
            locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
            if (!locked) {
                return;
            }
            LocalDateTime now = LocalDateTime.now();
            List<XpElectronicFence> expiredFenceList = xpElectronicFenceService.lambdaQuery()
                    .eq(XpElectronicFence::getFailureType, 2)
                    .le(XpElectronicFence::getFailureTime, now)
                    .in(XpElectronicFence::getStatus, 1, 2)
                    .list();
            if (expiredFenceList == null || expiredFenceList.isEmpty()) {
                return;
            }
            List<Long> electronicIds = expiredFenceList.stream()
                    .map(XpElectronicFence::getId)
                    .collect(Collectors.toList());
            log.info("电子围栏失效补偿开始, count={}, ids={}", electronicIds.size(), electronicIds);
            for (Long electronicId : electronicIds) {
                try {
                    xpElectronicFenceService.expireFence(electronicId);
                } catch (Exception e) {
                    log.error("电子围栏失效补偿单条处理异常, electronicId={}", electronicId, e);
                }
            }
            log.info("电子围栏失效补偿完成, count={}, ids={}", electronicIds.size(), electronicIds);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("电子围栏失效补偿任务获取锁被中断", e);
        } catch (Exception e) {
            log.error("电子围栏失效补偿任务执行异常", e);
        } finally {
            try {
                if (locked && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            } catch (Exception e) {
                log.warn("释放电子围栏失效补偿锁失败 lockKey={}", LOCK_KEY, e);
            }
        }
    }
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpElectronicFenceService.java
@@ -27,6 +27,11 @@
    R editElectronic(ElectronicFenceUpdateDto dto);
    /**
     * 校验围栏名称唯一
     */
    void validateElectronicNameUnique(String electronicName, Long excludeId);
    /**
     * 查询启用的电子围栏列表
     * @return
     */
@@ -37,9 +42,10 @@
     * @return
     */
    List<XpElectronicFence> getList();
    /**
     * 将围栏置为已停用,并同步禁用围栏区域
     * @param electronicId 围栏ID
     */
    void expireFence(Long electronicId);
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java
@@ -41,6 +41,11 @@
    XpPoint addPointWithGeneratedNumber(@Valid PointAddDto pointAddDto);
    /**
     * 校验点位名称唯一
     */
    void validatePointNameUnique(String pointName, Long excludeId);
    /**
     * 获取附近上车站点
     */
    NearestVo nearest(@Valid NearestDto dto);
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java
@@ -215,6 +215,7 @@
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    public R addElectronic(ElectronicFenceAddDto dto) {
        validateElectronicNameUnique(dto.getElectronicName(), null);
        // 获取现有的运营区域(areaType=1)
        LambdaQueryWrapper<XpElectronicFenceArea> xpElectronicFenceAreaLambdaQueryWrapper = new LambdaQueryWrapper<>();
        xpElectronicFenceAreaLambdaQueryWrapper.eq(XpElectronicFenceArea::getAreaType, 1);
@@ -347,6 +348,7 @@
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    public R editElectronic(ElectronicFenceUpdateDto dto) {
        validateElectronicNameUnique(dto.getElectronicName(), dto.getId());
        // 获取现有的运营区域(areaType=1),排除当前正在编辑的电子围栏
        LambdaQueryWrapper<XpElectronicFenceArea> xpElectronicFenceAreaLambdaQueryWrapper = new LambdaQueryWrapper<>();
        xpElectronicFenceAreaLambdaQueryWrapper.eq(XpElectronicFenceArea::getAreaType, 1);
@@ -452,7 +454,9 @@
            // 设置电子围栏ID并保存区域数据
            for (XpElectronicFenceArea area : updatedElectronicFenceAreas) {
                area.setElectronicId(xpElectronicFence.getId());
                area.setAreaStatus(1);
                if (area.getAreaType() == null || area.getAreaType() == 1) {
                    area.setAreaStatus(1);
                }
                area.setArea(area.getArea());
                area.setCityCode(dto.getCityCode());
                if (null != area.getId()) {
@@ -465,7 +469,9 @@
            // 设置电子围栏ID并保存区域数据
            for (XpElectronicFenceArea area : updatedElectronicFenceAreas) {
                area.setElectronicId(xpElectronicFence.getId());
                area.setAreaStatus(2);
                if (area.getAreaType() == null || area.getAreaType() == 1) {
                    area.setAreaStatus(2);
                }
                area.setArea(area.getArea());
                area.setCityCode(dto.getCityCode());
                if (null != area.getId()) {
@@ -535,6 +541,12 @@
                        old.setPoints(geoJsonPolygon);
                        mongoTemplate.save(old);
                    }
                } else if (newElectronicFenceArea.getAreaType() == 2
                        && newElectronicFenceArea.getId() != null) {
                    Query query = new Query();
                    query.addCriteria(Criteria.where("fenceId").is(String.valueOf(xpElectronicFence.getId())));
                    query.addCriteria(Criteria.where("areaId").is(String.valueOf(newElectronicFenceArea.getId())));
                    mongoTemplate.remove(query, XpGeoFencingProhibitedPolygon.class);
                }
            }
        }
@@ -756,6 +768,21 @@
        return R.ok();
    }
    @Override
    public void validateElectronicNameUnique(String electronicName, Long excludeId) {
        if (!StringUtils.hasText(electronicName)) {
            return;
        }
        long count = this.lambdaQuery()
                .eq(XpElectronicFence::getElectronicName, electronicName)
                .ne(excludeId != null, XpElectronicFence::getId, excludeId)
                .count();
        if (count > 0) {
            throw new ServiceException("围栏名称已存在", I18nKeyConstant.FENCE_NAME_EXISTS);
        }
    }
    private static List<Long> normalizePointIds(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
@@ -965,6 +992,36 @@
        return list();
    }
    @Override
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    public void expireFence(Long electronicId) {
        if (electronicId == null) {
            return;
        }
        XpElectronicFence electronicFence = this.getById(electronicId);
        if (electronicFence == null || electronicFence.getStatus() == null) {
            return;
        }
        Integer status = electronicFence.getStatus();
        if (status != 1 && status != 2) {
            return;
        }
        electronicFence.setStatus(3);
        this.updateById(electronicFence);
        List<XpElectronicFenceArea> electronicFenceAreaList = xpElectronicFenceAreaService.lambdaQuery()
                .eq(XpElectronicFenceArea::getElectronicId, electronicId)
                .list();
        if (CollUtil.isEmpty(electronicFenceAreaList)) {
            return;
        }
        for (XpElectronicFenceArea electronicFenceArea : electronicFenceAreaList) {
            electronicFenceArea.setAreaStatus(2);
        }
        xpElectronicFenceAreaService.updateBatchById(electronicFenceAreaList);
    }
    /**
     * 计算围栏中心点
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -128,6 +128,7 @@
        if (pointAddDto.getElectronicAreaId() == null) {
            throw new ServiceException("运营区域ID不能为空", I18nKeyConstant.POINT_ELECTRONIC_AREA_ID_EMPTY);
        }
        validatePointNameUnique(pointAddDto.getPointName(), null);
        String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId());
        String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE);
@@ -165,6 +166,20 @@
        xpPoint.setPointNumber(pointNumber);
        this.save(xpPoint);
        return xpPoint;
    }
    @Override
    public void validatePointNameUnique(String pointName, Long excludeId) {
        if (!StringUtils.hasText(pointName)) {
            return;
        }
        long count = this.lambdaQuery()
                .eq(XpPoint::getPointName, pointName)
                .ne(excludeId != null, XpPoint::getId, excludeId)
                .count();
        if (count > 0) {
            throw new ServiceException("点位名称已存在", I18nKeyConstant.POINT_NAME_EXISTS);
        }
    }
    @Override
@@ -324,7 +339,10 @@
        List<XpPoint> list = this.baseMapper.listAll(query);
        for (XpPoint xpPoint : list) {
            String pointPictures = xpPoint.getPointPictures();
            log.info("pointPictures:{}", pointPictures);
            if (StringUtils.isNotBlank(pointPictures)) {
                log.info("pointPictures进入处理:{}", pointPictures);
                JSONArray jsonArray = JSONArray.parseArray(pointPictures);
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
@@ -334,12 +352,15 @@
                    String downloadUrl ="";
                    try {
                        downloadUrl = aliOssService.getDownloadUrl(jsonObject.getString("id"), jsonObject.getString("name"), true);
                        log.info("downloadUrl:{}", downloadUrl);
                    } catch (UnsupportedEncodingException e) {
                        log.error("获取图片下载地址失败", e);
                    }
                    jsonObject.put("url", downloadUrl);
                }
                log.info("jsonArray:{}", jsonArray);
                xpPoint.setPointPictures(JSONArray.toJSONString(jsonArray));
                log.info("xpPoint:{}", xpPoint.getPointPictures());
            }
        }
xp-robotaxi-map-boot/src/main/resources/i18n/messages.properties
@@ -10,6 +10,7 @@
area.overlap.error=运营区域{0}与现有运营区域{1}发生重叠,请编辑已有的运营区域;
fence.structure.not.exist=围栏构成不存在
fence.not.exist=围栏不存在
fence.name.exists=围栏名称已存在
fence.status.cannot.copied=围栏启用状态不可复制;
order.create.fail=订单创建失败
open.city.exists=该城市已存在
@@ -39,6 +40,7 @@
point.city.not.exist=城市信息不存在
point.number.lock.fail=点位编号生成繁忙,请稍后重试
point.number.parse.fail=点位编号生成失败(历史编号格式异常)
point.name.exists=点位名称已存在
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游计划点位
point.enable.not.in.operating.area=当前点位不在围栏运营区域内,不能启用。
point.enable.in.prohibited.area=当前点位在禁行区内,不能启用。
xp-robotaxi-map-boot/src/main/resources/i18n/messages_en_US.properties
@@ -11,6 +11,7 @@
fence.structure.not.exist=The fence composition does not exist\
fence.status.cannot.copied=The enabled state of the fence cannot be copied;
fence.not.exist=The fence does not exist.
fence.name.exists=Fence name already exists
order.create.fail=Order creation failed
open.city.exists=This city already exists
pricing.exists=Pricing for this vehicle model/channel/city already exists
@@ -32,6 +33,7 @@
point.city.not.exist=City information does not exist
point.number.lock.fail=Point number generation is busy, please try again later
point.number.parse.fail=Point number generation failed (invalid historical number format)
point.name.exists=Point name already exists
point.delete.has.cruise.plan.point=Operation failed. This point is associated with cruise plan points.
point.enable.not.in.operating.area=This point is not within the fence operating area and cannot be enabled.
point.enable.in.prohibited.area=This point is in a prohibited area and cannot be enabled.
xp-robotaxi-map-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -10,6 +10,7 @@
area.overlap.error=运营区域{0}与现有运营区域{1}发生重叠,请编辑已有的运营区域;
fence.structure.not.exist=围栏构成不存在
fence.not.exist=围栏不存在
fence.name.exists=围栏名称已存在
fence.status.cannot.copied=围栏启用状态不可复制
order.create.fail=订单创建失败
open.city.exists=该城市已存在
@@ -32,6 +33,7 @@
point.city.not.exist=城市信息不存在
point.number.lock.fail=点位编号生成繁忙,请稍后重试
point.number.parse.fail=点位编号生成失败(历史编号格式异常)
point.name.exists=点位名称已存在
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游计划点位
point.enable.not.in.operating.area=当前点位不在围栏运营区域内,不能启用。
point.enable.in.prohibited.area=当前点位在禁行区内,不能启用。
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/utils/mango/VehiclePositionService.java
@@ -53,14 +53,17 @@
//                    .map(geoResult -> geoResult.getContent())
//                    .collect(Collectors.toList());
            //找到中心点
            // 找到中心点
            GeoJsonPoint geoJsonPoint = new GeoJsonPoint(longitude, latitude);
            //构造半径
            // 构造半径
            Distance distanceR = new Distance(distanceInMeters, Metrics.KILOMETERS);
            //画圆
            // 画圆
            Circle circle = new Circle(geoJsonPoint, distanceR);
            // 构造query对象
            Query query = Query.query(Criteria.where("point").withinSphere(circle));
            long latestAllowedAddTime = System.currentTimeMillis() - 60 * 1000;
            Query query = Query.query(new Criteria().andOperator(
                    Criteria.where("point").withinSphere(circle),
                    Criteria.where("addTime").gte(latestAllowedAddTime)
            ));
            List<VehiclePosition> vehiclePositions = mongoTemplate.find(query, VehiclePosition.class);
            return vehiclePositions;
        } catch (Exception e) {
@@ -70,6 +73,7 @@
    }
    /**
     * 根据车辆ID查询车辆信息
     * @param vehicleId 车辆ID
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/exportExcel/XpOrderExportExcel.java
@@ -43,11 +43,11 @@
    @ApiModelProperty(value = "起点")
    @Excel(name = "起点", width = 30)
    private String startAddress;
    private String startPointName;
    @ApiModelProperty(value = "终点")
    @Excel(name = "终点", width = 30)
    private String endAddress;
    private String endPointName;
    @ApiModelProperty(value = "选择车型")
    @Excel(name = "选择车型", width = 15)
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/vo/XpOrderPageVO.java
@@ -1,5 +1,6 @@
package com.xiaopeng.order.model.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -59,4 +60,10 @@
    private BigDecimal orderMoney;
    @ApiModelProperty(value = "状态 0=已取消,1=待接单,2=待出发,3=接驾中,4=待客上车,5=乘客已上车,6=服务中,7=待客下车,8=待付款,9=待评价,10=已完成")
    private Integer status;
    @ApiModelProperty(value = "下单站点名称")
    private String startPointName;
    @ApiModelProperty(value = "下单终点站点名称")
    private String endPointName;
}
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -135,6 +135,9 @@
@RequiredArgsConstructor(onConstructor_ = @Lazy)
public class XpOrderServiceImpl extends ServiceImpl<XpOrderMapper, XpOrder> implements IXpOrderService {
    private static final String REASSIGN_CANCEL_CONTEXT_PREFIX = "REASSIGN_CANCEL_CONTEXT:";
    private static final long REASSIGN_CANCEL_CONTEXT_TTL_MINUTES = 5L;
    private final UserResourcesClient userResourcesClient;
    private final XpPointClient xpPointClient;
    private final XpVehicleClient xpVehicleClient;
@@ -611,6 +614,8 @@
        vo.setEndAddress(order.getEndAddress());
        vo.setOrderMoney(order.getOrderMoney());
        vo.setStatus(order.getStatus());
        vo.setStartPointName(order.getStartPointName());
        vo.setEndPointName(order.getEndPointName());
        // 用户信息
        if (Objects.nonNull(order.getAppUserId()) && userInfoMap != null && userInfoMap.containsKey(order.getAppUserId().toString())) {
@@ -1005,6 +1010,8 @@
                    cancelDto.setOperation_type(OrderOperationType.CLOUD_CANCEL_ORDER);
                    cancelDto.setCancelScene(CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE.getCode());
                    orderMissionClient.cancelOrder(cancelDto);
                    cacheReassignCancelContext(orderId, oldVehicleId, newVehicleId,
                            oldVehicle.getVehicleVin(), currentStatus);
                    log.info("已取消原车辆订单指令,订单ID: {}, VIN: {}, taskId: {}, cancelScene: {}",
                            orderId, oldVehicle.getVehicleVin(), cancelDto.getTaskId(), cancelDto.getCancelScene());
@@ -1151,6 +1158,22 @@
    }
    private void cacheReassignCancelContext(Long orderId, Long oldVehicleId, Long newVehicleId,
                                            String oldVehicleVin, Integer preReassignStatus) {
        JSONObject context = new JSONObject();
        context.put("orderId", orderId);
        context.put("oldVehicleId", oldVehicleId);
        context.put("newVehicleId", newVehicleId);
        context.put("oldVehicleVin", oldVehicleVin);
        context.put("cancelScene", CancelOrderSceneEnum.REASSIGN_OLD_VEHICLE.getCode());
        context.put("operationTime", System.currentTimeMillis());
        context.put("preReassignStatus", preReassignStatus);
        redisService.setCacheObject(REASSIGN_CANCEL_CONTEXT_PREFIX + orderId, context.toJSONString(),
                REASSIGN_CANCEL_CONTEXT_TTL_MINUTES, TimeUnit.MINUTES);
        log.info("记录改派取消上下文成功,orderId: {}, oldVehicleId: {}, newVehicleId: {}, preReassignStatus: {}",
                orderId, oldVehicleId, newVehicleId, preReassignStatus);
    }
    /**
     * 为原车辆创建返程巡游订单
     * <p>
xp-robotaxi-traffic-control-boot/src/main/java/com/xiaopeng/trafficControl/TrafficControlApplication.java
@@ -7,7 +7,6 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
 * 合规监管
@@ -16,9 +15,8 @@
@EnableCustomSwagger2
@EnableRyFeignClients
@SpringBootApplication
@EnableScheduling//开启定时任务
@EnableScheduling
@ServletComponentScan
@EnableTransactionManagement//开启事务
public class TrafficControlApplication {
    public static void main(String[] args) {
        SpringApplication.run(TrafficControlApplication.class, args);
xp-robotaxi-traffic-control-boot/src/main/resources/application-local.yml
New file
@@ -0,0 +1,114 @@
spring:
  config:
    activate:
      on-profile: local
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  redis:
    database: 99
    host: r-bp1ip8die60uuo3ovypd.redis.rds.aliyuncs.com
    password: syAktm2x9uDusoF_
    port: 6379
  data:
    mongodb:
      host: dds-bp1f4a38f3cccad4-pub.mongodb.rds.aliyuncs.com
      port: 3717
      database: xp_robotaxi
      username: xp_robotaxi
      password: hDXSBe_hs0d5KxUf
  shardingsphere:
    props:
      sql.show: true
      show-sql: true
    datasource:
      names: xp0
      xp0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://pc-bp1o444934f546768po.rwlb.rds.aliyuncs.com:3306/xp_rt_operation?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: xp_rt_operation
        password: g0Tw2f_KoNlmW21M
        druid:
          initial-size: 10
          min-idle: 10
          max-active: 100
          max-wait: 60000
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          filters: stat,wall,log4j
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          remove-abandoned: true
          remove-abandoned-timeout: 1800
          log-abandoned: true
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            login-username: admin
            login-password: admin
            reset-enable: false
          wall:
            config:
              multi-statement-allow: false
    rules:
      sharding:
        tables:
          sys_oper_log:
            actual-data-nodes: xp0.sys_oper_log_${0..9}
            table-strategy:
              standard:
                sharding-column: userId
                sharding-algorithm-name: sys-oper-log-inline
            key-generate-strategy:
              column: oper_id
              key-generator-name: snowflake
          xp_agreement_app_user:
            actual-data-nodes: xp0.xp_agreement_app_user_${0..9}
            table-strategy:
              standard:
                sharding-column: app_user_id
                sharding-algorithm-name: xp-agreement-app-user-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
          xp_point:
            actual-data-nodes: xp0.xp_point_${0..9}
            table-strategy:
              standard:
                sharding-column: electronic_area_id
                sharding-algorithm-name: xp-point-inline
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
        sharding-algorithms:
          sys-oper-log-inline:
            type: INLINE
            props:
              algorithm-expression: sys_oper_log_${userId % 10}
          xp-agreement-app-user-inline:
            type: INLINE
            props:
              algorithm-expression: xp_agreement_app_user_${app_user_id % 10}
          xp-point-inline:
            type: INLINE
            props:
              algorithm-expression: xp_point_${electronic_area_id % 10}
        key-generators:
          snowflake:
            type: SNOWFLAKE
# swagger配置
swagger:
  title: 合规监管接口文档
  enabled: true
  license: Powered By xiaopeng
  licenseUrl: http://127.0.0.1:5800/doc.html
seata:
  enabled: false
xp-robotaxi-traffic-control-boot/src/main/resources/bootstrap.yml
New file
@@ -0,0 +1,160 @@
server:
  port: 6110
  tomcat:
    basedir: /data/tomcat
# Spring
spring:
  application:
    # 应用名称
    name: xp-robotaxi-traffic-control-boot
  main:
    allow-bean-definition-overriding: true
    allow-circular-references: true
  profiles:
    # 环境配置
    active: dev
#    active: prod
---
spring:
  config:
    activate:
      on-profile: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 192.168.110.80:8848
        service: ${spring.application.name}
        group: DEFAULT_GROUP
        namespace: 5185ac4b-6369-48eb-a21b-a7cbc441c5f1
        username: nacos
        password: nacos
      config:
        # 配置中心地址
        server-addr: 192.168.110.80:8848
        namespace: 5185ac4b-6369-48eb-a21b-a7cbc441c5f1
        group: DEFAULT_GROUP
        name: ${spring.application.name}
        username: nacos
        password: nacos
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
---
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: seata_tx_group    #此处配置自定义的seata事务分组名称
  enable-auto-data-source-proxy: false    #关闭数据库代理
  service:
    vgroup-mapping:
      seata_tx_group: default
  data-source-proxy-mode: AT
  config:
    type: nacos
    nacos:
      # 开发环境
      server-addr: 192.168.110.80:8848
      namespace: 5185ac4b-6369-48eb-a21b-a7cbc441c5f1
      group: DEFAULT_GROUP
      data-id: seata-server.properties
      username: nacos
      password: nacos
      # 生产环境
  #      server-addr: 192.168.0.137:8848,192.168.0.123:8848
  #      namespace: c6cf40b5-44e8-43f9-be61-8d10fa830e2a
  #      group: DEFAULT_GROUP
  #      data-id: seata-server.properties
  #      username: nacos
  #      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      # 开发环境
      server-addr: 192.168.110.80:8848
      namespace: 5185ac4b-6369-48eb-a21b-a7cbc441c5f1
      group: DEFAULT_GROUP
      username: nacos
      password: nacos
      # 生产环境
  #      server-addr: 192.168.0.137:8848,192.168.0.123:8848
  #      namespace: c6cf40b5-44e8-43f9-be61-8d10fa830e2a
  #      group: DEFAULT_GROUP
  #      username: nacos
  #      password: nacos
  cloud:
    nacos:
      discovery:
        # 开发环境
        server-addr: 192.168.110.80:8848
        namespace: 5185ac4b-6369-48eb-a21b-a7cbc441c5f1
        group: DEFAULT_GROUP
        application: seata-server #Nacos 中 Seata 名称
        username: nacos
        password: nacos
        # 生产环境
    #        server-addr: 192.168.0.137:8848,192.168.0.123:8848
    #        namespace: c6cf40b5-44e8-43f9-be61-8d10fa830e2a
    #        group: DEFAULT_GROUP
    #        application: seata-server #Nacos 中 Seata 名称
    #        username: nacos
    #        password: nacos
    sentinel:
      transport:
        dashboard: 192.168.0.137:8080  #  Sentinel控制台地址
      eager: true
---
spring:
  config:
    activate:
      on-profile: prod
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 192.168.0.137:8848,192.168.0.123:8848
        service: ${spring.application.name}
        group: DEFAULT_GROUP
        namespace: c6cf40b5-44e8-43f9-be61-8d10fa830e2a
        username: nacos
        password: nacos
      config:
        # 配置中心地址
        server-addr: 192.168.0.137:8848,192.168.0.123:8848
        namespace: c6cf40b5-44e8-43f9-be61-8d10fa830e2a
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        name: ${spring.application.name}
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
---
spring:
  config:
    activate:
      on-profile: test
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
        service: ${spring.application.name}
        group: DEFAULT_GROUP
        namespace: 96712c7a-480b-4f40-b783-39f00f3b33ce
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        namespace: 96712c7a-480b-4f40-b783-39f00f3b33ce
        group: DEFAULT_GROUP
        name: ${spring.application.name}
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java
@@ -208,6 +208,7 @@
        LambdaQueryWrapper<XpVehicle> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(XpVehicle::getElectronicId, electronicId)
                .eq(XpVehicle::getOperatingStatus, 1)
                .eq(XpVehicle::getVehicleType, 2)
                .eq(XpVehicle::getState, 0);
        // 排序:按电量降序、入库时间降序
@@ -527,6 +528,8 @@
        //绑定XPUID
        String vin = xpVehicle.getVehicleVin();
        R<String> xpuIdByVin = xpuVinClient.getXpuIdByVin(vin);
        if(200 != xpuIdByVin.getCode()){
            throw new ServiceException(xpuIdByVin.getMsg());
        if (200 != xpuIdByVin.getCode()) {
            throw new RuntimeException(xpuIdByVin.getMsg());
        }
@@ -583,8 +586,8 @@
            // 仅在 VIN 变更或原 XPUID 为空时才重新绑定 XPUID,避免不必要的远程调用
            String vin = xpVehicle.getVehicleVin();
            R<String> xpuIdByVin = xpuVinClient.getXpuIdByVin(vin);
            if (200 != xpuIdByVin.getCode()) {
                throw new RuntimeException(xpuIdByVin.getMsg());
            if(200 != xpuIdByVin.getCode()){
                throw new ServiceException(xpuIdByVin.getMsg());
            }
            String xpuid = xpuIdByVin.getData();
            xpVehicle.setXpuId(xpuid);