xuhy
2026-03-14 cf7fde24bf5a20ca8328729406f85ee16984da7a
Merge remote-tracking branch 'origin/master'
4个文件已添加
32个文件已修改
873 ■■■■■ 已修改文件
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpOperationsUserServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceOperationStrategyValidateDto.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceUpdateDto.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/maintenance/XpClockInClientFallbackFactory.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/maintenance/XpWorkOrderClientFallbackFactory.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/maintenance/XpClockInClient.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/maintenance/XpWorkOrderClient.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpBanner.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpCruisePlanVehicle.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpPoint.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/XpOperationsUserProfileVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/maintenance/TaskRuleDetailVO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpClockInController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpWorkOrderController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/TaskRuleDetailDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/mapper/XpClockInMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpClockInService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpClockInMapper.xml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpElectronicFenceManagementController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/utils/OperationStrategyIndex.java 384 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/i18n/messages.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/i18n/messages_zh_CN.properties 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpPointMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/constant/I18nKeyConstant.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpFeedbackTypeManagementController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/exportExcle/XpWhitelistLogExportExcel.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpAppVersionServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/i18n/messages.properties 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_zh_CN.properties 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/service/impl/XpOperationsUserServiceImpl.java
@@ -150,17 +150,17 @@
        // 7. 查询是否上班中
        try {
            R<Boolean> workStatusResult = xpClockInClient.checkWorkStatus(userId);
            R<Integer> workStatusResult = xpClockInClient.checkWorkStatus(userId);
            if (workStatusResult != null && workStatusResult.getCode() == 200) {
                vo.setWorkFlag(workStatusResult.getData());
            } else {
                // 调用失败,默认 false
                vo.setWorkFlag(false);
                vo.setWorkFlag(1);
                log.warn("查询用户上班状态失败, userId={}, result={}", userId, workStatusResult);
            }
        } catch (Exception e) {
            // 异常情况,默认 false
            vo.setWorkFlag(false);
            vo.setWorkFlag(1);
            log.error("查询用户上班状态异常, userId={}", userId, e);
        }
@@ -491,6 +491,6 @@
        if (StringUtils.isBlank(phoneNumber)) {
            return Collections.emptyList();
        }
        return lambdaQuery().eq(XpOperationsUser::getPhoneNumber, phoneNumber).list();
        return lambdaQuery().like(XpOperationsUser::getPhoneNumber, phoneNumber).list();
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceOperationStrategyValidateDto.java
New file
@@ -0,0 +1,20 @@
package com.xiaopeng.api.dto.operation;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
@ApiModel(value = "围栏运营策略缩短校验DTO")
public class ElectronicFenceOperationStrategyValidateDto {
    @ApiModelProperty(value = "围栏ID", required = true)
    @NotNull(message = "id不能为空")
    private Long id;
    @ApiModelProperty(value = "缩短后的运营策略JSON", required = true)
    private String operationStrategy;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceUpdateDto.java
@@ -54,4 +54,13 @@
    @ApiModelProperty(value = "电子围栏区域")
    @NotEmpty(message = "电子围栏区域不能为空")
    private List<XpElectronicFenceArea> electronicFenceAreas;
    @ApiModelProperty(value = "范围扩大 需要新增的点位ID列表")
    private List<Long> addPointIdList;
    @ApiModelProperty(value = "范围缩小 需要改为失效状态的点位ID列表")
    private List<Long> delPointIdList;
    @ApiModelProperty(value = "禁行区覆盖点位 需要改为停用状态的点位ID列表")
    private List<Long> disablePointIdList;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/maintenance/XpClockInClientFallbackFactory.java
@@ -20,11 +20,11 @@
    public XpClockInClient create(Throwable cause) {
        return new XpClockInClient() {
            @Override
            public R<Boolean> checkWorkStatus(Long appUserId) {
            public R<Integer> checkWorkStatus(Long appUserId) {
                log.error("调用打卡服务检查上班状态失败, appUserId={}, 异常: {}",
                    appUserId, cause.getMessage(), cause);
                // 降级策略: 返回 false (未上班状态)
                return R.ok(false);
                // 降级策略: 返回  (未上班状态)
                return R.ok(1);
            }
        };
    }
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/maintenance/XpWorkOrderClientFallbackFactory.java
New file
@@ -0,0 +1,26 @@
package com.xiaopeng.api.factory.maintenance;
import com.xiaopeng.api.feignClient.maintenance.XpTaskClient;
import com.xiaopeng.api.feignClient.maintenance.XpWorkOrderClient;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.common.core.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class XpWorkOrderClientFallbackFactory implements FallbackFactory<XpWorkOrderClient> {
    @Override
    public XpWorkOrderClient create(Throwable cause) {
        return new XpWorkOrderClient() {
            @Override
            public R<Boolean> include(Long id) {
                log.error("XpWorkOrderClientFallbackFactory.include error:{}", cause.getMessage());
                return null;
            }
        };
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/maintenance/XpClockInClient.java
@@ -26,5 +26,5 @@
     * @return true=上班中, false=未上班或已下班
     */
    @GetMapping("/inner/clock-in/checkWorkStatus")
    R<Boolean> checkWorkStatus(@RequestParam("appUserId") Long appUserId);
    R<Integer> checkWorkStatus(@RequestParam("appUserId") Long appUserId);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/maintenance/XpWorkOrderClient.java
New file
@@ -0,0 +1,26 @@
package com.xiaopeng.api.feignClient.maintenance;
import com.xiaopeng.api.factory.maintenance.XpClockInClientFallbackFactory;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 打卡服务 Feign 客户端
 *
 * @author mitao
 * @date 2026-02-14
 */
@FeignClient(
    contextId = "XpClockInClient",
    value = ServiceNameConstantsEnum.XP_MAINTENANCE_SERVICE,
    fallbackFactory = XpClockInClientFallbackFactory.class
)
public interface XpWorkOrderClient {
    @GetMapping("/api/v1/workOrder/include")
    R<Boolean> include(@RequestParam Long id);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java
@@ -1,8 +1,6 @@
package com.xiaopeng.api.model.maintenance;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import com.xiaopeng.common.core.web.domain.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -57,6 +55,7 @@
    private String taskImage;
    @ApiModelProperty(value = "车辆id")
    @TableField(updateStrategy = FieldStrategy.NEVER)
    private Long vehicleId;
    @ApiModelProperty(value = "车辆位置")
    private String vehicleLocation;
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpBanner.java
@@ -69,8 +69,13 @@
    @ApiModelProperty(value = "广告图片")
    @TableField("banner_pictures")
    private String bannerPictures;
    @ApiModelProperty(value = "广告标题")
    @TableField("banner_title")
    private String bannerTitle;
    @ApiModelProperty(value = "排序")
    @TableField("sort")
    private Integer sort;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpCruisePlanVehicle.java
@@ -30,6 +30,10 @@
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "自增id")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "巡游计划ID")
    private Long cruisePlanId;
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpPoint.java
@@ -34,10 +34,9 @@
    private Long id;
    @ApiModelProperty(value = "电子围栏区域id")
    @TableField(value = "electronic_area_id", updateStrategy = FieldStrategy.NEVER)
    private Long electronicAreaId;
    @TableField("fp_electronic_area_id")
    @TableField(value = "fp_electronic_area_id", updateStrategy = FieldStrategy.NEVER)
    private Long fpElectronicAreaId;
    @ApiModelProperty(value = "点位名称")
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/account/XpOperationsUserProfileVO.java
@@ -83,8 +83,8 @@
    @ApiModelProperty(value = "角色列表")
    private List<RoleInfoVO> roles;
    @ApiModelProperty("是否上班中")
    private Boolean workFlag;
    @ApiModelProperty("打卡状态 1-未打卡 2-上班中 3-休息")
    private Integer workFlag;
    @ApiModelProperty(value = "登录设备类型 1-Android 2-iOS")
    private Integer deviceType;
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/maintenance/TaskRuleDetailVO.java
@@ -22,7 +22,7 @@
    @ApiModelProperty(value = "车辆日常监测 频次")
    private Integer vehicleInspectionFrequency;
    @ApiModelProperty(value = "车辆日常检测 任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleInspectionTime;
    @ApiModelProperty(value = "车辆日常监测 紧急程度 0=P0 1=P1 2=P2 3=P3")
@@ -32,7 +32,7 @@
    private Boolean vehicleCollection;
    @ApiModelProperty(value = "车辆收车每日时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleCollectionTime;
    @ApiModelProperty(value = "车辆保养周期是否启用")
@@ -45,7 +45,7 @@
    private Integer maintenanceCycleTimeType;
    @ApiModelProperty(value = "车辆保养周期 任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime maintenanceCycleTaskTime;
    @ApiModelProperty(value = "车辆保养周期 紧急程度 0=P0 1=P1 2=P2 3=P3")
@@ -64,7 +64,7 @@
    private Integer vehicleCleanCycle;
    @ApiModelProperty(value = "车身清洁保养任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleCleanTime;
    @ApiModelProperty(value = "车身清洁保养周期X 1=天 2=月")
@@ -77,7 +77,7 @@
    private Integer routineMaintenanceCycle;
    @ApiModelProperty(value = "日常整备保养任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime routineMaintenanceTime;
    @ApiModelProperty(value = "日常整备保养周期X 1=天 2=月")
xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -41,4 +41,5 @@
com.xiaopeng.api.factory.maintenance.XpTaskHandleDetailRoutineClientFallbackFactory
com.xiaopeng.api.factory.maintenance.XpTaskClientFallbackFactory
com.xiaopeng.api.factory.maintenance.XpTaskRuleClientFallbackFactory
com.xiaopeng.api.factory.maintenance.XpWorkOrderClientFallbackFactory
com.xiaopeng.api.factory.pushMessage.XpAliPushMessageClientFallbackFactory
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpClockInController.java
@@ -58,9 +58,9 @@
     */
    @GetMapping("/inner/clock-in/checkWorkStatus")
    @ApiOperation(value = "检查用户是否上班中", tags = {"内部接口"})
    public R<Boolean> checkWorkStatus(@RequestParam("appUserId") Long appUserId) {
    public R<Integer> checkWorkStatus(@RequestParam("appUserId") Long appUserId) {
        log.info("检查用户上班状态, appUserId={}", appUserId);
        Boolean workFlag = xpClockInService.checkWorkStatus(appUserId);
        Integer workFlag = xpClockInService.checkWorkStatus(appUserId);
        return R.ok(workFlag);
    }
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpWorkOrderController.java
@@ -1,6 +1,7 @@
package com.xiaopeng.maintenance.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.model.maintenance.XpWorkOrder;
import com.xiaopeng.api.model.maintenance.XpWorkOrderLog;
import com.xiaopeng.common.core.domain.R;
@@ -74,4 +75,19 @@
        XpWorkOrder detail =workOrderService.workOrderDetail(id);
        return R.ok(detail);
    }
    /**
     * 判断工单是否包含这个类型的id
     * @param id
     * @return
     */
    @GetMapping("/include")
    public R<Boolean> include(@RequestParam Long id){
        long count = workOrderService.count(new LambdaQueryWrapper<XpWorkOrder>().eq(XpWorkOrder::getFeedbackTypeId, id));
        if(count>0){
            return R.ok(true);
        }
        return R.ok(false);
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/TaskRuleDetailDto.java
@@ -23,7 +23,7 @@
    @ApiModelProperty(value = "车辆日常监测 频次")
    private Integer vehicleInspectionFrequency;
    @ApiModelProperty(value = "车辆日常检测 任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleInspectionTime;
    @ApiModelProperty(value = "车辆日常监测 紧急程度 0=P0 1=P1 2=P2 3=P3")
@@ -33,7 +33,7 @@
    private Boolean vehicleCollection;
    @ApiModelProperty(value = "车辆收车每日时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleCollectionTime;
    @ApiModelProperty(value = "车辆保养周期是否启用")
@@ -46,7 +46,7 @@
    private Integer maintenanceCycleTimeType;
    @ApiModelProperty(value = "车辆保养周期 任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime maintenanceCycleTaskTime;
    @ApiModelProperty(value = "车辆保养周期 紧急程度 0=P0 1=P1 2=P2 3=P3")
@@ -65,7 +65,7 @@
    private Integer vehicleCleanCycle;
    @ApiModelProperty(value = "车身清洁保养任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime vehicleCleanTime;
    @ApiModelProperty(value = "车身清洁保养周期X 1=天 2=月")
@@ -78,7 +78,7 @@
    private Integer routineMaintenanceCycle;
    @ApiModelProperty(value = "日常整备保养任务下放时间")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    @JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
    private LocalTime routineMaintenanceTime;
    @ApiModelProperty(value = "日常整备保养周期X 1=天 2=月")
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/mapper/XpClockInMapper.java
@@ -23,20 +23,6 @@
public interface XpClockInMapper extends BaseMapper<XpClockIn> {
    /**
     * 查询用户当天的打卡记录
     * @param appUserId 用户ID
     * @param startTime 开始时间 (当天00:00:00)
     * @param endTime 结束时间 (当天23:59:59)
     * @return 打卡记录列表
     */
    List<XpClockIn> selectTodayClockInRecords(
        @Param("appUserId") Long appUserId,
        @Param("startTime") LocalDateTime startTime,
        @Param("endTime") LocalDateTime endTime
    );
    /**
     * 获取打卡统计
     * @param pageInfo
     * @return
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpClockInService.java
@@ -49,7 +49,7 @@
     * @param appUserId 用户ID
     * @return true=上班中(只打了上班卡), false=未上班或已下班
     */
    Boolean checkWorkStatus(Long appUserId);
    Integer checkWorkStatus(Long appUserId);
    /**
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
@@ -1,6 +1,7 @@
package com.xiaopeng.maintenance.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
@@ -198,10 +199,10 @@
    }
    @Override
    public Boolean checkWorkStatus(Long appUserId) {
    public Integer checkWorkStatus(Long appUserId) {
        if (appUserId == null) {
            log.warn("检查上班状态失败: appUserId 为空");
            return false;
            return 1;
        }
        try {
@@ -210,29 +211,23 @@
            LocalDateTime endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);   // 23:59:59
            // 2. 查询当天打卡记录
            List<XpClockIn> records = baseMapper.selectTodayClockInRecords(
                    appUserId, startTime, endTime
            );
            XpClockIn xpClockIn = baseMapper.selectOne(new LambdaQueryWrapper<XpClockIn>()
                    .eq(XpClockIn::getAppUserId, appUserId)
                    .between(XpClockIn::getClockInTime, startTime, endTime)
                    .last("limit 1"));
            // 3. 判断上班状态
            if (records == null || records.isEmpty()) {
                // 无打卡记录 = 未上班
                log.debug("用户 {} 今日无打卡记录, workFlag=false", appUserId);
                return false;
            } else if (records.size() == 1) {
                // 1条记录 = 已上班未下班
                log.debug("用户 {} 今日打卡1次, workFlag=true", appUserId);
                return true;
            // 有上班打卡状态,无下班打卡状态
            if (Objects.isNull(xpClockIn)) {
                return 1;
            } else if (Objects.nonNull(xpClockIn.getClockInResult()) && Objects.isNull(xpClockIn.getClockOutResult())) {
                return 2;
            } else {
                // 2条及以上记录 = 已下班
                log.debug("用户 {} 今日打卡{}次, workFlag=false", appUserId, records.size());
                return false;
                return 3;
            }
        } catch (Exception e) {
            log.error("检查用户上班状态异常, appUserId={}", appUserId, e);
            // 异常情况返回 false
            return false;
            return 1;
        }
    }
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -405,7 +405,8 @@
        }
        return R.ok(res);
    }
    @ShardingSphereTransactionType(TransactionType.BASE)
    @Transactional(rollbackFor = Exception.class)
    @Override
    public R handleVehicleAccident(HandleVehicleAccidentDto dto, XpOperationsUser xpOperationsUser) {
        XpTask task = this.getById(dto.getId());
@@ -419,6 +420,7 @@
        task.setHandleTime(LocalDateTime.now());
        task.setHandleUserId(xpOperationsUser.getId().toString());
        task.setStatus(2);
        updateById(task);
        // 存储事故处理日志
        XpTaskHandleDetail xpTaskHandleDetail = new XpTaskHandleDetail();
        xpTaskHandleDetail.setTaskId(task.getId());
@@ -453,6 +455,9 @@
                    I18nKeyConstant.VEHICLE_NOT_EXIST
            );
        }
        //查询任务处理记录
        XpTaskHandleDetailRoutine xpTaskHandleDetailRoutine = xpTaskHandleDetailRoutineMapper.selectOne(new LambdaQueryWrapper<XpTaskHandleDetailRoutine>()
                .eq(XpTaskHandleDetailRoutine::getTaskId, id).last("limit 1"));
        res.setId(id);
        res.setTaskUrgencyLevel(task.getTaskUrgencyLevel());
        res.setEndTime(task.getEndTime());
@@ -474,6 +479,12 @@
        res.setTaskImage(task.getTaskImage());
        res.setHandleUserId(task.getHandleUserId());
        res.setHandleTime(task.getHandleTime());
        if (Objects.nonNull(xpTaskHandleDetailRoutine)) {
            res.setTaskDescription(xpTaskHandleDetailRoutine.getTaskDescription());
            task.setHandleUserId(xpTaskHandleDetailRoutine.getOperationsUserId().toString());
            res.setHandleTime(xpTaskHandleDetailRoutine.getCreateTime());
            res.setTaskImage(xpTaskHandleDetailRoutine.getTaskImage());
        }
        if (Objects.nonNull(task.getHandleUserId()) && task.getHandleUserId().split(",").length == 1) {
            XpOperationsUser data = xpOperationUserClient.queryById(Long.valueOf(task.getHandleUserId())).getData();
            if (Objects.nonNull(data)) {
xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpClockInMapper.xml
@@ -18,29 +18,6 @@
        <result column="update_time" property="updateTime"/>
    </resultMap>
    <!-- 查询用户当天的打卡记录 -->
    <select id="selectTodayClockInRecords" resultMap="BaseResultMap">
        SELECT id,
               app_user_id,
               clock_in_time,
               clock_out_time,
               shift_id,
               clock_in_result,
               clock_out_result,
               clock_in_lon,
               clock_in_lat,
               clock_out_lon,
               clock_out_lat,
               create_time,
               update_time
        FROM xp_clock_in
        WHERE app_user_id = #{appUserId}
          AND clock_in_time BETWEEN #{startTime} AND #{endTime}
          AND clock_in_result IN (1, 2)
        ORDER BY clock_in_time ASC
    </select>
    <select id="getClockInStatistics" resultType="com.xiaopeng.maintenance.dto.ClockInStatisticsDto">
        select
        aa.app_user_id as appUserId,
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpElectronicFenceManagementController.java
@@ -5,8 +5,8 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceAreaUpdateStatusDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceOperationStrategyValidateDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto;
import com.xiaopeng.api.feignClient.operation.XpElectronicClient;
import com.xiaopeng.api.model.operation.XpElectronicFence;
import com.xiaopeng.api.model.operation.XpElectronicFenceArea;
import com.xiaopeng.api.model.operation.XpPoint;
@@ -17,27 +17,29 @@
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.utils.bean.BeanUtils;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.common.security.service.TokenService;
import com.xiaopeng.map.constant.I18nKeyConstant;
import com.xiaopeng.map.service.XpElectronicFenceAreaService;
import com.xiaopeng.map.service.XpElectronicFenceService;
import com.xiaopeng.map.service.XpPointService;
import com.xiaopeng.map.service.XpRegionService;
import com.xiaopeng.map.utils.OperationStrategyIndex;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.locationtech.jts.geom.Coordinate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.xiaopeng.map.service.serviceimpl.XpElectronicFenceServiceImpl.getPinyinInitials;
@@ -66,6 +68,9 @@
    private XpRegionService regionService;
    @Autowired
    private XpRegionService xpRegionService;
    @Autowired
    private XpPointService xpPointService;
    /**
     * 查询省市管理
     */
@@ -146,11 +151,76 @@
     */
    @PostMapping("/edit")
    @ApiOperation(value = "编辑电子围栏")
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    public R edit(@Validated @RequestBody ElectronicFenceUpdateDto dto) {
        return xpElectronicFenceService.editElectronic(dto);
    }
    /**
     * 校验编辑围栏时运营时间段缩短影响的点位数量
     * <p>
     * 统计:围栏下 operation_type=2(自定义运营时间段) 的点位中,
     * 当围栏运营时间范围发生缩短时,不在新运营时间范围(Union)内的点位数量(需要停用)。
     * </p>
     */
    @PostMapping("/validateOperationStrategyShorten")
    @ApiOperation(value = "校验围栏运营时间段缩短影响点位数量")
    public R<Integer> validateOperationStrategyShorten(@Valid @RequestBody ElectronicFenceOperationStrategyValidateDto dto) {
        XpElectronicFence fence = xpElectronicFenceService.getById(dto.getId());
        if (fence == null) {
            throw new ServiceException("围栏不存在", I18nKeyConstant.FENCE_NOT_EXIST);
        }
        OperationStrategyIndex oldIndex = OperationStrategyIndex.parse(fence.getOperationStrategy());
        OperationStrategyIndex newIndex = OperationStrategyIndex.parse(dto.getOperationStrategy());
        if (oldIndex.equalsNormalized(newIndex)) {
            return R.ok(0);
        }
        if (!oldIndex.hasAnyShrinkComparedTo(newIndex)) {
            return R.ok(0);
        }
        List<Long> fenceAreaIds = xpElectronicFenceAreaService.lambdaQuery()
                .eq(XpElectronicFenceArea::getElectronicId, dto.getId())
                .list()
                .stream()
                .map(XpElectronicFenceArea::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(fenceAreaIds)) {
            return R.ok(0);
        }
        List<XpPoint> customPoints = xpPointService.lambdaQuery()
                .in(XpPoint::getElectronicAreaId, fenceAreaIds)
                .eq(XpPoint::getPointType, 1)
                .eq(XpPoint::getOperationType, 2)
                .list();
        if (CollUtil.isEmpty(customPoints)) {
            return R.ok(0);
        }
        int disableCount = 0;
        for (XpPoint point : customPoints) {
            if (point == null || point.getEffectiveTimePeriod() == null || point.getEffectiveTimePeriod().isEmpty()) {
                continue;
            }
            List<OperationStrategyIndex.TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(point.getEffectiveTimePeriod());
            if (pointIntervals.isEmpty()) {
                continue;
            }
            boolean shouldDisable = !newIndex.isPointScheduleWithin(pointIntervals);
            if (shouldDisable) {
                disableCount++;
            }
        }
        return R.ok(disableCount);
    }
    /**
     * 复制电子围栏
     */
    @PostMapping("/copy")
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java
@@ -186,7 +186,7 @@
            throw new ServiceException("系统异常", I18nKeyConstant.SYSTEM_EXCEPTION);
        }
        if (Boolean.TRUE.equals(existsResult.getData())) {
            throw new ServiceException("操作失败,该点位有关联的巡游点位", I18nKeyConstant.POINT_DELETE_HAS_CRUISE_PLAN_POINT);
            throw new ServiceException("操作失败,该点位有关联的巡游计划点位", I18nKeyConstant.POINT_DELETE_HAS_CRUISE_PLAN_POINT);
        }
        xpPointService.removeBatchByIds(pointIds);
        return R.ok();
@@ -201,25 +201,12 @@
    @ShardingSphereTransactionType(TransactionType.BASE)
    public R edit(@Validated @RequestBody PointUpdateDto pointAddDto) {
        XpPoint xpPoint = xpPointService.getById(pointAddDto.getId());
        if (!xpPoint.getElectronicAreaId().equals(pointAddDto.getElectronicAreaId())) {
            BeanUtils.copyProperties(pointAddDto, xpPoint);
            if (xpGeoFencingPolygonService.isInFence(pointAddDto.getPointLon(), pointAddDto.getPointLat(), pointAddDto.getFenceId(), "prohibited_area")) {
                return R.ok("in_prohibited_area");
            }
            xpPointService.updateById(xpPoint);
        } else {
            if (xpGeoFencingPolygonService.isInFence(pointAddDto.getPointLon(), pointAddDto.getPointLat(), pointAddDto.getFenceId(), "prohibited_area")) {
                return R.ok("in_prohibited_area");
            }
            //先删除原有的数据,因为会存在点位的从属区域变化的情况,所以先删除原有的数据后添加新的数据重新分片到新的库中
            BeanUtils.copyProperties(pointAddDto, xpPoint);
            xpPointService.removeById(pointAddDto.getId());
            xpPoint.setId(null);
            xpPointService.save(xpPoint);
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        if (xpGeoFencingPolygonService.isInFence(pointAddDto.getPointLon(), pointAddDto.getPointLat(), pointAddDto.getFenceId(), "prohibited_area")) {
            return R.ok("in_prohibited_area");
        }
        xpPointService.updateById(xpPoint);
        return R.ok();
    }
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java
@@ -1,6 +1,7 @@
package com.xiaopeng.map.service.serviceimpl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -27,6 +28,8 @@
import com.xiaopeng.map.mapper.XpRegionMapper;
import com.xiaopeng.map.service.XpElectronicFenceAreaService;
import com.xiaopeng.map.service.XpElectronicFenceService;
import com.xiaopeng.map.service.XpPointService;
import com.xiaopeng.map.utils.OperationStrategyIndex;
import com.xiaopeng.map.utils.GDMapGeocodingUtil;
import com.xiaopeng.map.utils.PolygonOverlapService;
import lombok.extern.slf4j.Slf4j;
@@ -72,6 +75,10 @@
    private MongoTemplate mongoTemplate;
    @Autowired
    private XpPointMapper xpPointMapper;
    @Autowired
    private XpPointService xpPointService;
    @Autowired
    private XpRegionMapper xpRegionMapper;
    @Autowired
@@ -427,6 +434,8 @@
        XpElectronicFence electronicFence = this.getById(dto.getId());
        Integer status = electronicFence.getStatus();
        String oldOperationStrategy = electronicFence.getOperationStrategy();
        String newOperationStrategy = dto.getOperationStrategy();
        // 更新主表数据
@@ -651,7 +660,6 @@
                                UpdateWrapper<XpPoint> xpPointUpdateWrapper = new UpdateWrapper<>();
                                xpPointUpdateWrapper.eq("id", xpPoint.getId());
                                xpPointUpdateWrapper.set("point_type", 2);
                                xpPointUpdateWrapper.set("electronic_areaId", null);
                                xpPointMapper.update(xpPoint, xpPointUpdateWrapper);
                            }
                        }
@@ -666,9 +674,109 @@
                xpPointMapper.updateById(xpPoint);
            }
        }
        OperationStrategyIndex oldIndex = OperationStrategyIndex.parse(oldOperationStrategy);
        OperationStrategyIndex newIndex = OperationStrategyIndex.parse(newOperationStrategy);
        boolean needAutoDisableByUnion = !oldIndex.equalsNormalized(newIndex) && oldIndex.hasAnyShrinkComparedTo(newIndex);
        List<Long> addPointIds = normalizePointIds(dto.getAddPointIdList());
        List<Long> delPointIds = normalizePointIds(dto.getDelPointIdList());
        List<Long> disablePointIds = normalizePointIds(dto.getDisablePointIdList());
        boolean needPointHandling = CollUtil.isNotEmpty(addPointIds)
                || CollUtil.isNotEmpty(delPointIds)
                || CollUtil.isNotEmpty(disablePointIds);
        boolean needFenceAreaIds = needPointHandling || needAutoDisableByUnion;
        XpElectronicFenceArea operationArea = needFenceAreaIds
                ? xpElectronicFenceAreaService.lambdaQuery()
                .eq(XpElectronicFenceArea::getElectronicId, dto.getId())
                .eq(XpElectronicFenceArea::getAreaType, 1)
                .orderByDesc(XpElectronicFenceArea::getId)
                .last("limit 1")
                .one()
                : null;
        Long operationAreaId = operationArea == null ? null : operationArea.getId();
        if (needPointHandling && operationAreaId == null) {
            throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST);
        }
        if (CollUtil.isNotEmpty(addPointIds)) {
            if (operationAreaId == null) {
                throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST);
            }
            xpPointService.lambdaUpdate()
                    .in(XpPoint::getId, addPointIds)
                    .set(XpPoint::getElectronicAreaId, operationAreaId)
                    .set(XpPoint::getVerificationStatus, 1)
                    .update();
        }
        if (CollUtil.isNotEmpty(disablePointIds)) {
            // operation_status: 3=已停用
            xpPointService.lambdaUpdate()
                    .in(XpPoint::getId, disablePointIds)
                    .eq(XpPoint::getElectronicAreaId, operationAreaId)
                    .set(XpPoint::getOperationStatus, 3)
                    .update();
        }
        if (CollUtil.isNotEmpty(delPointIds)) {
            // operation_status: 4=已失效
            xpPointService.lambdaUpdate()
                    .in(XpPoint::getId, delPointIds)
                    .eq(XpPoint::getElectronicAreaId, operationAreaId)
                    .set(XpPoint::getOperationStatus, 4)
                    .update();
        }
        // 围栏运营时间缩短校验:若缩短后点位时间段不在新范围内,则更新为已停用(operation_status=3)
        if (needAutoDisableByUnion && operationAreaId != null) {
            LambdaQueryWrapper<XpPoint> pointQuery = new LambdaQueryWrapper<>();
            pointQuery.eq(XpPoint::getElectronicAreaId, operationAreaId)
                    .eq(XpPoint::getPointType, 1)
                    .eq(XpPoint::getOperationType, 2);
            List<XpPoint> points = xpPointMapper.selectList(pointQuery);
            if (points != null && !points.isEmpty()) {
                List<Long> disablePointIdList = new ArrayList<>();
                for (XpPoint pointItem : points) {
                    if (pointItem == null) {
                        continue;
                    }
                    if (pointItem.getOperationStatus() != null && pointItem.getOperationStatus() == 4) {
                        continue;
                    }
                    String effectiveTimePeriod = pointItem.getEffectiveTimePeriod();
                    if (effectiveTimePeriod == null || effectiveTimePeriod.isEmpty()) {
                        continue;
                    }
                    List<OperationStrategyIndex.TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(effectiveTimePeriod);
                    if (pointIntervals.isEmpty()) {
                        continue;
                    }
                    boolean shouldDisable = !newIndex.isPointScheduleWithin(pointIntervals);
                    if (shouldDisable && pointItem.getId() != null) {
                        disablePointIdList.add(pointItem.getId());
                    }
                }
                if (!disablePointIdList.isEmpty()) {
                    XpPoint updatePoint = new XpPoint();
                    updatePoint.setOperationStatus(3);
                    UpdateWrapper<XpPoint> updateWrapper = new UpdateWrapper<>();
                    updateWrapper.in("id", disablePointIdList);
                    updateWrapper.and(w -> w.isNull("operation_status").or().ne("operation_status", 4));
                    xpPointMapper.update(updatePoint, updateWrapper);
                    log.info("围栏运营时间缩短停用点位完成 fenceId={}, disableCount={}", dto.getId(), disablePointIdList.size());
                }
            }
        }
        return R.ok();
    }
    private static List<Long> normalizePointIds(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
    }
    /**
     * 判断点是否在多边形内
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -133,6 +133,7 @@
        XpPoint xpPoint = new XpPoint();
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId());
        xpPoint.setPointNumber(pointNumber);
        this.save(xpPoint);
        return xpPoint;
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/utils/OperationStrategyIndex.java
New file
@@ -0,0 +1,384 @@
package com.xiaopeng.map.utils;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * 围栏运营策略时间范围索引工具类
 */
public final class OperationStrategyIndex {
    private final List<List<TimeInterval>> strategyIntervals;
    /**
     * 构造索引(内部使用)。
     *
     * @param strategyIntervals 策略组时间区间列表(每个策略组已 merge)
     */
    private OperationStrategyIndex(List<List<TimeInterval>> strategyIntervals) {
        this.strategyIntervals = strategyIntervals == null ? Collections.emptyList() : strategyIntervals;
    }
    /**
     * 解析围栏 {@code operation_strategy} JSON 字符串为运营时间范围索引。
     * <p>
     * 说明:会按“策略组”聚合并合并时间段;点位只要被任意一个策略组的时间段完整覆盖,则为正常。
     * </p>
     *
     * @param operationStrategyJson 围栏运营策略 JSON(数组字符串)
     * @return 索引对象;解析失败/空入参返回空索引
     */
    public static OperationStrategyIndex parse(String operationStrategyJson) {
        if (operationStrategyJson == null || operationStrategyJson.isEmpty()) {
            return new OperationStrategyIndex(Collections.emptyList());
        }
        try {
            JSONArray jsonArray = JSONArray.parseArray(operationStrategyJson);
            if (jsonArray == null || jsonArray.isEmpty()) {
                return new OperationStrategyIndex(Collections.emptyList());
            }
            List<List<TimeInterval>> groups = new java.util.ArrayList<>();
            for (int i = 0; i < jsonArray.size(); i++) {
                JSONObject obj = jsonArray.getJSONObject(i);
                if (obj == null) {
                    continue;
                }
                JSONArray timeArray = obj.getJSONArray("time");
                if (timeArray == null || timeArray.isEmpty()) {
                    continue;
                }
                List<TimeInterval> intervals = TimeInterval.parseRangesDedup(timeArray);
                List<TimeInterval> merged = TimeInterval.merge(intervals);
                if (!merged.isEmpty()) {
                    groups.add(merged);
                }
            }
            return new OperationStrategyIndex(groups);
        } catch (Exception e) {
            return new OperationStrategyIndex(Collections.emptyList());
        }
    }
    /**
     * 解析点位 {@code effective_time_period} JSON 数组字符串为时间区间列表(并合并)。
     *
     * @param jsonArrayStr 点位生效时间段 JSON(数组字符串)
     * @return 时间区间列表;解析失败返回空列表
     */
    public static List<TimeInterval> parsePointEffectiveTimePeriod(String jsonArrayStr) {
        return TimeInterval.parseJsonRangesDedup(jsonArrayStr);
    }
    /**
     * 判断两个围栏策略索引的 Union 运营时间覆盖范围是否一致。
     * <p>
     * 注意:不关心 JSON 的原始顺序与分组,只比较合并后的 Union 覆盖范围是否相同。
     * </p>
     *
     * @param other 待比较索引
     * @return true 表示 Union 覆盖范围一致
     */
    public boolean equalsNormalized(OperationStrategyIndex other) {
        if (other == null) {
            return false;
        }
        return equalsIntervals(this.unionAllowed(), other.unionAllowed());
    }
    /**
     * 判断相对新索引是否发生“运营时间范围缩短”。
     * <p>
     * 缩短定义:新 Union 覆盖范围被旧 Union 完整覆盖,且与旧 Union 不相等(newUnion ⊂ oldUnion)。
     * </p>
     *
     * @param newIndex 新索引(编辑后的围栏策略)
     * @return true 表示发生缩短
     */
    public boolean hasAnyShrinkComparedTo(OperationStrategyIndex newIndex) {
        if (newIndex == null) {
            return false;
        }
        List<TimeInterval> oldUnion = this.unionAllowed();
        List<TimeInterval> newUnion = newIndex.unionAllowed();
        if (equalsIntervals(oldUnion, newUnion)) {
            return false;
        }
        return TimeInterval.isWithin(newUnion, oldUnion);
    }
    /**
     * 获取当前索引的 Union 运营时间覆盖范围(合并后的时间区间列表)。
     *
     * @return Union 区间列表(已 merge 并排序)
     */
    public List<TimeInterval> unionAllowed() {
        if (strategyIntervals == null || strategyIntervals.isEmpty()) {
            return Collections.emptyList();
        }
        List<TimeInterval> union = strategyIntervals.stream()
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .collect(Collectors.toList());
        return TimeInterval.merge(union);
    }
    /**
     * 判断点位的时间段是否为正常。
     * <p>
     * 规则:只要点位的全部运营时间段被围栏“任意一个策略组”的时间段范围完整覆盖,则为正常。
     * </p>
     *
     * @param pointIntervals 点位时间区间列表
     * @return true 表示正常;false 表示不在任何一个策略组覆盖范围内
     */
    public boolean isPointScheduleWithin(List<TimeInterval> pointIntervals) {
        if (pointIntervals == null || pointIntervals.isEmpty()) {
            return true;
        }
        if (strategyIntervals == null || strategyIntervals.isEmpty()) {
            return false;
        }
        for (List<TimeInterval> groupIntervals : strategyIntervals) {
            if (TimeInterval.isWithin(pointIntervals, groupIntervals)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 兼容旧接口:忽略 {@code effectivePeriod},统一按每日(Union 覆盖范围)判断。
     *
     * @param effectivePeriod 点位生效周期(不再使用)
     * @param pointIntervals  点位时间区间列表
     * @return true 表示点位时间段被围栏 Union 覆盖
     */
    public boolean isPointScheduleWithin(Integer effectivePeriod, List<TimeInterval> pointIntervals) {
        return isPointScheduleWithin(pointIntervals);
    }
    /**
     * 区间列表归一化:merge 合并并排序。
     */
    private static List<TimeInterval> normalizeIntervals(List<TimeInterval> intervals) {
        return TimeInterval.merge(intervals);
    }
    /**
     * 比较两组区间在归一化(merge)后的覆盖范围是否完全一致。
     */
    private static boolean equalsIntervals(List<TimeInterval> a, List<TimeInterval> b) {
        List<TimeInterval> x = normalizeIntervals(a);
        List<TimeInterval> y = normalizeIntervals(b);
        if (x.size() != y.size()) {
            return false;
        }
        for (int i = 0; i < x.size(); i++) {
            TimeInterval xi = x.get(i);
            TimeInterval yi = y.get(i);
            if (xi.startMinute != yi.startMinute || xi.endMinute != yi.endMinute) {
                return false;
            }
        }
        return true;
    }
    public static final class TimeInterval {
        private final int startMinute; // inclusive
        private final int endMinute;   // inclusive
        /**
         * 构造时间区间(闭区间,单位:分钟)。
         */
        private TimeInterval(int startMinute, int endMinute) {
            this.startMinute = startMinute;
            this.endMinute = endMinute;
        }
        /**
         * 解析 JSON 数组字符串为时间区间列表,并做 merge 合并。
         *
         * @param jsonArrayStr JSON 数组字符串(例如 ["08:00-12:00","13:00-18:00"])
         * @return merge 后的区间列表;解析失败返回空列表
         */
        static List<TimeInterval> parseJsonRangesDedup(String jsonArrayStr) {
            if (jsonArrayStr == null || jsonArrayStr.isEmpty()) {
                return Collections.emptyList();
            }
            try {
                JSONArray arr = JSONArray.parseArray(jsonArrayStr);
                if (arr == null || arr.isEmpty()) {
                    return Collections.emptyList();
                }
                return merge(parseRangesDedup(arr));
            } catch (Exception e) {
                return Collections.emptyList();
            }
        }
        /**
         * 解析 JSONArray 中的时间段为区间列表(不做 merge)。
         *
         * @param timeArray 时间段数组(元素形如 "HH:mm-HH:mm")
         * @return 区间列表
         */
        static List<TimeInterval> parseRanges(JSONArray timeArray) {
            List<TimeInterval> result = new java.util.ArrayList<>();
            for (int i = 0; i < timeArray.size(); i++) {
                String range = timeArray.getString(i);
                result.addAll(parseRange(range));
            }
            return result;
        }
        /**
         * 解析 JSONArray 中的时间段为区间列表(先按字符串去重,再解析;不做 merge)。
         */
        static List<TimeInterval> parseRangesDedup(JSONArray timeArray) {
            if (timeArray == null || timeArray.isEmpty()) {
                return Collections.emptyList();
            }
            Set<String> uniq = new LinkedHashSet<>();
            for (int i = 0; i < timeArray.size(); i++) {
                String range = timeArray.getString(i);
                if (range != null && !range.isEmpty()) {
                    uniq.add(range);
                }
            }
            List<TimeInterval> result = new java.util.ArrayList<>();
            for (String range : uniq) {
                result.addAll(parseRange(range));
            }
            return result;
        }
        /**
         * 解析单个时间段字符串为区间列表。
         * <p>
         * 注意:业务约束不会出现跨天时间段;若出现 end &lt; start,则视为非法配置并忽略该时间段。
         * </p>
         */
        private static List<TimeInterval> parseRange(String range) {
            if (range == null || !range.contains("-")) {
                return Collections.emptyList();
            }
            String[] parts = range.split("-");
            if (parts.length != 2) {
                return Collections.emptyList();
            }
            Integer start = parseMinute(parts[0].trim());
            Integer end = parseMinute(parts[1].trim());
            if (start == null || end == null) {
                return Collections.emptyList();
            }
            if (end < start) {
                return Collections.emptyList();
            }
            return java.util.Collections.singletonList(new TimeInterval(start, end));
        }
        /**
         * 将 HH:mm 解析为分钟数(0-1439),非法格式返回 null。
         */
        private static Integer parseMinute(String hhmm) {
            if (hhmm == null || hhmm.length() != 5 || hhmm.charAt(2) != ':') {
                return null;
            }
            try {
                int hour = Integer.parseInt(hhmm.substring(0, 2));
                int minute = Integer.parseInt(hhmm.substring(3, 5));
                if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
                    return null;
                }
                return hour * 60 + minute;
            } catch (Exception e) {
                return null;
            }
        }
        /**
         * 合并重叠/相邻区间,并按起始分钟排序。
         */
        static List<TimeInterval> merge(List<TimeInterval> intervals) {
            if (intervals == null || intervals.isEmpty()) {
                return Collections.emptyList();
            }
            List<TimeInterval> sorted = intervals.stream()
                    .filter(Objects::nonNull)
                    .sorted(java.util.Comparator.comparingInt(a -> a.startMinute))
                    .collect(Collectors.toList());
            if (sorted.isEmpty()) {
                return Collections.emptyList();
            }
            List<TimeInterval> merged = new java.util.ArrayList<>();
            TimeInterval cur = sorted.get(0);
            for (int i = 1; i < sorted.size(); i++) {
                TimeInterval next = sorted.get(i);
                if (next.startMinute <= cur.endMinute + 1) {
                    cur = new TimeInterval(cur.startMinute, Math.max(cur.endMinute, next.endMinute));
                } else {
                    merged.add(cur);
                    cur = next;
                }
            }
            merged.add(cur);
            return merged;
        }
        /**
         * 判断 pointIntervals 是否完全被 allowedIntervals 覆盖(每段都需被某个 allowed 段完全包含)。
         */
        static boolean isWithin(List<TimeInterval> pointIntervals, List<TimeInterval> allowedIntervals) {
            if (pointIntervals == null || pointIntervals.isEmpty()) {
                return true;
            }
            List<TimeInterval> allowed = merge(allowedIntervals);
            if (allowed.isEmpty()) {
                return false;
            }
            for (TimeInterval p : pointIntervals) {
                boolean covered = false;
                for (TimeInterval a : allowed) {
                    if (a.startMinute <= p.startMinute && a.endMinute >= p.endMinute) {
                        covered = true;
                        break;
                    }
                }
                if (!covered) {
                    return false;
                }
            }
            return true;
        }
        /**
         * 字符串化:便于 main 输出与排查(格式:startMinute-endMinute)。
         */
        @Override
        public String toString() {
            return startMinute + "-" + endMinute;
        }
    }
    /**
     * main 自测:使用固定样例打印 old/new Union、是否修改/缩短,以及点位在旧/新范围内判断结果。
     */
    public static void main(String[] args) {
        String pointJson = "[\"05:00-12:00\", \"13:00-23:59\"]";
        String strategyJson = "[{\"name\": \"策略组1\", \"time\": [\"06:00-12:00\", \"13:00-23:59\"], \"type\": 0, \"typeStr\": []}, {\"name\": \"策略组2\", \"time\": [\"05:00-12:00\", \"12:00-23:00\"], \"type\": 2, \"typeStr\": \"\"}, {\"name\": \"策略组3\", \"time\": [\"09:00-23:59\"], \"type\": 1, \"typeStr\": \"\"}]";
        OperationStrategyIndex index = OperationStrategyIndex.parse(strategyJson);
        List<TimeInterval> pointIntervals = OperationStrategyIndex.parsePointEffectiveTimePeriod(pointJson);
        System.out.println("pointIntervals=" + pointIntervals);
        System.out.println("fenceUnion=" + index.unionAllowed());
        boolean ok = index.isPointScheduleWithin(pointIntervals);
        System.out.println("isNormal=" + ok);
    }
}
xp-robotaxi-map-boot/src/main/resources/i18n/messages.properties
@@ -39,4 +39,4 @@
point.city.not.exist=城市信息不存在
point.number.lock.fail=点位编号生成繁忙,请稍后重试
point.number.parse.fail=点位编号生成失败(历史编号格式异常)
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游点位
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游计划点位
xp-robotaxi-map-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -32,4 +32,4 @@
point.city.not.exist=城市信息不存在
point.number.lock.fail=点位编号生成繁忙,请稍后重试
point.number.parse.fail=点位编号生成失败(历史编号格式异常)
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游点位
point.delete.has.cruise.plan.point=操作失败,该点位有关联的巡游计划点位
xp-robotaxi-map-boot/src/main/resources/mapper/XpPointMapper.xml
@@ -91,7 +91,7 @@
                        <when test="st == 5">OR (t1.verification_status = 5 and t1.operation_status = 1)</when>
                        <when test="st == 6">OR t1.operation_status = 2</when>
                        <when test="st == 7">OR t1.operation_status = 3</when>
                        <when test="st == 8">OR t1.operation_status = 4</when>
                        <when test="st == 8">OR (t1.verification_status = 5 and t1.operation_status = 4)</when>
                        <otherwise>OR 1 = 0</otherwise>
                    </choose>
                </foreach>
@@ -157,7 +157,7 @@
                        <when test="st == 5">OR (xp.verification_status = 5 and xp.operation_status = 1)</when>
                        <when test="st == 6">OR xp.operation_status = 2</when>
                        <when test="st == 7">OR xp.operation_status = 3</when>
                        <when test="st == 8">OR xp.operation_status = 4</when>
                        <when test="st == 8">OR (xp.verification_status = 5 and xp.operation_status = 4)</when>
                        <otherwise>OR 1 = 0</otherwise>
                    </choose>
                </foreach>
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/constant/I18nKeyConstant.java
@@ -100,4 +100,6 @@
    // 邀请码数量上限
    String INVITATION_LIMIT_EXCEEDED = "invitation.limit.exceeded";
    // 包含这个反馈类型
    String INCLUDE = "include";
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpFeedbackTypeManagementController.java
@@ -1,8 +1,11 @@
package com.xiaopeng.operation.controller;
import com.xiaopeng.api.feignClient.maintenance.XpWorkOrderClient;
import com.xiaopeng.api.model.operation.XpFeedbackType;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.operation.constant.I18nKeyConstant;
import com.xiaopeng.operation.service.IXpFeedbackTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -28,9 +31,14 @@
    private final IXpFeedbackTypeService xpFeedbackTypeService;
    private final XpWorkOrderClient xpWorkOrderClient;
    public XpFeedbackTypeManagementController(IXpFeedbackTypeService xpFeedbackTypeService) {
    public XpFeedbackTypeManagementController(IXpFeedbackTypeService xpFeedbackTypeService, XpWorkOrderClient xpWorkOrderClient) {
        this.xpFeedbackTypeService = xpFeedbackTypeService;
        this.xpWorkOrderClient = xpWorkOrderClient;
    }
    @GetMapping("/getList")
    @ApiOperation(value = "获取意见反馈类型列表")
@@ -51,6 +59,11 @@
    @ApiOperation(value = "删除意见反馈类型")
    @DeleteMapping(value = "/deleteFeedbackType")
    public R<Boolean> deleteFeedbackType(Long id) {
        // 判断工单是否包含这个类型
        R<Boolean> include = xpWorkOrderClient.include(id);
        if(include==null || include.getData()){
           throw new ServiceException("该类型被工单引用,无法删除",I18nKeyConstant.INCLUDE);
        }
        return R.ok(xpFeedbackTypeService.removeById(id));
    }
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/exportExcle/XpWhitelistLogExportExcel.java
@@ -32,7 +32,7 @@
    private String operationAccount;
    @ApiModelProperty(value =" 操作类型 1=新增 2=编辑 3=删除 4=导入 5=导出 6=批量冻结")
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "操作类型",width = 20,replace = {"1_新增","2_编辑","3_删除","4_导入","5_导出","6_批量冻结"})
    @cn.afterturn.easypoi.excel.annotation.Excel(name = "操作类型",width = 20,replace = {"新增_1","编辑_2","删除_3","导入_4","导出_5","批量冻结_6"})
    private Integer operationType;
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpAppVersionServiceImpl.java
@@ -62,7 +62,7 @@
        if (Objects.isNull(latestVersion)) {
            log.warn("未找到App版本信息");
            throw new ServiceException("App版本信息不存在");
            return null;
        }
        // 转换为 VO
xp-robotaxi-operation-boot/src/main/resources/i18n/messages.properties
@@ -43,3 +43,4 @@
invitation.import.code.contains.chinese=第{0}行邀请码包含中文:{1}
invitation.import.code.duplicate=导入文件中邀请码重复:{0}
invitation.import.code.exists=以下邀请码已存在:{0}
include=该类型被工单引用,无法删除
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -36,4 +36,5 @@
invitation.import.code.contains.chinese=第{0}行邀请码包含中文:{1}
invitation.import.code.duplicate=导入文件中邀请码重复:{0}
invitation.import.code.exists=以下邀请码已存在:{0}
include=该类型被工单引用,无法删除
invitation.limit.exceeded=系统最多只能有{0}个邀请码,当前已有{1}个,本次新增{2}个,无法生成