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