xuhy
2026-02-28 d1b1e3179557554302782ce0af580ff6a6679953
Merge remote-tracking branch 'origin/master'
11个文件已添加
28个文件已修改
3265 ■■■■■ 已修改文件
xp-robotaxi-account-boot/src/main/resources/mapper/account/SysUserMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpPointClientFallbackFactory.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/vehicleConvoy/XpFleetOperationsClientFallbackFactory.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpPointClient.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/vehicleConvoy/XpFleetOperationsClient.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/account/SysUser.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpScheduling.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpSchedulingDetail.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/order/XpHistoryEndPointsVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/order/XpHistoryStartPointsVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/constant/TaskScheduleConstants.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpSchedulingDetailController.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/ExportSchedulingDetailDto.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/SchedulingDetailDto.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/InsuranceInspectionScheduleTask.java 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/RoutinePreparationScheduleTask.java 395 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCleanScheduleTask.java 395 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCollectionScheduleTask.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleInspectionScheduleTask.java 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleMaintenanceScheduleTask.java 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpSchedulingDetailService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpSchedulingDetailServiceImpl.java 472 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingDetailDay.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingDetailVo.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingVo.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpSchedulingDetailMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpInvitationCodeController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpPointController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpRegionController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpPointServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpRegionServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/vo/XpOrderHistoryListVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-order-boot/src/main/resources/mapper/order/XpOrderMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpFleetOperationsController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/resources/mapper/account/SysUserMapper.xml
@@ -115,7 +115,7 @@
    <select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
        select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
    </select>
    <select id="getList" resultMap="SysUserResult">
    <select id="getList" resultType="com.xiaopeng.api.model.account.SysUser">
        select su.*,sr.role_name, sur.role_id from sys_user su
        left join sys_user_role sur on su.id= sur.user_id
        left join sys_role sr on sr.id = sur.role_id where 1=1
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/operation/XpPointClientFallbackFactory.java
@@ -14,6 +14,7 @@
import com.xiaopeng.api.query.operation.PointPageListListQuery;
import com.xiaopeng.api.vo.order.XpHistoryEndPointsVo;
import com.xiaopeng.api.vo.order.XpHistoryStartPointsVo;
import com.xiaopeng.api.vo.order.XpPointInfoVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import lombok.extern.slf4j.Slf4j;
@@ -23,6 +24,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@Slf4j
@Component
@@ -101,6 +103,12 @@
                log.error("获取地图点位失败:{}", cause.getMessage());
                return R.ok(new ArrayList<>());
            }
            @Override
            public R<List<XpPoint>> getPointsInfo(Set<Long> points) {
                log.error("获取点位信息失败:{}", cause.getMessage());
                return null;
            }
        };
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/vehicleConvoy/XpFleetOperationsClientFallbackFactory.java
@@ -33,6 +33,13 @@
                log.error("根据人员ID获取车队信息失败 {}",  cause.getMessage());
                return R.fail("根据人员ID获取车队信息失败");
            }
            @Override
            public R<List<Long>> getOperationsIdsByFleetId(Long fleetId) {
                log.error("根据车队ID获取人员信息失败 {}",  cause.getMessage());
                return R.fail("根据车队ID获取人员信息失败");
            }
        };
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/operation/XpPointClient.java
@@ -14,6 +14,7 @@
import com.xiaopeng.api.query.operation.PointPageListListQuery;
import com.xiaopeng.api.vo.order.XpHistoryEndPointsVo;
import com.xiaopeng.api.vo.order.XpHistoryStartPointsVo;
import com.xiaopeng.api.vo.order.XpPointInfoVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum;
import com.xiaopeng.common.core.web.page.PageInfo;
@@ -22,6 +23,7 @@
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
@FeignClient(contextId = "XpPointClient", value = ServiceNameConstantsEnum.XP_OPERATION_SERVICE, fallbackFactory = XpPointClientFallbackFactory.class)
public interface XpPointClient {
@@ -74,6 +76,7 @@
    R<List<XpPoint>> pointMap(@RequestBody PointMapQuery query);
    @PostMapping("/base/v1/mini-program/inner/point/getPointsInfo")
    R<List<XpPoint>> getPointsInfo(@RequestBody Set<Long> points);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/vehicleConvoy/XpFleetOperationsClient.java
@@ -37,4 +37,14 @@
     */
    @GetMapping("/inner/fleet-operations/getFleetIdsByUserId/{userId}")
    R<List<Long>> getFleetIdsByUserId(@PathVariable("userId") Long userId);
    /**
     * 根据车队ID查询关联的运维人员ID列表
     *
     * @param fleetId 车队ID
     * @return 运维人员ID列表
     */
    @GetMapping("/inner/fleet-operations/getOperationsIdsByFleetId/{fleetId}")
    R<List<Long>> getOperationsIdsByFleetId(@PathVariable("fleetId") Long fleetId);
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/account/SysUser.java
@@ -86,6 +86,10 @@
    @TableField(exist = false)
    private Long deptId;
    @ApiModelProperty(value = "角色名称")
    @TableField(exist = false)
    private String roleName;
    public boolean isAdmin()
    {
        return isAdmin(this.id);
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpScheduling.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.io.Serializable;
@@ -34,7 +36,7 @@
    private Long id;
    @ApiModelProperty(value = "排版日期")
    private LocalDateTime schedulingDate;
    private LocalDate schedulingDate;
    @ApiModelProperty(value = "排版人员id")
    private Long commitUserId;
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpSchedulingDetail.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.io.Serializable;
@@ -46,6 +48,6 @@
    private Boolean isAllDay;
    @ApiModelProperty(value = "计划时间")
    private LocalDateTime planTime;
    private LocalDate planTime;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/maintenance/XpTask.java
@@ -85,4 +85,6 @@
    @ApiModelProperty(value = "状态 0=待到达 1=待处理 2=已完成 3=已取消")
    private Integer status;
    private String belongUserIds;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/order/XpHistoryEndPointsVo.java
@@ -25,4 +25,10 @@
    @ApiModelProperty("站点id")
    private Long id;
    @ApiModelProperty("停靠时间")
    private Integer time;
    private Long electronicAreaId;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/order/XpHistoryStartPointsVo.java
@@ -25,4 +25,10 @@
    @ApiModelProperty("站点id")
    private Long id;
    @ApiModelProperty("停靠时间")
    private Integer time;
    private Long electronicAreaId;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/constant/TaskScheduleConstants.java
New file
@@ -0,0 +1,169 @@
package com.xiaopeng.maintenance.constant;
/**
 * 定时任务常量类
 *
 * @author mitao
 * @since 2026-02-27
 */
public class TaskScheduleConstants {
    private TaskScheduleConstants() {
        // 私有构造函数,防止实例化
    }
    /**
     * 任务类别 - 常规任务
     */
    public static final Integer TASK_CATEGORY_ROUTINE = 1;
    /**
     * 任务类别 - 事故任务
     */
    public static final Integer TASK_CATEGORY_ACCIDENT = 2;
    /**
     * 任务类型 - 车辆检查
     */
    public static final String TASK_TYPE_DETECTION = "1";
    /**
     * 任务类型 - 车辆充电
     */
    public static final String TASK_TYPE_CHARGING = "2";
    /**
     * 任务类型 - 车辆整备
     */
    public static final String TASK_TYPE_PREPARATION = "3";
    /**
     * 任务类型 - 整备洗车
     */
    public static final String TASK_TYPE_CLEAN = "4";
    /**
     * 任务类型 - 车辆维修
     */
    public static final String TASK_TYPE_REPAIR = "5";
    /**
     * 任务类型 - 车辆保养
     */
    public static final String TASK_TYPE_MAINTENANCE = "6";
    /**
     * 任务类型 - 车辆保险到期
     */
    public static final String TASK_TYPE_INSURANCE_EXPIRE = "7";
    /**
     * 任务类型 - 车辆年检到期
     */
    public static final String TASK_TYPE_INSPECTION_EXPIRE = "8";
    /**
     * 任务类型 - 车辆运营资质到期
     */
    public static final String TASK_TYPE_QUALIFICATION_EXPIRE = "9";
    /**
     * 任务状态 - 待到达
     */
    public static final Integer TASK_STATUS_PENDING_ARRIVE = 0;
    /**
     * 任务状态 - 待处理
     */
    public static final Integer TASK_STATUS_PENDING = 1;
    /**
     * 任务状态 - 已完成
     */
    public static final Integer TASK_STATUS_COMPLETED = 2;
    /**
     * 任务状态 - 已取消
     */
    public static final Integer TASK_STATUS_CANCELLED = 3;
    /**
     * 任务规则角色类型 - 车辆日常检测任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_INSPECTION = 1;
    /**
     * 任务规则角色类型 - 车辆每日收车
     */
    public static final Integer RULE_ROLE_TYPE_COLLECTION = 2;
    /**
     * 任务规则角色类型 - 车辆保养周期任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_MAINTENANCE = 3;
    /**
     * 任务规则角色类型 - 充电策略任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_CHARGING = 4;
    /**
     * 任务规则角色类型 - 车身清洁任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_CLEAN = 5;
    /**
     * 任务规则角色类型 - 日常整备任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_PREPARATION = 6;
    /**
     * 任务规则角色类型 - 年检/保险策略任务接收角色
     */
    public static final Integer RULE_ROLE_TYPE_INSURANCE = 7;
    /**
     * 任务规则角色类型 - 事故任务上报角色
     */
    public static final Integer RULE_ROLE_TYPE_ACCIDENT_REPORT = 8;
    /**
     * 任务规则角色类型 - 事故任务处理角色
     */
    public static final Integer RULE_ROLE_TYPE_ACCIDENT_HANDLE = 9;
    /**
     * 定时任务执行状态 - 成功
     */
    public static final Integer SCHEDULE_STATUS_SUCCESS = 1;
    /**
     * 定时任务执行状态 - 失败
     */
    public static final Integer SCHEDULE_STATUS_FAILED = 0;
    /**
     * 定时任务创建者名称
     */
    public static final String SCHEDULE_CREATOR = "系统定时任务";
    /**
     * 任务紧急程度 - P0
     */
    public static final Integer URGENCY_LEVEL_P0 = 0;
    /**
     * 任务紧急程度 - P1
     */
    public static final Integer URGENCY_LEVEL_P1 = 1;
    /**
     * 任务紧急程度 - P2
     */
    public static final Integer URGENCY_LEVEL_P2 = 2;
    /**
     * 任务紧急程度 - P3
     */
    public static final Integer URGENCY_LEVEL_P3 = 3;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/controller/XpSchedulingDetailController.java
@@ -1,13 +1,15 @@
package com.xiaopeng.maintenance.controller;
import com.xiaopeng.api.model.maintenance.XpErrorType;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.api.model.account.SysUser;import com.xiaopeng.api.model.maintenance.XpErrorType;
import com.xiaopeng.api.vo.account.LoginUser;import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.common.security.service.TokenService;
import com.xiaopeng.common.security.utils.SecurityUtils;
import com.xiaopeng.maintenance.dto.SchedulingDetailDto;
import com.xiaopeng.maintenance.dto.ShiftAttendanceDTO;
import com.xiaopeng.maintenance.service.IXpSchedulingDetailService;
import com.xiaopeng.maintenance.vo.SaveSchedulingVo;
import com.xiaopeng.maintenance.vo.SchedulingDetailVo;
import com.xiaopeng.maintenance.vo.ShiftAttendanceVO;
import io.swagger.annotations.Api;
@@ -15,6 +17,9 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -30,9 +35,10 @@
@RequestMapping("")
public class XpSchedulingDetailController {
    @Autowired
    @Resource
    private IXpSchedulingDetailService xpSchedulingDetailService;
    @Autowired
    @Resource
    private TokenService tokenService;
    @ApiOperation(value = "查询个人排班与考勤信息", response = XpErrorType.class, tags = "运维APP-排班与考勤")
@@ -44,11 +50,27 @@
    }
//    public R<PageInfo<SchedulingDetailDto>> pageList(@RequestBody SchedulingDetailVo query){
//
//    }
    @ApiOperation(value = "排班考勤-排班计划管理-查询列表", response = XpErrorType.class, tags = "管理后台-运维执行")
    @PostMapping("/api/v1/scheduling/pageList")
    public R<PageInfo<SchedulingDetailDto>> pageList(@RequestBody SchedulingDetailVo query){
        PageInfo<SchedulingDetailDto> pageInfo = xpSchedulingDetailService.pageList(query);
        return R.ok(pageInfo);
    }
    @ApiOperation(value = "排班考勤-排班计划管理-保存排班数据", response = XpErrorType.class, tags = "管理后台-运维执行")
    @PostMapping("/api/v1/scheduling/saveSchedulingDetail")
    public R<?> saveSchedulingDetail(@RequestBody SaveSchedulingVo vo){
        SysUser sysUser = SecurityUtils.getLoginUser().getSysUser();
        return xpSchedulingDetailService.saveSchedulingDetail(vo, sysUser.getId());
    }
    @ApiOperation(value = "排班考勤-排班计划管理-导出排班数据", response = XpErrorType.class, tags = "管理后台-运维执行")
    @GetMapping("/api/v1/scheduling/exportExcel")
    public void exportExcel(@RequestParam("month") String month, HttpServletResponse response) {
        xpSchedulingDetailService.exportExcel(month, response);
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/ExportSchedulingDetailDto.java
New file
@@ -0,0 +1,92 @@
package com.xiaopeng.maintenance.dto;
import com.xiaopeng.common.core.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Map;
@Data
@ApiModel
public class ExportSchedulingDetailDto {
    private String fleetId;
    @Excel(name = "序号", sort = 1)
    private String sort;
    @Excel(name = "队伍", sort = 2)
    private String fleetName;
    @Excel(name = "1", sort = 3)
    private String one;
    @Excel(name = "2", sort = 4)
    private String two;
    @Excel(name = "3", sort = 5)
    private String three;
    @Excel(name = "4", sort = 6)
    private String four;
    @Excel(name = "5", sort = 7)
    private String five;
    @Excel(name = "6", sort = 8)
    private String six;
    @Excel(name = "7", sort = 9)
    private String seven;
    @Excel(name = "8", sort = 10)
    private String eight;
    @Excel(name = "9", sort = 11)
    private String nine;
    @Excel(name = "10", sort = 12)
    private String ten;
    @Excel(name = "11", sort = 13)
    private String eleven;
    @Excel(name = "12", sort = 14)
    private String twelve;
    @Excel(name = "13", sort = 15)
    private String thirteen;
    @Excel(name = "14", sort = 16)
    private String fourteen;
    @Excel(name = "15", sort = 17)
    private String fifteen;
    @Excel(name = "16", sort = 18)
    private String sixteen;
    @Excel(name = "17", sort = 19)
    private String seventeen;
    @Excel(name = "18", sort = 20)
    private String eighteen;
    @Excel(name = "19", sort = 21)
    private String nineteen;
    @Excel(name = "20", sort = 22)
    private String twenty;
    @Excel(name = "21", sort = 23)
    private String twentyOne;
    @Excel(name = "22", sort = 24)
    private String twentyTwo;
    @Excel(name = "23", sort = 25)
    private String twentyThree;
    @Excel(name = "24", sort = 26)
    private String twentyFour;
    @Excel(name = "25", sort = 27)
    private String twentyFive;
    @Excel(name = "26", sort = 28)
    private String twentySix;
    @Excel(name = "27", sort = 29)
    private String twentySeven;
    @Excel(name = "28", sort = 30)
    private String twentyEight;
    @Excel(name = "29", sort = 31)
    private String twentyNine;
    @Excel(name = "30", sort = 32)
    private String thirty;
    @Excel(name = "31", sort = 33)
    private String thirtyOne;
    @Excel(name = "排班小时", sort = 34)
    private String allDay;
    @Excel(name = "", sort = 35)
    private String morning;
    @Excel(name = "", sort = 36)
    private String midDay;
    @Excel(name = "", sort = 37)
    private String evening;
    /**
     * 天数,用逗号分隔
     */
    private String days;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/dto/SchedulingDetailDto.java
@@ -4,73 +4,87 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Map;
@Data
@ApiModel
public class SchedulingDetailDto {
    @ApiModelProperty("排班ID")
    private Long schedulingId;
    @ApiModelProperty("车队ID")
    private String fleetId;
    @ApiModelProperty("车队")
    private String fleetName;
    @ApiModelProperty("1")
    private String one;
    private Map<String, String> one;
    @ApiModelProperty("2")
    private String two;
    private Map<String, String> two;
    @ApiModelProperty("3")
    private String three;
    private Map<String, String> three;
    @ApiModelProperty("4")
    private String four;
    private Map<String, String> four;
    @ApiModelProperty("5")
    private String five;
    private Map<String, String> five;
    @ApiModelProperty("6")
    private String six;
    private Map<String, String> six;
    @ApiModelProperty("7")
    private String seven;
    private Map<String, String> seven;
    @ApiModelProperty("8")
    private String eight;
    private Map<String, String> eight;
    @ApiModelProperty("9")
    private String nine;
    private Map<String, String> nine;
    @ApiModelProperty("10")
    private String ten;
    private Map<String, String> ten;
    @ApiModelProperty("11")
    private String eleven;
    private Map<String, String> eleven;
    @ApiModelProperty("12")
    private String twelve;
    private Map<String, String> twelve;
    @ApiModelProperty("13")
    private String thirteen;
    private Map<String, String> thirteen;
    @ApiModelProperty("14")
    private String fourteen;
    private Map<String, String> fourteen;
    @ApiModelProperty("15")
    private String fifteen;
    private Map<String, String> fifteen;
    @ApiModelProperty("16")
    private String sixteen;
    private Map<String, String> sixteen;
    @ApiModelProperty("17")
    private String seventeen;
    private Map<String, String> seventeen;
    @ApiModelProperty("18")
    private String eighteen;
    private Map<String, String> eighteen;
    @ApiModelProperty("19")
    private String nineteen;
    private Map<String, String> nineteen;
    @ApiModelProperty("20")
    private String twenty;
    private Map<String, String> twenty;
    @ApiModelProperty("21")
    private String twentyOne;
    private Map<String, String> twentyOne;
    @ApiModelProperty("22")
    private String twentyTwo;
    private Map<String, String> twentyTwo;
    @ApiModelProperty("23")
    private String twentyThree;
    private Map<String, String> twentyThree;
    @ApiModelProperty("24")
    private String twentyFour;
    private Map<String, String> twentyFour;
    @ApiModelProperty("25")
    private String twentyFive;
    private Map<String, String> twentyFive;
    @ApiModelProperty("26")
    private String twentySix;
    private Map<String, String> twentySix;
    @ApiModelProperty("27")
    private String twentySeven;
    private Map<String, String> twentySeven;
    @ApiModelProperty("28")
    private String twentyEight;
    private Map<String, String> twentyEight;
    @ApiModelProperty("29")
    private String twentyNine;
    private Map<String, String> twentyNine;
    @ApiModelProperty("30")
    private String thirty;
    private Map<String, String> thirty;
    @ApiModelProperty("31")
    private String thirtyOne;
    private Map<String, String> thirtyOne;
    @ApiModelProperty("全天")
    private Long allDay;
    @ApiModelProperty("早")
    private Long morning;
    @ApiModelProperty("中")
    private Long midDay;
    @ApiModelProperty("晚")
    private Long evening;
    /**
     * 天数,用逗号分隔
     */
    private String days;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/InsuranceInspectionScheduleTask.java
New file
@@ -0,0 +1,433 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 年检/保险策略定时任务
 * <p>
 * 根据任务规则配置,在车辆保险、年检、运营资质即将到期时自动创建提醒任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class InsuranceInspectionScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(InsuranceInspectionScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "年检/保险策略定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    /**
     * 每天凌晨1点执行一次
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断年检/保险策略功能是否启用
     * 3. 获取所有运营中的车辆
     * 4. 分别检查三种到期情况并创建任务:
     *    - 保险到期
     *    - 年检到期
     *    - 运营资质到期
     * </p>
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void executeInsuranceInspectionTask() {
        log.info("========== 开始执行年检/保险策略定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断年检/保险策略功能是否启用
        if (!Boolean.TRUE.equals(taskRule.getInsurance())) {
            log.info("年检/保险策略功能未启用,跳过执行");
            return;
        }
        log.info("开始执行年检/保险策略任务下发,保险提前{}天, 年检提前{}天, 运营资质提前{}天, 紧急程度: P{}",
                taskRule.getInsuranceExpires(),
                taskRule.getInsuranceInspectionExpires(),
                taskRule.getOperationQualificationExpires(),
                taskRule.getInsuranceLevel());
        try {
            // 执行任务创建
            int createdCount = createExpireTasks(taskRule);
            log.info("========== 年检/保险策略定时任务执行完成,创建任务数: {} ==========", createdCount);
        } catch (Exception e) {
            log.error("年检/保险策略定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 创建到期提醒任务
     *
     * @param taskRule 任务规则配置
     * @return 创建的任务数量
     */
    private int createExpireTasks(XpTaskRule taskRule) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_INSURANCE);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到年检/保险任务接收角色的运维人员");
            return 0;
        }
        log.info("找到 {} 个运维人员可接收年检/保险任务", operationUserIds.size());
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建到期提醒任务");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
        int totalCreatedCount = 0;
        // 3. 检查保险到期
        totalCreatedCount += checkAndCreateInsuranceExpireTasks(vehicles, operationUserIds, taskRule);
        // 4. 检查年检到期
        totalCreatedCount += checkAndCreateInspectionExpireTasks(vehicles, operationUserIds, taskRule);
        // 5. 检查运营资质到期
        totalCreatedCount += checkAndCreateQualificationExpireTasks(vehicles, operationUserIds, taskRule);
        return totalCreatedCount;
    }
    /**
     * 检查并创建保险到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateInsuranceExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getInsuranceExpires() == null || taskRule.getInsuranceExpires() <= 0) {
            log.info("保险到期提醒天数未配置,跳过保险到期检查");
            return 0;
        }
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getInsuranceEndTime(), taskRule.getInsuranceExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_INSURANCE_EXPIRE))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要保险到期提醒的车辆");
            return 0;
        }
        log.info("找到 {} 辆车辆需要保险到期提醒", needWarnVehicles.size());
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_INSURANCE_EXPIRE,
                "保险即将到期"
        );
    }
    /**
     * 检查并创建年检到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateInspectionExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getInsuranceInspectionExpires() == null || taskRule.getInsuranceInspectionExpires() <= 0) {
            log.info("年检到期提醒天数未配置,跳过年检到期检查");
            return 0;
        }
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getAnnualInspectionDate(), taskRule.getInsuranceInspectionExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_INSPECTION_EXPIRE))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要年检到期提醒的车辆");
            return 0;
        }
        log.info("找到 {} 辆车辆需要年检到期提醒", needWarnVehicles.size());
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_INSPECTION_EXPIRE,
                "年检即将到期"
        );
    }
    /**
     * 检查并创建运营资质到期任务
     *
     * @param vehicles         车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int checkAndCreateQualificationExpireTasks(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        if (taskRule.getOperationQualificationExpires() == null || taskRule.getOperationQualificationExpires() <= 0) {
            log.info("运营资质到期提醒天数未配置,跳过运营资质到期检查");
            return 0;
        }
        List<XpVehicle> needWarnVehicles = vehicles.stream()
                .filter(v -> isWithinExpireWarningDays(v.getQualificationExpireTime(), taskRule.getOperationQualificationExpires()))
                .filter(v -> !hasUnfinishedTask(v.getId(), TaskScheduleConstants.TASK_TYPE_QUALIFICATION_EXPIRE))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(needWarnVehicles)) {
            log.info("没有需要运营资质到期提醒的车辆");
            return 0;
        }
        log.info("找到 {} 辆车辆需要运营资质到期提醒", needWarnVehicles.size());
        return createTasksForExpireType(
                needWarnVehicles,
                operationUserIds,
                taskRule,
                TaskScheduleConstants.TASK_TYPE_QUALIFICATION_EXPIRE,
                "运营资质即将到期"
        );
    }
    /**
     * 判断是否在到期提醒范围内
     *
     * @param expireTime        到期时间
     * @param daysBeforeExpire 提前X天提醒
     * @return 是否需要提醒
     */
    private boolean isWithinExpireWarningDays(LocalDateTime expireTime, Integer daysBeforeExpire) {
        if (expireTime == null || daysBeforeExpire == null || daysBeforeExpire <= 0) {
            return false;
        }
        LocalDate today = LocalDate.now();
        LocalDate expireDate = expireTime.toLocalDate();
        long daysUntilExpire = ChronoUnit.DAYS.between(today, expireDate);
        // 距离到期天数 <= 配置的提前天数,且未过期(daysUntilExpire >= 0)
        return daysUntilExpire >= 0 && daysUntilExpire <= daysBeforeExpire;
    }
    /**
     * 检查是否已存在未完成的同类型任务
     * <p>
     * 避免重复创建到期提醒任务
     * </p>
     *
     * @param vehicleId  车辆ID
     * @param taskTypeId 任务类型ID
     * @return 是否存在未完成任务
     */
    private boolean hasUnfinishedTask(Long vehicleId, String taskTypeId) {
        XpTask existingTask = taskMapper.selectOne(
                new LambdaQueryWrapper<XpTask>()
                        .eq(XpTask::getVehicleId, vehicleId)
                        .eq(XpTask::getTaskTypeId, taskTypeId)
                        .in(XpTask::getStatus,
                                TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE,
                                TaskScheduleConstants.TASK_STATUS_PENDING)
                        .last("limit 1")
        );
        return existingTask != null;
    }
    /**
     * 为指定到期类型创建任务
     *
     * @param vehicles         需要提醒的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @param taskTypeId       任务类型ID
     * @param taskDescription  任务描述
     * @return 创建的任务数量
     */
    private int createTasksForExpireType(List<XpVehicle> vehicles, List<Long> operationUserIds,
                                          XpTaskRule taskRule, String taskTypeId, String taskDescription) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(taskTypeId);
            task.setTaskUrgencyLevel(taskRule.getInsuranceLevel() != null
                    ? taskRule.getInsuranceLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的" + taskDescription + "提醒任务");
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
            // 设置任务时限
            if (taskRule.getProcessingTimeLimit() != null) {
                task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
            }
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setBelongUserIds(belongUserIds);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个{}提醒任务", tasksToSave.size(), taskDescription);
        }
        return tasksToSave.size();
    }
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
        return new ArrayList<>(userIds);
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/RoutinePreparationScheduleTask.java
New file
@@ -0,0 +1,395 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 日常整备定时任务
 * <p>
 * 根据任务规则配置,在指定时间自动创建日常整备任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class RoutinePreparationScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(RoutinePreparationScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "日常整备定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    /**
     * 每分钟检查一次是否需要执行任务下发
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断日常整备功能是否启用(周期值 > 0)
     * 3. 判断当前时间是否为配置的任务下发时间
     * 4. 查询所有运营中的车辆
     * 5. 根据整备周期判断是否需要创建任务
     * 6. 创建任务并分配给相关角色的运维人员
     * </p>
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executePreparationTask() {
        log.info("========== 开始执行日常整备定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断日常整备功能是否启用(周期值 > 0 表示启用)
        if (!isEnabled(taskRule)) {
            log.info("日常整备功能未启用(周期值未配置或为0),跳过执行");
            return;
        }
        // 3. 判断当前时间是否为配置的任务下发时间
        if (!isExecuteTime(taskRule.getRoutineMaintenanceTime())) {
            log.debug("当前时间不是任务下发时间,跳过执行");
            return;
        }
        // 计算整备周期(天数)
        int cycleDays = calculateCycleDays(taskRule.getRoutineMaintenanceCycle(), taskRule.getRoutineMaintenanceTimeType());
        log.info("开始执行日常整备任务下发,整备周期: {}天, 紧急程度: P{}",
                cycleDays,
                taskRule.getRoutineMaintenanceLevel());
        try {
            // 执行任务创建
            int createdCount = createPreparationTasks(taskRule, cycleDays);
            log.info("========== 日常整备定时任务执行完成,创建任务数: {} ==========", createdCount);
        } catch (Exception e) {
            log.error("日常整备定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 判断日常整备功能是否启用
     * <p>
     * 根据周期值判断:周期值 > 0 则认为启用
     * </p>
     *
     * @param taskRule 任务规则
     * @return 是否启用
     */
    private boolean isEnabled(XpTaskRule taskRule) {
        return taskRule.getRoutineMaintenanceCycle() != null
                && taskRule.getRoutineMaintenanceCycle() > 0;
    }
    /**
     * 判断当前时间是否为任务下发时间
     * <p>
     * 精确到分钟级别的时间匹配
     * </p>
     *
     * @param executeTime 配置的执行时间
     * @return 是否应该执行
     */
    private boolean isExecuteTime(LocalTime executeTime) {
        if (executeTime == null) {
            log.warn("任务下发时间未配置");
            return false;
        }
        LocalTime now = LocalTime.now();
        // 精确到分钟匹配
        return executeTime.getHour() == now.getHour() && executeTime.getMinute() == now.getMinute();
    }
    /**
     * 计算整备周期天数
     * <p>
     * 根据周期类型转换为天数:
     * - 类型1:天,直接返回周期数值
     * - 类型2:月,按30天/月计算
     * </p>
     *
     * @param cycleTime     周期数值
     * @param cycleTimeType 周期类型:1=天, 2=月
     * @return 周期天数
     */
    private int calculateCycleDays(Integer cycleTime, Integer cycleTimeType) {
        // 默认周期为1
        if (cycleTime == null || cycleTime <= 0) {
            cycleTime = 1;
        }
        // 默认为天
        if (cycleTimeType == null || cycleTimeType == 1) {
            return cycleTime;
        } else {
            // 月(按30天计算)
            return cycleTime * 30;
        }
    }
    /**
     * 创建日常整备任务
     *
     * @param taskRule  任务规则配置
     * @param cycleDays 整备周期(天数)
     * @return 创建的任务数量
     */
    private int createPreparationTasks(XpTaskRule taskRule, int cycleDays) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_PREPARATION);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到日常整备任务接收角色的运维人员");
            return 0;
        }
        log.info("找到 {} 个运维人员可接收日常整备任务", operationUserIds.size());
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建整备任务");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
        // 3. 过滤出今日需要整备的车辆
        List<XpVehicle> needPreparationVehicles = filterVehiclesNeedPreparation(vehicles, cycleDays);
        if (CollectionUtils.isEmpty(needPreparationVehicles)) {
            log.info("今日没有需要整备的车辆");
            return 0;
        }
        log.info("过滤后有 {} 辆车辆需要今日整备", needPreparationVehicles.size());
        // 4. 创建任务并分配给运维人员
        return createTasksAndAssign(needPreparationVehicles, operationUserIds, taskRule);
    }
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
        return new ArrayList<>(userIds);
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
    /**
     * 过滤出需要整备的车辆
     * <p>
     * 根据整备周期判断:查询车辆上次整备任务时间,
     * 如果距今天数 >= 配置周期,则需要整备
     * </p>
     *
     * @param vehicles  所有车辆
     * @param cycleDays 整备周期(天数)
     * @return 需要整备的车辆列表
     */
    private List<XpVehicle> filterVehiclesNeedPreparation(List<XpVehicle> vehicles, int cycleDays) {
        LocalDate today = LocalDate.now();
        List<XpVehicle> needPreparationVehicles = new ArrayList<>();
        for (XpVehicle vehicle : vehicles) {
            // 查询该车辆最近一次整备任务
            XpTask lastTask = taskMapper.selectOne(
                    new LambdaQueryWrapper<XpTask>()
                            .eq(XpTask::getVehicleId, vehicle.getId())
                            .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ROUTINE)
                            .eq(XpTask::getTaskTypeId, TaskScheduleConstants.TASK_TYPE_PREPARATION)
                            .orderByDesc(XpTask::getCreateTime)
                            .last("limit 1")
            );
            // 如果从未整备过,需要整备
            if (lastTask == null || lastTask.getCreateTime() == null) {
                needPreparationVehicles.add(vehicle);
                continue;
            }
            // 计算距离上次整备的天数
            LocalDate lastPreparationDate = lastTask.getCreateTime().toLocalDate();
            long daysSinceLastPreparation = java.time.temporal.ChronoUnit.DAYS.between(lastPreparationDate, today);
            // 如果距离上次整备天数 >= 配置周期,则需要整备
            if (daysSinceLastPreparation >= cycleDays) {
                needPreparationVehicles.add(vehicle);
            }
        }
        return needPreparationVehicles;
    }
    /**
     * 创建任务并分配给运维人员
     *
     * @param vehicles         需要整备的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int createTasksAndAssign(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(TaskScheduleConstants.TASK_TYPE_PREPARATION);
            task.setTaskUrgencyLevel(taskRule.getRoutineMaintenanceLevel() != null
                    ? taskRule.getRoutineMaintenanceLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的日常整备任务");
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
            // 设置任务时限
            if (taskRule.getProcessingTimeLimit() != null) {
                task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
            }
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setBelongUserIds(belongUserIds);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个日常整备任务", tasksToSave.size());
        }
        return tasksToSave.size();
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCleanScheduleTask.java
New file
@@ -0,0 +1,395 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 车身清洁定时任务
 * <p>
 * 根据任务规则配置,在指定时间自动创建车身清洁任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class VehicleCleanScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(VehicleCleanScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "车身清洁定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    /**
     * 每分钟检查一次是否需要执行任务下发
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断车身清洁功能是否启用(周期值 > 0)
     * 3. 判断当前时间是否为配置的任务下发时间
     * 4. 查询所有运营中的车辆
     * 5. 根据清洁周期判断是否需要创建任务
     * 6. 创建任务并分配给相关角色的运维人员
     * </p>
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executeCleanTask() {
        log.info("========== 开始执行车身清洁定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断车身清洁功能是否启用(周期值 > 0 表示启用)
        if (!isEnabled(taskRule)) {
            log.info("车身清洁功能未启用(周期值未配置或为0),跳过执行");
            return;
        }
        // 3. 判断当前时间是否为配置的任务下发时间
        if (!isExecuteTime(taskRule.getVehicleCleanTime())) {
            log.debug("当前时间不是任务下发时间,跳过执行");
            return;
        }
        // 计算清洁周期(天数)
        int cycleDays = calculateCycleDays(taskRule.getVehicleCleanCycle(), taskRule.getVehicleCleanTimeType());
        log.info("开始执行车身清洁任务下发,清洁周期: {}天, 紧急程度: P{}",
                cycleDays,
                taskRule.getVehicleCleanLevel());
        try {
            // 执行任务创建
            int createdCount = createCleanTasks(taskRule, cycleDays);
            log.info("========== 车身清洁定时任务执行完成,创建任务数: {} ==========", createdCount);
        } catch (Exception e) {
            log.error("车身清洁定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 判断车身清洁功能是否启用
     * <p>
     * 根据周期值判断:周期值 > 0 则认为启用
     * </p>
     *
     * @param taskRule 任务规则
     * @return 是否启用
     */
    private boolean isEnabled(XpTaskRule taskRule) {
        return taskRule.getVehicleCleanCycle() != null
                && taskRule.getVehicleCleanCycle() > 0;
    }
    /**
     * 判断当前时间是否为任务下发时间
     * <p>
     * 精确到分钟级别的时间匹配
     * </p>
     *
     * @param executeTime 配置的执行时间
     * @return 是否应该执行
     */
    private boolean isExecuteTime(LocalTime executeTime) {
        if (executeTime == null) {
            log.warn("任务下发时间未配置");
            return false;
        }
        LocalTime now = LocalTime.now();
        // 精确到分钟匹配
        return executeTime.getHour() == now.getHour() && executeTime.getMinute() == now.getMinute();
    }
    /**
     * 计算清洁周期天数
     * <p>
     * 根据周期类型转换为天数:
     * - 类型1:天,直接返回周期数值
     * - 类型2:月,按30天/月计算
     * </p>
     *
     * @param cycleTime     周期数值
     * @param cycleTimeType 周期类型:1=天, 2=月
     * @return 周期天数
     */
    private int calculateCycleDays(Integer cycleTime, Integer cycleTimeType) {
        // 默认周期为1
        if (cycleTime == null || cycleTime <= 0) {
            cycleTime = 1;
        }
        // 默认为天
        if (cycleTimeType == null || cycleTimeType == 1) {
            return cycleTime;
        } else {
            // 月(按30天计算)
            return cycleTime * 30;
        }
    }
    /**
     * 创建车身清洁任务
     *
     * @param taskRule  任务规则配置
     * @param cycleDays 清洁周期(天数)
     * @return 创建的任务数量
     */
    private int createCleanTasks(XpTaskRule taskRule, int cycleDays) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_CLEAN);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到车身清洁任务接收角色的运维人员");
            return 0;
        }
        log.info("找到 {} 个运维人员可接收车身清洁任务", operationUserIds.size());
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建清洁任务");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
        // 3. 过滤出今日需要清洁的车辆
        List<XpVehicle> needCleanVehicles = filterVehiclesNeedClean(vehicles, cycleDays);
        if (CollectionUtils.isEmpty(needCleanVehicles)) {
            log.info("今日没有需要清洁的车辆");
            return 0;
        }
        log.info("过滤后有 {} 辆车辆需要今日清洁", needCleanVehicles.size());
        // 4. 创建任务并分配给运维人员
        return createTasksAndAssign(needCleanVehicles, operationUserIds, taskRule);
    }
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
        return new ArrayList<>(userIds);
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
    /**
     * 过滤出需要清洁的车辆
     * <p>
     * 根据清洁周期判断:查询车辆上次清洁任务时间,
     * 如果距今天数 >= 配置周期,则需要清洁
     * </p>
     *
     * @param vehicles  所有车辆
     * @param cycleDays 清洁周期(天数)
     * @return 需要清洁的车辆列表
     */
    private List<XpVehicle> filterVehiclesNeedClean(List<XpVehicle> vehicles, int cycleDays) {
        LocalDate today = LocalDate.now();
        List<XpVehicle> needCleanVehicles = new ArrayList<>();
        for (XpVehicle vehicle : vehicles) {
            // 查询该车辆最近一次清洁任务
            XpTask lastTask = taskMapper.selectOne(
                    new LambdaQueryWrapper<XpTask>()
                            .eq(XpTask::getVehicleId, vehicle.getId())
                            .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ROUTINE)
                            .eq(XpTask::getTaskTypeId, TaskScheduleConstants.TASK_TYPE_CLEAN)
                            .orderByDesc(XpTask::getCreateTime)
                            .last("limit 1")
            );
            // 如果从未清洁过,需要清洁
            if (lastTask == null || lastTask.getCreateTime() == null) {
                needCleanVehicles.add(vehicle);
                continue;
            }
            // 计算距离上次清洁的天数
            LocalDate lastCleanDate = lastTask.getCreateTime().toLocalDate();
            long daysSinceLastClean = java.time.temporal.ChronoUnit.DAYS.between(lastCleanDate, today);
            // 如果距离上次清洁天数 >= 配置周期,则需要清洁
            if (daysSinceLastClean >= cycleDays) {
                needCleanVehicles.add(vehicle);
            }
        }
        return needCleanVehicles;
    }
    /**
     * 创建任务并分配给运维人员
     *
     * @param vehicles         需要清洁的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int createTasksAndAssign(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(TaskScheduleConstants.TASK_TYPE_CLEAN);
            task.setTaskUrgencyLevel(taskRule.getVehicleCleanLevel() != null
                    ? taskRule.getVehicleCleanLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的车身清洁任务");
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
            // 设置任务时限
            if (taskRule.getProcessingTimeLimit() != null) {
                task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
            }
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setBelongUserIds(belongUserIds);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个车身清洁任务", tasksToSave.size());
        }
        return tasksToSave.size();
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleCollectionScheduleTask.java
New file
@@ -0,0 +1,188 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 车辆收车定时任务
 * <p>
 * 根据任务规则配置,在指定时间自动向所有车辆发起收车指令
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class VehicleCollectionScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(VehicleCollectionScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "车辆收车定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    /**
     * 每分钟检查一次是否需要执行收车任务
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断收车功能是否启用
     * 3. 判断当前时间是否为配置的收车时间
     * 4. 获取所有运营中的车辆
     * 5. 向每辆车发送收车指令
     * </p>
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executeCollectionTask() {
        log.info("========== 开始执行车辆收车定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断收车功能是否启用
        if (!Boolean.TRUE.equals(taskRule.getVehicleCollection())) {
            log.info("车辆收车功能未启用,跳过执行");
            return;
        }
        // 3. 判断当前时间是否为收车时间
        if (!isExecuteTime(taskRule.getVehicleCollectionTime())) {
            log.debug("当前时间不是收车时间,跳过执行");
            return;
        }
        log.info("开始执行车辆收车任务");
        try {
            // 执行收车逻辑
            int processedCount = executeVehicleCollection();
            log.info("========== 车辆收车定时任务执行完成,处理车辆数: {} ==========", processedCount);
        } catch (Exception e) {
            log.error("车辆收车定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 判断当前时间是否为收车时间
     * <p>
     * 精确到分钟级别的时间匹配
     * </p>
     *
     * @param executeTime 配置的执行时间
     * @return 是否应该执行
     */
    private boolean isExecuteTime(LocalTime executeTime) {
        if (executeTime == null) {
            log.warn("收车时间未配置");
            return false;
        }
        LocalTime now = LocalTime.now();
        // 精确到分钟匹配
        return executeTime.getHour() == now.getHour() && executeTime.getMinute() == now.getMinute();
    }
    /**
     * 执行车辆收车逻辑
     *
     * @return 处理的车辆数量
     */
    private int executeVehicleCollection() {
        // 1. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需执行收车");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆需要收车", vehicles.size());
        // 2. 向每辆车发送收车指令
        int processedCount = 0;
        for (XpVehicle vehicle : vehicles) {
            try {
                sendCollectionCommand(vehicle);
                processedCount++;
            } catch (Exception e) {
                log.error("向车辆[{}]发送收车指令失败", vehicle.getPlateNumber(), e);
            }
        }
        return processedCount;
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
    /**
     * 向车辆发送收车指令
     * <p>
     * TODO: 具体发送收车指令的实现
     * 可能需要:
     * 1. 调用车辆控制接口发送收车指令
     * 2. 更新车辆状态为"已收车"
     * </p>
     *
     * @param vehicle 车辆信息
     */
    private void sendCollectionCommand(XpVehicle vehicle) {
        // TODO: 具体发送收车指令的实现
        log.info("向车辆[{}]发送收车指令", vehicle.getPlateNumber());
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleInspectionScheduleTask.java
New file
@@ -0,0 +1,355 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 车辆日常检测定时任务
 * <p>
 * 根据任务规则配置,在指定时间自动创建车辆日常检测任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class VehicleInspectionScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(VehicleInspectionScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "车辆日常检测定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    /**
     * 每分钟检查一次是否需要执行任务下发
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断日常检测功能是否启用
     * 3. 判断当前时间是否为配置的任务下发时间
     * 4. 查询所有运营中的车辆
     * 5. 根据检测频次判断是否需要创建任务
     * 6. 创建任务并分配给相关角色的运维人员
     * </p>
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executeInspectionTask() {
        log.info("========== 开始执行车辆日常检测定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断日常检测功能是否启用
        if (!Boolean.TRUE.equals(taskRule.getVehicleInspection())) {
            log.info("车辆日常检测功能未启用,跳过执行");
            return;
        }
        // 3. 判断当前时间是否为配置的任务下发时间
        if (!isExecuteTime(taskRule.getVehicleInspectionTime())) {
            log.debug("当前时间不是任务下发时间,跳过执行");
            return;
        }
        log.info("开始执行车辆日常检测任务下发,检测频次: 每{}天/次, 紧急程度: P{}",
                taskRule.getVehicleInspectionFrequency(),
                taskRule.getVehicleInspectionLevel());
        try {
            // 执行任务创建
            int createdCount = createInspectionTasks(taskRule);
            log.info("========== 车辆日常检测定时任务执行完成,创建任务数: {} ==========", createdCount);
        } catch (Exception e) {
            log.error("车辆日常检测定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 判断当前时间是否为任务下发时间
     * <p>
     * 精确到分钟级别的时间匹配
     * </p>
     *
     * @param executeTime 配置的执行时间
     * @return 是否应该执行
     */
    private boolean isExecuteTime(LocalTime executeTime) {
        if (executeTime == null) {
            log.warn("任务下发时间未配置");
            return false;
        }
        LocalTime now = LocalTime.now();
        // 精确到分钟匹配
        return executeTime.getHour() == now.getHour() && executeTime.getMinute() == now.getMinute();
    }
    /**
     * 创建车辆日常检测任务
     *
     * @param taskRule 任务规则配置
     * @return 创建的任务数量
     */
    private int createInspectionTasks(XpTaskRule taskRule) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_INSPECTION);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到车辆日常检测任务接收角色的运维人员");
            return 0;
        }
        log.info("找到 {} 个运维人员可接收日常检测任务", operationUserIds.size());
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建检测任务");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
        // 3. 过滤出今日需要检测的车辆
        List<XpVehicle> needInspectionVehicles = filterVehiclesNeedInspection(vehicles, taskRule.getVehicleInspectionFrequency());
        if (CollectionUtils.isEmpty(needInspectionVehicles)) {
            log.info("今日没有需要检测的车辆");
            return 0;
        }
        log.info("过滤后有 {} 辆车辆需要今日检测", needInspectionVehicles.size());
        // 4. 创建任务并分配给运维人员
        return createTasksAndAssign(needInspectionVehicles, operationUserIds, taskRule);
    }
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
        return new ArrayList<>(userIds);
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
    /**
     * 过滤出需要检测的车辆
     * <p>
     * 根据检测频次判断:vehicleInspectionFrequency 表示每X天检测一次
     * 查询车辆上次检测时间,如果距今天数 >= 频次配置,则需要检测
     * </p>
     *
     * @param vehicles            所有车辆
     * @param inspectionFrequency 检测频次(天)
     * @return 需要检测的车辆列表
     */
    private List<XpVehicle> filterVehiclesNeedInspection(List<XpVehicle> vehicles, Integer inspectionFrequency) {
        // 默认每天检测一次
        if (inspectionFrequency == null || inspectionFrequency <= 0) {
            inspectionFrequency = 1;
        }
        LocalDate today = LocalDate.now();
        List<XpVehicle> needInspectionVehicles = new ArrayList<>();
        for (XpVehicle vehicle : vehicles) {
            // 查询该车辆最近一次检测任务
            XpTask lastTask = taskMapper.selectOne(
                    new LambdaQueryWrapper<XpTask>()
                            .eq(XpTask::getVehicleId, vehicle.getId())
                            .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ROUTINE)
                            .eq(XpTask::getTaskTypeId, TaskScheduleConstants.TASK_TYPE_DETECTION)
                            .orderByDesc(XpTask::getCreateTime)
                            .last("limit 1")
            );
            // 如果从未检测过,需要检测
            if (lastTask == null || lastTask.getCreateTime() == null) {
                needInspectionVehicles.add(vehicle);
                continue;
            }
            // 计算距离上次检测的天数
            LocalDate lastInspectionDate = lastTask.getCreateTime().toLocalDate();
            long daysSinceLastInspection = java.time.temporal.ChronoUnit.DAYS.between(lastInspectionDate, today);
            // 如果距离上次检测天数 >= 频次配置,则需要检测
            if (daysSinceLastInspection >= inspectionFrequency) {
                needInspectionVehicles.add(vehicle);
            }
        }
        return needInspectionVehicles;
    }
    /**
     * 创建任务并分配给运维人员
     *
     * @param vehicles        需要检测的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule        任务规则
     * @return 创建的任务数量
     */
    private int createTasksAndAssign(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(TaskScheduleConstants.TASK_TYPE_DETECTION);
            task.setTaskUrgencyLevel(taskRule.getVehicleInspectionLevel() != null
                    ? taskRule.getVehicleInspectionLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的车辆日常检测任务");
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
            // 设置任务时限
            if (taskRule.getProcessingTimeLimit() != null) {
                task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
            }
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setBelongUserIds(belongUserIds);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个车辆日常检测任务", tasksToSave.size());
        }
        return tasksToSave.size();
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/schedule/VehicleMaintenanceScheduleTask.java
New file
@@ -0,0 +1,381 @@
package com.xiaopeng.maintenance.schedule;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpVehicleClient;
import com.xiaopeng.api.model.account.XpOperationsRole;
import com.xiaopeng.api.model.maintenance.XpTask;
import com.xiaopeng.api.model.maintenance.XpTaskRule;
import com.xiaopeng.api.model.maintenance.XpTaskRuleRole;
import com.xiaopeng.api.model.vehicleConvoy.XpVehicle;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.maintenance.constant.TaskScheduleConstants;
import com.xiaopeng.maintenance.mapper.XpTaskMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleMapper;
import com.xiaopeng.maintenance.mapper.XpTaskRuleRoleMapper;
import com.xiaopeng.maintenance.utils.UUIDUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 车辆保养周期定时任务
 * <p>
 * 根据任务规则配置,在指定时间自动创建车辆保养任务,
 * 并下发给关联角色的运维人员
 * </p>
 *
 * @author mitao
 * @since 2026-02-27
 */
@Component
public class VehicleMaintenanceScheduleTask {
    private static final Logger log = LoggerFactory.getLogger(VehicleMaintenanceScheduleTask.class);
    /**
     * 任务名称
     */
    private static final String TASK_NAME = "车辆保养周期定时任务";
    @Autowired
    private XpTaskRuleMapper taskRuleMapper;
    @Autowired
    private XpTaskRuleRoleMapper taskRuleRoleMapper;
    @Autowired
    private XpTaskMapper taskMapper;
    @Autowired
    private XpVehicleClient xpVehicleClient;
    @Autowired
    private XpOperationUserClient xpOperationUserClient;
    /**
     * 每分钟检查一次是否需要执行任务下发
     * <p>
     * 检查逻辑:
     * 1. 获取任务规则配置
     * 2. 判断保养周期功能是否启用
     * 3. 判断当前时间是否为配置的任务下发时间
     * 4. 查询所有运营中的车辆
     * 5. 根据保养周期判断是否需要创建任务
     * 6. 创建任务并分配给相关角色的运维人员
     * </p>
     */
    @Scheduled(cron = "0 * * * * ?")
    public void executeMaintenanceTask() {
        log.info("========== 开始执行车辆保养周期定时任务检查 ==========");
        // 1. 获取任务规则配置
        XpTaskRule taskRule = getTaskRule();
        if (taskRule == null) {
            log.info("未找到任务规则配置,跳过执行");
            return;
        }
        // 2. 判断保养周期功能是否启用
        if (!Boolean.TRUE.equals(taskRule.getMaintenanceCycle())) {
            log.info("车辆保养周期功能未启用,跳过执行");
            return;
        }
        // 3. 判断当前时间是否为配置的任务下发时间
        if (!isExecuteTime(taskRule.getMaintenanceCycleTaskTime())) {
            log.debug("当前时间不是任务下发时间,跳过执行");
            return;
        }
        // 计算保养周期(天数)
        int cycleDays = calculateCycleDays(taskRule.getMaintenanceCycleTime(), taskRule.getMaintenanceCycleTimeType());
        log.info("开始执行车辆保养周期任务下发,保养周期: {}天, 紧急程度: P{}",
                cycleDays,
                taskRule.getMaintenanceCycleLevel());
        try {
            // 执行任务创建
            int createdCount = createMaintenanceTasks(taskRule, cycleDays);
            log.info("========== 车辆保养周期定时任务执行完成,创建任务数: {} ==========", createdCount);
        } catch (Exception e) {
            log.error("车辆保养周期定时任务执行失败", e);
        }
    }
    /**
     * 获取任务规则配置
     *
     * @return 任务规则
     */
    private XpTaskRule getTaskRule() {
        return taskRuleMapper.selectOne(new LambdaQueryWrapper<XpTaskRule>().last("limit 1"));
    }
    /**
     * 判断当前时间是否为任务下发时间
     * <p>
     * 精确到分钟级别的时间匹配
     * </p>
     *
     * @param executeTime 配置的执行时间
     * @return 是否应该执行
     */
    private boolean isExecuteTime(LocalTime executeTime) {
        if (executeTime == null) {
            log.warn("任务下发时间未配置");
            return false;
        }
        LocalTime now = LocalTime.now();
        // 精确到分钟匹配
        return executeTime.getHour() == now.getHour() && executeTime.getMinute() == now.getMinute();
    }
    /**
     * 计算保养周期天数
     * <p>
     * 根据周期类型转换为天数:
     * - 类型1:天,直接返回周期数值
     * - 类型2:月,按30天/月计算
     * </p>
     *
     * @param cycleTime     周期数值
     * @param cycleTimeType 周期类型:1=天, 2=月
     * @return 周期天数
     */
    private int calculateCycleDays(Integer cycleTime, Integer cycleTimeType) {
        // 默认周期为1
        if (cycleTime == null || cycleTime <= 0) {
            cycleTime = 1;
        }
        // 默认为天
        if (cycleTimeType == null || cycleTimeType == 1) {
            return cycleTime;
        } else {
            // 月(按30天计算)
            return cycleTime * 30;
        }
    }
    /**
     * 创建车辆保养任务
     *
     * @param taskRule  任务规则配置
     * @param cycleDays 保养周期(天数)
     * @return 创建的任务数量
     */
    private int createMaintenanceTasks(XpTaskRule taskRule, int cycleDays) {
        // 1. 获取任务接收角色的运维人员
        List<Long> operationUserIds = getOperationUserIdsByRoleType(TaskScheduleConstants.RULE_ROLE_TYPE_MAINTENANCE);
        if (CollectionUtils.isEmpty(operationUserIds)) {
            log.warn("未找到车辆保养任务接收角色的运维人员");
            return 0;
        }
        log.info("找到 {} 个运维人员可接收保养任务", operationUserIds.size());
        // 2. 获取所有运营中的车辆
        List<XpVehicle> vehicles = getOperatingVehicles();
        if (CollectionUtils.isEmpty(vehicles)) {
            log.info("没有运营中的车辆,无需创建保养任务");
            return 0;
        }
        log.info("找到 {} 辆运营中的车辆", vehicles.size());
        // 3. 过滤出今日需要保养的车辆
        List<XpVehicle> needMaintenanceVehicles = filterVehiclesNeedMaintenance(vehicles, cycleDays);
        if (CollectionUtils.isEmpty(needMaintenanceVehicles)) {
            log.info("今日没有需要保养的车辆");
            return 0;
        }
        log.info("过滤后有 {} 辆车辆需要今日保养", needMaintenanceVehicles.size());
        // 4. 创建任务并分配给运维人员
        return createTasksAndAssign(needMaintenanceVehicles, operationUserIds, taskRule);
    }
    /**
     * 根据角色类型获取运维人员ID列表
     *
     * @param roleType 角色类型
     * @return 运维人员ID列表
     */
    private List<Long> getOperationUserIdsByRoleType(Integer roleType) {
        // 1. 获取该类型对应的角色ID
        List<XpTaskRuleRole> ruleRoles = taskRuleRoleMapper.selectList(
                new LambdaQueryWrapper<XpTaskRuleRole>()
                        .eq(XpTaskRuleRole::getRuleType, roleType)
        );
        if (CollectionUtils.isEmpty(ruleRoles)) {
            return Collections.emptyList();
        }
        // 2. 获取角色对应的运维人员
        Set<Long> userIds = new HashSet<>();
        for (XpTaskRuleRole ruleRole : ruleRoles) {
            try {
                R<List<XpOperationsRole>> result = xpOperationUserClient.getXpOperationsRoleByRoleId(ruleRole.getRoleId());
                if (result != null && result.getData() != null) {
                    userIds.addAll(
                            result.getData().stream()
                                    .map(XpOperationsRole::getOperationsUserId)
                                    .collect(Collectors.toList())
                    );
                }
            } catch (Exception e) {
                log.error("获取角色[{}]对应的运维人员失败", ruleRole.getRoleId(), e);
            }
        }
        return new ArrayList<>(userIds);
    }
    /**
     * 获取所有运营中的车辆
     *
     * @return 车辆列表
     */
    private List<XpVehicle> getOperatingVehicles() {
        try {
            R<List<XpVehicle>> result = xpVehicleClient.listVehicle();
            if (result != null && result.getData() != null) {
                // 过滤运营中的车辆 (vehicleType=2 表示运营, operatingStatus=1 表示运营中)
                return result.getData().stream()
                        .filter(v -> v.getVehicleType() != null && v.getVehicleType() == 2)
                        .filter(v -> v.getOperatingStatus() != null && v.getOperatingStatus() == 1)
                        .collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取车辆列表失败", e);
        }
        return Collections.emptyList();
    }
    /**
     * 过滤出需要保养的车辆
     * <p>
     * 根据保养周期判断:查询车辆上次保养任务时间,
     * 如果距今天数 >= 配置周期,则需要保养
     * </p>
     *
     * @param vehicles  所有车辆
     * @param cycleDays 保养周期(天数)
     * @return 需要保养的车辆列表
     */
    private List<XpVehicle> filterVehiclesNeedMaintenance(List<XpVehicle> vehicles, int cycleDays) {
        LocalDate today = LocalDate.now();
        List<XpVehicle> needMaintenanceVehicles = new ArrayList<>();
        for (XpVehicle vehicle : vehicles) {
            // 查询该车辆最近一次保养任务
            XpTask lastTask = taskMapper.selectOne(
                    new LambdaQueryWrapper<XpTask>()
                            .eq(XpTask::getVehicleId, vehicle.getId())
                            .eq(XpTask::getTaskCategory, TaskScheduleConstants.TASK_CATEGORY_ROUTINE)
                            .eq(XpTask::getTaskTypeId, TaskScheduleConstants.TASK_TYPE_MAINTENANCE)
                            .orderByDesc(XpTask::getCreateTime)
                            .last("limit 1")
            );
            // 如果从未保养过,需要保养
            if (lastTask == null || lastTask.getCreateTime() == null) {
                needMaintenanceVehicles.add(vehicle);
                continue;
            }
            // 计算距离上次保养的天数
            LocalDate lastMaintenanceDate = lastTask.getCreateTime().toLocalDate();
            long daysSinceLastMaintenance = java.time.temporal.ChronoUnit.DAYS.between(lastMaintenanceDate, today);
            // 如果距离上次保养天数 >= 配置周期,则需要保养
            if (daysSinceLastMaintenance >= cycleDays) {
                needMaintenanceVehicles.add(vehicle);
            }
        }
        return needMaintenanceVehicles;
    }
    /**
     * 创建任务并分配给运维人员
     *
     * @param vehicles         需要保养的车辆列表
     * @param operationUserIds 运维人员ID列表
     * @param taskRule         任务规则
     * @return 创建的任务数量
     */
    private int createTasksAndAssign(List<XpVehicle> vehicles, List<Long> operationUserIds, XpTaskRule taskRule) {
        List<XpTask> tasksToSave = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        // 将所有配置角色的运维人员ID拼接,任务分配给该角色下的所有运维人员
        String belongUserIds = operationUserIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        for (XpVehicle vehicle : vehicles) {
            XpTask task = new XpTask();
            // 生成任务编号
            String taskCode = sdf.format(new Date()) + UUIDUtil.getNumberRandom(5);
            task.setTaskCode(taskCode);
            // 设置任务基本信息
            task.setTaskCategory(TaskScheduleConstants.TASK_CATEGORY_ROUTINE);
            task.setTaskTypeId(TaskScheduleConstants.TASK_TYPE_MAINTENANCE);
            task.setTaskUrgencyLevel(taskRule.getMaintenanceCycleLevel() != null
                    ? taskRule.getMaintenanceCycleLevel()
                    : TaskScheduleConstants.URGENCY_LEVEL_P2);
            task.setTaskDescription("系统自动下发的车辆保养任务");
            // 设置车辆信息
            task.setVehicleId(vehicle.getId());
            // 设置任务时限
            if (taskRule.getProcessingTimeLimit() != null) {
                task.setEndTime(LocalDateTime.now().plusMinutes(taskRule.getProcessingTimeLimit()));
            }
            // 设置任务状态为待到达
            task.setStatus(TaskScheduleConstants.TASK_STATUS_PENDING_ARRIVE);
            // 设置任务接收人(配置角色下的所有运维人员都可以看到)
            task.setBelongUserIds(belongUserIds);
            // 设置创建信息
            task.setReportTime(LocalDateTime.now());
            task.setCreateBy(TaskScheduleConstants.SCHEDULE_CREATOR);
            tasksToSave.add(task);
        }
        // 批量保存任务
        if (!CollectionUtils.isEmpty(tasksToSave)) {
            for (XpTask task : tasksToSave) {
                taskMapper.insert(task);
            }
            log.info("成功创建 {} 个车辆保养任务", tasksToSave.size());
        }
        return tasksToSave.size();
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/IXpSchedulingDetailService.java
@@ -2,12 +2,17 @@
import com.xiaopeng.api.model.maintenance.XpSchedulingDetail;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.maintenance.dto.SchedulingDetailDto;
import com.xiaopeng.maintenance.dto.ShiftAttendanceDTO;
import com.xiaopeng.maintenance.vo.SaveSchedulingVo;
import com.xiaopeng.maintenance.vo.SchedulingDetailVo;
import com.xiaopeng.maintenance.vo.ShiftAttendanceVO;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletResponse;
/**
 * <p>
@@ -33,4 +38,20 @@
     * @return
     */
    PageInfo<SchedulingDetailDto> pageList(SchedulingDetailVo query);
    /**
     * 保存排班明细
     * @param vo
     * @return
     */
    R saveSchedulingDetail(SaveSchedulingVo vo, Long userId);
    /**
     * 导出排班计划
     * @param month
     * @param response
     */
    void exportExcel(String month, HttpServletResponse response);
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpSchedulingDetailServiceImpl.java
@@ -1,15 +1,19 @@
package com.xiaopeng.maintenance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
import com.xiaopeng.api.feignClient.maintenance.XpCheckLocationClient;
import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetClient;
import com.xiaopeng.api.model.maintenance.XpCheckLocation;
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.feignClient.vehicleConvoy.XpFleetOperationsClient;
import com.xiaopeng.api.model.account.XpOperationsUser;import com.xiaopeng.api.model.maintenance.*;
import com.xiaopeng.api.model.vehicleConvoy.XpFleet;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.utils.poi.ExcelUtil;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.maintenance.dto.ExportSchedulingDetailDto;
import com.xiaopeng.maintenance.dto.SchedulingDetailDto;
import com.xiaopeng.maintenance.dto.ShiftAttendanceDTO;
import com.xiaopeng.maintenance.mapper.XpCheckLocationMapper;
@@ -17,12 +21,21 @@
import com.xiaopeng.maintenance.mapper.XpSchedulingDetailMapper;
import com.xiaopeng.maintenance.mapper.XpShiftMapper;
import com.xiaopeng.maintenance.service.IXpSchedulingDetailService;
import com.xiaopeng.maintenance.vo.SchedulingDetailVo;
import com.xiaopeng.maintenance.vo.ShiftAttendanceVO;
import com.xiaopeng.maintenance.service.IXpSchedulingService;
import com.xiaopeng.maintenance.service.IXpShiftService;
import com.xiaopeng.maintenance.vo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Objects;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
/**
 * <p>
@@ -35,14 +48,22 @@
@Service
public class XpSchedulingDetailServiceImpl extends ServiceImpl<XpSchedulingDetailMapper, XpSchedulingDetail> implements IXpSchedulingDetailService {
    @Autowired
    @Resource
    private XpFleetClient xpFleetClient;
    @Autowired
    @Resource
    private XpCheckLocationMapper xpCheckLocationMapper;
    @Autowired
    @Resource
    private XpShiftMapper xpShiftMapper;
    @Autowired
    @Resource
    private XpClockInMapper xpClockInMapper;
    @Resource
    private IXpSchedulingService xpSchedulingService;
    @Resource
    private IXpShiftService xpShiftService;
    @Resource
    private XpFleetOperationsClient xpFleetOperationsClient;
    @Resource
    private XpOperationUserClient xpOperationUserClient;
    @Override
    public ShiftAttendanceVO shiftAttendance(ShiftAttendanceDTO dto) {
@@ -78,10 +99,10 @@
                .eq(XpClockIn::getAppUserId, dto.getOperationsUserId())
                .likeRight(XpClockIn::getClockInTime, dto.getSchedulingDate())
                .last("LIMIT 1"));
        if(Objects.isNull(xpClockIn)){
        if (Objects.isNull(xpClockIn)) {
            shiftAttendanceVO.setClockIn(false);
            shiftAttendanceVO.setClockOut(false);
        }else {
        } else {
            shiftAttendanceVO.setXpClockIn(xpClockIn);
            shiftAttendanceVO.setClockIn(true);
            shiftAttendanceVO.setClockOut(xpClockIn.getClockOutTime() != null);
@@ -92,15 +113,438 @@
    /**
     * 排班明细分页列表
     *
     * @param query
     * @return
     */
    @Override
    public PageInfo<SchedulingDetailDto> pageList(SchedulingDetailVo query) {
        PageInfo<SchedulingDetailDto> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
        List<SchedulingDetailDto> schedulingDetailDtos = this.baseMapper.pageList(pageInfo, query.getMonth());
        LocalDate parse1 = LocalDate.parse(query.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM"));
        //找出月份的最大天
        int dayOfMonth = parse1.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
        for (SchedulingDetailDto schedulingDetailDto : schedulingDetailDtos) {
            String days = schedulingDetailDto.getDays();
            if (StringUtils.isNotBlank(days)) {
                String[] split = days.split(",");
                List<String> list = Arrays.asList(split);
                for (int i = 1; i <= dayOfMonth; i++) {
                    Map<String, String> map1 = new HashMap<>();
                    for (String s : split) {
                        String[] split1 = s.split("_");
                        if (i == Integer.parseInt(split1[0])) {
                            map1.put("shiftId", split1[1]);
                            map1.put("shiftName", split1[2]);
                            map1.put("shiftColor", split1[3]);
                            break;
                        }
                    }
                    setEnglishDay(i, schedulingDetailDto, map1);
                }
                long allDay = list.stream().filter(s -> s.contains("全天")).count();
                long morning = list.stream().filter(s -> s.contains("早")).count();
                long midDay = list.stream().filter(s -> s.contains("中")).count();
                long evening = list.stream().filter(s -> s.contains("晚")).count();
                schedulingDetailDto.setAllDay(allDay);
                schedulingDetailDto.setMorning(morning);
                schedulingDetailDto.setMidDay(midDay);
                schedulingDetailDto.setEvening(evening);
            }
        }
        pageInfo.setRecords(schedulingDetailDtos);
        return pageInfo;
    }
    /**
     * 给每天的字段赋值
     * @param day
     * @param dto
     * @param map
     */
    private void setEnglishDay(int day, SchedulingDetailDto dto, Map<String, String> map) {
        switch (day) {
            case 1:
                dto.setOne(map);
                break;
            case 2:
                dto.setTwo(map);
                break;
            case 3:
                dto.setThree(map);
                break;
            case 4:
                dto.setFour(map);
                break;
            case 5:
                dto.setFive(map);
                break;
            case 6:
                dto.setSix(map);
                break;
            case 7:
                dto.setSeven(map);
                break;
            case 8:
                dto.setEight(map);
                break;
            case 9:
                dto.setNine(map);
                break;
            case 10:
                dto.setTen(map);
                break;
            case 11:
                dto.setEleven(map);
                break;
            case 12:
                dto.setTwelve(map);
                break;
            case 13:
                dto.setThirteen(map);
                break;
            case 14:
                dto.setFourteen(map);
                break;
            case 15:
                dto.setFifteen(map);
                break;
            case 16:
                dto.setSixteen(map);
                break;
            case 17:
                dto.setSeventeen(map);
                break;
            case 18:
                dto.setEighteen(map);
                break;
            case 19:
                dto.setNineteen(map);
                break;
            case 20:
                dto.setTwenty(map);
                break;
            case 21:
                dto.setTwentyOne(map);
                break;
            case 22:
                dto.setTwentyTwo(map);
                break;
            case 23:
                dto.setTwentyThree(map);
                break;
            case 24:
                dto.setTwentyFour(map);
                break;
            case 25:
                dto.setTwentyFive(map);
                break;
            case 26:
                dto.setTwentySix(map);
                break;
            case 27:
                dto.setTwentySeven(map);
                break;
            case 28:
                dto.setTwentyEight(map);
                break;
            case 29:
                dto.setTwentyNine(map);
                break;
            case 30:
                dto.setThirty(map);
                break;
            case 31:
                dto.setThirtyOne(map);
                break;
        }
    }
        return null;
    /**
     * 给每天的字段赋值
     * @param day
     * @param dto
     * @param shiftName
     */
    private void setEnglishDay(int day, ExportSchedulingDetailDto dto, String shiftName) {
        switch (day) {
            case 1:
                dto.setOne(shiftName);
                break;
            case 2:
                dto.setTwo(shiftName);
                break;
            case 3:
                dto.setThree(shiftName);
                break;
            case 4:
                dto.setFour(shiftName);
                break;
            case 5:
                dto.setFive(shiftName);
                break;
            case 6:
                dto.setSix(shiftName);
                break;
            case 7:
                dto.setSeven(shiftName);
                break;
            case 8:
                dto.setEight(shiftName);
                break;
            case 9:
                dto.setNine(shiftName);
                break;
            case 10:
                dto.setTen(shiftName);
                break;
            case 11:
                dto.setEleven(shiftName);
                break;
            case 12:
                dto.setTwelve(shiftName);
                break;
            case 13:
                dto.setThirteen(shiftName);
                break;
            case 14:
                dto.setFourteen(shiftName);
                break;
            case 15:
                dto.setFifteen(shiftName);
                break;
            case 16:
                dto.setSixteen(shiftName);
                break;
            case 17:
                dto.setSeventeen(shiftName);
                break;
            case 18:
                dto.setEighteen(shiftName);
                break;
            case 19:
                dto.setNineteen(shiftName);
                break;
            case 20:
                dto.setTwenty(shiftName);
                break;
            case 21:
                dto.setTwentyOne(shiftName);
                break;
            case 22:
                dto.setTwentyTwo(shiftName);
                break;
            case 23:
                dto.setTwentyThree(shiftName);
                break;
            case 24:
                dto.setTwentyFour(shiftName);
                break;
            case 25:
                dto.setTwentyFive(shiftName);
                break;
            case 26:
                dto.setTwentySix(shiftName);
                break;
            case 27:
                dto.setTwentySeven(shiftName);
                break;
            case 28:
                dto.setTwentyEight(shiftName);
                break;
            case 29:
                dto.setTwentyNine(shiftName);
                break;
            case 30:
                dto.setThirty(shiftName);
                break;
            case 31:
                dto.setThirtyOne(shiftName);
                break;
        }
    }
    /**
     * 保存排班明细
     *
     * @param vo
     * @return
     */
    @Override
    public R saveSchedulingDetail(SaveSchedulingVo vo, Long userId) {
        String month = vo.getMonth();
        List<XpSchedulingDetail> list = this.list(new QueryWrapper<XpSchedulingDetail>().eq("is_deleted", 0).last(" and DATE_FORMAT(planTime,'%Y-%m') = '" + month + "'"));
        //保存排班数据
        XpScheduling xpScheduling = xpSchedulingService.getOne(new QueryWrapper<XpScheduling>().eq("is_deleted", 0).last(" and DATE_FORMAT(schedulingDate,'%Y-%m') = '" + month + "'"));
        if(null == xpScheduling){
            xpScheduling = new XpScheduling();
            xpScheduling.setDeleted(false);
            xpScheduling.setCreateTime(LocalDateTime.now());
            xpScheduling.setUpdateTime(LocalDateTime.now());
            xpScheduling.setCreateBy(userId.toString());
            xpScheduling.setUpdateBy(userId.toString());
        }
        xpScheduling.setSchedulingDate(LocalDate.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")));
        xpScheduling.setCommitUserId(userId);
        xpSchedulingService.saveOrUpdate(xpScheduling);
        List<XpShift> xpShifts = xpShiftService.list(new QueryWrapper<XpShift>().eq("is_deleted", 0));
        //构建排班明细数据
        List<SaveSchedulingDetailVo> datas = vo.getDatas();
        List<XpSchedulingDetail> schedulingDetails = new ArrayList<>();
        for (SaveSchedulingDetailVo data : datas) {
            Long fleetId = data.getFleetId();
            List<SaveSchedulingDetailDay> days = data.getDays();
            for (SaveSchedulingDetailDay day : days) {
                Long shiftId = day.getShiftId();
                LocalDate parse = LocalDate.parse(day.getDayTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                //查询判断数据是否存在
                XpSchedulingDetail schedulingDetail = list.stream().filter(s -> s.getFleetId().equals(fleetId)
                        && s.getShiftId().equals(shiftId)
                        && s.getPlanTime().equals(parse)
                ).findAny().orElse(null);
                //构建存储对象
                if(null == schedulingDetail){
                    schedulingDetail = new XpSchedulingDetail();
                    schedulingDetail.setCreateBy(userId.toString());
                    schedulingDetail.setCreateTime(LocalDateTime.now());
                    schedulingDetail.setUpdateBy(userId.toString());
                    schedulingDetail.setUpdateTime(LocalDateTime.now());
                    schedulingDetail.setDeleted(false);
                }else{
                    //构建通知消息
                    Long old_shiftId = schedulingDetail.getShiftId();
                    if(!old_shiftId.equals(shiftId)){
                        //获取车队的所有人员
                        XpShift xpShift1 = xpShifts.stream().filter(s -> s.getId().equals(old_shiftId)).findAny().orElse(null);
                        XpShift xpShift2 = xpShifts.stream().filter(s -> s.getId().equals(shiftId)).findAny().orElse(null);
                        List<Long> operationsIds = xpFleetOperationsClient.getOperationsIdsByFleetId(fleetId).getData();
                        if(null != operationsIds && !operationsIds.isEmpty()){
                            List<XpOperationsUser> data1 = xpOperationUserClient.listByIds(operationsIds).getData();
                            for (XpOperationsUser xpOperationsUser : data1) {
                                String nickName = xpOperationsUser.getNickName();
                                String message = nickName + "请注意," + month + "你的排班由" + xpShift1.getShiftName() + "变更为了" + xpShift2.getShiftName() + "。详情请联系平台管理员。";
                            }
                        }
                    }
                }
                schedulingDetail.setSchedulingId(xpScheduling.getId());
                schedulingDetail.setFleetId(fleetId);
                schedulingDetail.setShiftId(shiftId);
                XpShift xpShift = xpShifts.stream().filter(s -> s.getId().equals(shiftId)).findAny().orElse(null);
                schedulingDetail.setIsAllDay(xpShift.getShiftName().contains("全天"));
                schedulingDetail.setPlanTime(parse);
                schedulingDetails.add(schedulingDetail);
            }
        }
        //批量保存排班明细数据
        if(schedulingDetails.isEmpty()){
            this.saveOrUpdateBatch(schedulingDetails);
        }
        return R.ok();
    }
    /**
     * 导出排班数据
     * @param month
     * @param response
     */
    @Override
    public void exportExcel(String month, HttpServletResponse response) {
        PageInfo<SchedulingDetailDto> pageInfo = new PageInfo<>(0, 10000);
        List<SchedulingDetailDto> schedulingDetailDtos = this.baseMapper.pageList(pageInfo, month);
        List<ExportSchedulingDetailDto> exportSchedulingDetailDtos = new ArrayList<>();
        //获取月份的最大天数
        LocalDate parse1 = LocalDate.parse(month, DateTimeFormatter.ofPattern("yyyy-MM"));
        int dayOfMonth = parse1.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
        //构建表头第二行数据
        ExportSchedulingDetailDto exportSchedulingDetailDto = new ExportSchedulingDetailDto();
        exportSchedulingDetailDto.setFleetName("");
        for (int i = 1; i <= dayOfMonth; i++) {
            LocalDate parse = LocalDate.parse(month + "-" + i, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            String week = "";
            switch (parse.getDayOfWeek()){
                case MONDAY:
                    week = "一";
                    break;
                case TUESDAY:
                    week = "二";
                    break;
                case WEDNESDAY:
                    week = "三";
                    break;
                case THURSDAY:
                    week = "四";
                    break;
                case FRIDAY:
                    week = "五";
                    break;
                case SATURDAY:
                    week = "六";
                    break;
                case SUNDAY:
                    week = "日";
                    break;
            }
            setEnglishDay(i, exportSchedulingDetailDto, week);
        }
        exportSchedulingDetailDto.setAllDay("全天");
        exportSchedulingDetailDto.setMorning("早");
        exportSchedulingDetailDto.setMidDay("中");
        exportSchedulingDetailDto.setEvening("晚");
        exportSchedulingDetailDtos.add(exportSchedulingDetailDto);
        //构建数据部分的内容
        for (SchedulingDetailDto schedulingDetailDto : schedulingDetailDtos) {
            exportSchedulingDetailDto = new ExportSchedulingDetailDto();
            exportSchedulingDetailDto.setFleetName(schedulingDetailDto.getFleetName());
            String days = schedulingDetailDto.getDays();
            if (StringUtils.isNotBlank(days)) {
                String[] split = days.split(",");
                List<String> list = Arrays.asList(split);
                for (int i = 1; i <= dayOfMonth; i++) {
                    String shiftName = "休";
                    for (String s : split) {
                        String[] split1 = s.split("_");
                        if (i == Integer.parseInt(split1[0])) {
                            shiftName = split1[2];
                            break;
                        }
                    }
                    setEnglishDay(i, exportSchedulingDetailDto, shiftName);
                }
                long allDay = list.stream().filter(s -> s.contains("全天")).count();
                long morning = list.stream().filter(s -> s.contains("早")).count();
                long midDay = list.stream().filter(s -> s.contains("中")).count();
                long evening = list.stream().filter(s -> s.contains("晚")).count();
                exportSchedulingDetailDto.setAllDay(allDay + "");
                exportSchedulingDetailDto.setMorning(morning + "");
                exportSchedulingDetailDto.setMidDay(midDay + "");
                exportSchedulingDetailDto.setEvening(evening + "");
            }else{
                for (int i = 1; i <= 31; i++) {
                    String shiftName = "休";
                    setEnglishDay(i, exportSchedulingDetailDto, shiftName);
                }
                exportSchedulingDetailDto.setAllDay("0");
                exportSchedulingDetailDto.setMorning("0");
                exportSchedulingDetailDto.setMidDay("0");
                exportSchedulingDetailDto.setEvening("0");
            }
            exportSchedulingDetailDtos.add(exportSchedulingDetailDto);
        }
        String[] split = month.split("-");
        ExcelUtil<ExportSchedulingDetailDto> excelUtil = new ExcelUtil<>(ExportSchedulingDetailDto.class);
        excelUtil.exportExcel(response, exportSchedulingDetailDtos, "排班计划", "日期:" + split[0] + "年" + split[1] + "月");
    }
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -209,7 +209,7 @@
                Map<Integer, List<XpOperationsUser>> operationUserMap = data.stream().collect(Collectors.groupingBy(XpOperationsUser::getDeviceType));
                for (Map.Entry<Integer, List<XpOperationsUser>> integerListEntry : operationUserMap.entrySet()) {
                    List<XpOperationsUser> value = integerListEntry.getValue();
                    pushAliMessage(data, content, xpTask);
                    pushAliMessage(value, content, xpTask);
                }
            }
        }
@@ -227,7 +227,7 @@
        param.put("content", content);
        param.put("strategy", NotificationTypeEnum.ACCIDENT_NEW.getCode());
        param.put("type", TaskTypeNotificationEnum.ACCIDENT_TASK.getCode());
        param.put("id", xpTask.getId());
        param.put("id", xpTask.getId().toString());
        xpAliPushMessageClient.pushMessage(AliPushMessageDTO.builder()
                .body(JSONObject.toJSONString(param))
                .account(account).title("车辆事故警示")
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingDetailDay.java
New file
@@ -0,0 +1,14 @@
package com.xiaopeng.maintenance.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel
public class SaveSchedulingDetailDay {
    @ApiModelProperty("日期yyyy-MM-dd")
    private String dayTime;
    @ApiModelProperty("班次id")
    private Long shiftId;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingDetailVo.java
New file
@@ -0,0 +1,16 @@
package com.xiaopeng.maintenance.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel
public class SaveSchedulingDetailVo {
    @ApiModelProperty("车队id")
    private Long fleetId;
    @ApiModelProperty("每日配置数据")
    private List<SaveSchedulingDetailDay> days;
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/vo/SaveSchedulingVo.java
New file
@@ -0,0 +1,14 @@
package com.xiaopeng.maintenance.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.List;
@Data
@ApiModel
public class SaveSchedulingVo {
    @ApiModelProperty("日期")
    private String month;
    @ApiModelProperty("保存数据")
    private List<SaveSchedulingDetailVo> datas;
}
xp-robotaxi-maintenance-boot/src/main/resources/mapper/maintenance/XpSchedulingDetailMapper.xml
@@ -4,9 +4,20 @@
    <select id="pageList" resultType="com.xiaopeng.maintenance.dto.SchedulingDetailDto">
        select * from xp_scheduling_detail where 1=1
        SELECT
            aa.fleet_id as fleetId
            aa.fleet_name as fleetName,
            bb.days
        FROM
            xp_fleet aa
        LEFT JOIN (
            select
            a.fleet_id, GROUP_CONCAT(concat(DATE_FORMAT(a.plan_time, '%d' ), '_', a.shift_id, '_', b.shift_name, '_', b.shift_color)) as days
            from xp_scheduling_detail a
            left join xp_shift b on (a.shift_id = b.id)
            where a.is_deleted = 0 and DATE_FORMAT(a.plan_time, '%Y-%m') = #{month}
            group by a.fleet_id
        ) AS bb ON ( aa.id = bb.fleet_id)
        WHERE aa.is_deleted = 0
    </select>
</mapper>
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java
@@ -57,8 +57,10 @@
    @ApiOperation(value = "获取所有运营中的区域", response = XpElectronicFenceArea.class)
    @PostMapping("/listElectronicFenceArea")
    public R<List<XpElectronicFenceArea>> listElectronicFenceArea() {
        R<List<XpElectronicFenceArea>> listR = xpPointClient.listElectronicFenceArea();
        return xpPointClient.listElectronicFenceArea();
        return listR;
    }
    @ApiOperation(value = "点位管理分页列表", response = XpPoint.class)
    @PostMapping("/pageList")
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpInvitationCodeController.java
@@ -109,7 +109,7 @@
        HttpServletResponse response = WebUtils.response();
        ServletOutputStream outputStream = null;
        try {
            String fileName = URLEncoder.encode("邀请码导入模板.xls", "utf-8");
            String fileName = URLEncoder.encode("邀请码导入模板.xlsx", "utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setHeader("content-Type", "application/vnd.ms-excel");
            response.setHeader("Pragma", "no-cache");
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpPointController.java
@@ -6,6 +6,7 @@
import com.xiaopeng.api.model.operation.XpPoint;
import com.xiaopeng.api.vo.order.XpHistoryEndPointsVo;
import com.xiaopeng.api.vo.order.XpHistoryStartPointsVo;
import com.xiaopeng.api.vo.order.XpPointInfoVo;
import com.xiaopeng.api.vo.order.XpPointsSearchVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.controller.BaseController;
@@ -21,6 +22,7 @@
import javax.validation.Valid;
import java.util.List;
import java.util.Set;
@Slf4j
@RestController
@@ -79,6 +81,12 @@
    }
    @PostMapping("/inner/point/getPointsInfo")
    public R<List<XpPoint>> getPointsInfo(@RequestBody Set<Long> points){
        List<XpPoint> xpPoints = xpPointService.listByIds(points);
        return R.ok(xpPoints);
    }
    /**
     * 获取下单最大距离 最小距离
     */
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpRegionController.java
@@ -78,7 +78,7 @@
                redisService.deleteObject(CacheConstants.XP_REGION);
            }
        }else {
            list = regionService.list(Wrappers.lambdaQuery(XpRegion.class).ne(XpRegion::getParentId, 0).orderByAsc(XpRegion::getInitial));
            list = regionService.list(Wrappers.lambdaQuery(XpRegion.class).ne(XpRegion::getParentId, 0).isNotNull(XpRegion::getInitial).orderByAsc(XpRegion::getInitial));
            redisService.setCacheList(CacheConstants.XP_REGION_CITY, list);
        }
        List<XpOpenCity> openCities = openCityService.list(new LambdaQueryWrapper<XpOpenCity>().eq(XpOpenCity::getStatus,1));
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpPointServiceImpl.java
@@ -176,6 +176,8 @@
            xpHistoryStartPointsVo.setId(xpPoint.getId());
            double distanced = FastDistanceCalculator.distanceFastKm(historyStartPointsDto.getLat(), historyStartPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon());
            xpHistoryStartPointsVo.setDistance(distanced);
            xpHistoryStartPointsVo.setTime(xpPoint.getParkingTimeLimit());
            xpHistoryStartPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId());
            resultList.add(xpHistoryStartPointsVo);
        }
@@ -196,6 +198,8 @@
            xpHistoryEndPointsVo.setId(xpPoint.getId());
            double distanced = FastDistanceCalculator.distanceFastKm(historyEndPointsDto.getLat(), historyEndPointsDto.getLon(), xpPoint.getPointLat(), xpPoint.getPointLon());
            xpHistoryEndPointsVo.setDistance(distanced);
            xpHistoryEndPointsVo.setTime(xpPoint.getParkingTimeLimit());
            xpHistoryEndPointsVo.setElectronicAreaId(xpPoint.getElectronicAreaId());
            resultList.add(xpHistoryEndPointsVo);
        }
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpRegionServiceImpl.java
@@ -68,7 +68,7 @@
                "G", "H", "I", "J", "K", "L",
                "M", "N", "O", "P", "Q", "R",
                "S", "T", "U", "V", "W", "X", "Y", "Z"};
        List<XpRegion> tRegions = this.baseMapper.selectList(Wrappers.lambdaQuery(XpRegion.class));
        List<XpRegion> tRegions = this.baseMapper.selectList(Wrappers.lambdaQuery(XpRegion.class).isNotNull(XpRegion::getInitial));
        List<List<XpRegion>> result = new ArrayList<>();
        for (String initial : initials) {
            List<XpRegion> init = tRegions.stream().filter(e -> e.getInitial().equals(initial) && !e.getCode().contains("0000")).collect(Collectors.toList());
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/controller/XpOrderController.java
@@ -475,16 +475,9 @@
    public R<RouteVo> queryOrderRoute(@Validated @RequestBody QueryOrderRouteDTO dto, HttpServletRequest request) {
        String uid = request.getHeader("XP-Uid");
        // 查询订单信息
        XpOrder xpOrder = xpOrderService.getById(dto.getOrderId());
        // 封装消息内容
        Integer routeType = dto.getRouteType();
        if(routeType == 2){
            routeType = 3;
        }else {
            routeType = xpOrder.getStatus() == 3 ? 1 : 2;
        }
//        XpOrder xpOrder = xpOrderService.getById(dto.getOrderId());
        // 获取路线规划
        RouteVo routeVo = redisService.getCacheObject(CacheConstants.ORDER_ROUTE_CAPTURE + "_" + dto.getOrderId() + "_" +routeType);
        RouteVo routeVo = redisService.getCacheObject(CacheConstants.ORDER_ROUTE_CAPTURE + "_" + dto.getOrderId() + "_" +dto.getRouteType());
        return R.ok(routeVo);
    }
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/model/vo/XpOrderHistoryListVO.java
@@ -35,5 +35,34 @@
    private LocalDateTime time;
    private Long startPointId;
    private String  startPointName;
    private Double  startLon;
    private Double  startLat;
    private Long xpElectronicFenceAreaId;
    private Integer startParkingTimeLimit;
    private Long endPointId;
    private String  endPointName;
    private Double endLat;
    private Double endLon;
    private Integer endParkingTimeLimit;
}
xp-robotaxi-order-boot/src/main/java/com/xiaopeng/order/service/impl/XpOrderServiceImpl.java
@@ -696,9 +696,19 @@
    public XpOrderHistoryVO getHistoryOrderList(XpHistoryOrderListQuery query, String uid) {
        Page<XpOrderHistoryListVO> page = new Page<>(query.getPageNum(), query.getPageSize());
        List<XpOrderHistoryListVO> list = this.baseMapper.getHistoryOrderList(page, query, Long.parseLong(uid));
        XpOrderHistoryVO xpOrderHistoryVO = new XpOrderHistoryVO();
        if(list.isEmpty()){
            return xpOrderHistoryVO;
        }
        Set<Long> points = list.stream().map(XpOrderHistoryListVO::getStartPointId).collect(Collectors.toSet());
        points.addAll(list.stream().map(XpOrderHistoryListVO::getEndPointId).collect(Collectors.toSet()));
        // 查询 点位信息
        R<List<XpPoint>> pointInfoVos = xpPointClient.getPointsInfo(points);
        List<XpPoint> pointList = pointInfoVos.getData();
        xpOrderHistoryVO.setTotal((int) page.getTotal());
        // 根据time分组
@@ -708,6 +718,28 @@
            xpOrderHistoryDateVO.setDateLabel(entry.getKey());
            List<XpOrderHistoryListVO> value = entry.getValue();
            value = value.stream().sorted(Comparator.comparing(XpOrderHistoryListVO::getTime).reversed()).collect(Collectors.toList());
            for (XpOrderHistoryListVO xpOrderHistoryListVO : value) {
                XpPoint xpPoint = pointList.stream().filter(e -> e.getId().equals(xpOrderHistoryListVO.getStartPointId())).findFirst().orElse(null);
                if(xpPoint!=null){
                    xpOrderHistoryListVO.setStartPointName(xpPoint.getPointName());
                    xpOrderHistoryListVO.setStartLat(xpPoint.getPointLat());
                    xpOrderHistoryListVO.setStartLon(xpPoint.getPointLon());
                    xpOrderHistoryListVO.setXpElectronicFenceAreaId(xpPoint.getElectronicAreaId());
                    xpOrderHistoryListVO.setStartParkingTimeLimit(xpPoint.getParkingTimeLimit());
                }
                xpPoint = pointList.stream().filter(e -> e.getId().equals(xpOrderHistoryListVO.getEndPointId())).findFirst().orElse(null);
                if(xpPoint!=null){
                    xpOrderHistoryListVO.setEndPointName(xpPoint.getPointName());
                    xpOrderHistoryListVO.setEndLat(xpPoint.getPointLat());
                    xpOrderHistoryListVO.setEndLon(xpPoint.getPointLon());
                    xpOrderHistoryListVO.setXpElectronicFenceAreaId(xpPoint.getElectronicAreaId());
                    xpOrderHistoryListVO.setEndParkingTimeLimit(xpPoint.getParkingTimeLimit());
                }
            }
            xpOrderHistoryDateVO.setOrders(value);
            return xpOrderHistoryDateVO;
        }).collect(Collectors.toList());
xp-robotaxi-order-boot/src/main/resources/mapper/order/XpOrderMapper.xml
@@ -45,7 +45,7 @@
        ORDER BY date ASC
    </select>
    <select id="getHistoryOrderList" resultType="com.xiaopeng.order.model.vo.XpOrderHistoryListVO">
        select start_point_name as originAddress, end_point_name as destinationAddress,
        select start_point_name as originAddress, end_point_name as destinationAddress,start_point_id as startPointId,end_point_id as endPointId,
        DATE_FORMAT(create_time, '%H:%i') AS orderTime, pay_money as amount,create_time as time,DATE_FORMAT(create_time,'%Y-%m-%d') AS timeStr,
        id as orderId from xp_order where app_user_id = #{uid} and is_deleted = 0 and status in (9,10)
        <if test="query.status != null">
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/controller/XpFleetOperationsController.java
@@ -44,7 +44,7 @@
     * @return 车辆isd
     */
    @GetMapping("/inner/fleet-operations/getVehicleByUserId/{id}")
    R<List<Long>> getVehicleByUserId(@PathVariable("id") Long id){
    public R<List<Long>> getVehicleByUserId(@PathVariable("id") Long id){
        List<Long> fleetIds = xpFleetOperationsService.lambdaQuery().eq(XpFleetOperations::getOperationsUserId, id)
                .list().stream().map(XpFleetOperations::getFleetId).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(fleetIds)){
@@ -73,4 +73,23 @@
                .collect(Collectors.toList());
        return R.ok(fleetIds);
    }
    /**
     * 根据车队ID查询关联的运维人员ID列表(内部接口)
     *
     * @param fleetId 车队ID
     * @return 运维人员ID列表
     */
    @GetMapping("/inner/fleet-operations/getOperationsIdsByFleetId/{fleetId}")
    public R<List<Long>> getOperationsIdsByFleetId(@PathVariable("fleetId") Long fleetId) {
        List<Long> operationsIds = xpFleetOperationsService.lambdaQuery()
                .eq(XpFleetOperations::getFleetId, fleetId)
                .list()
                .stream()
                .map(XpFleetOperations::getOperationsUserId)
                .distinct()
                .collect(Collectors.toList());
        return R.ok(operationsIds);
    }
}