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