| | |
| | | |
| | | // 构建天地图地理编码API URL |
| | | String url = "http://api.tianditu.gov.cn/geocoder"; |
| | | String params = "ds={\"keyWord\":\"" + address + \"}" + |
| | | "&tk=" + tiandituMapConfig.getTk(); |
| | | String params = "ds={\"keyWord\":\"" + address + "\"}&tk=" + tiandituMapConfig.getTk(); |
| | | |
| | | logger.info("天地图地理编码请求: address={}", address); |
| | | |
| | |
| | | |
| | | // 第一步:起点地址转坐标 |
| | | String geocodingUrl1 = "http://api.tianditu.gov.cn/geocoder"; |
| | | String geocodingParams1 = "ds={\"keyWord\":\"" + fromAddress + \"}" + |
| | | String geocodingParams1 = "ds={\"keyWord\":\"" + fromAddress + "\"}" + |
| | | "&tk=" + tiandituMapConfig.getTk(); |
| | | |
| | | String geocodingResponse1 = HttpUtils.sendGet(geocodingUrl1, geocodingParams1); |
| | |
| | | |
| | | // 第二步:终点地址转坐标 |
| | | String geocodingUrl2 = "http://api.tianditu.gov.cn/geocoder"; |
| | | String geocodingParams2 = "ds={\"keyWord\":\"" + toAddress + \"}" + |
| | | String geocodingParams2 = "ds={\"keyWord\":\"" + toAddress + "\"}" + |
| | | "&tk=" + tiandituMapConfig.getTk(); |
| | | |
| | | String geocodingResponse2 = HttpUtils.sendGet(geocodingUrl2, geocodingParams2); |
| New file |
| | |
| | | package com.ruoyi.web.controller.system; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import com.ruoyi.common.annotation.Log; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import com.ruoyi.common.core.page.TableDataInfo; |
| | | import com.ruoyi.common.enums.BusinessType; |
| | | import com.ruoyi.system.domain.VehicleGpsSegmentMileage; |
| | | import com.ruoyi.system.mapper.VehicleGpsSegmentMileageMapper; |
| | | import com.ruoyi.system.service.IVehicleGpsSegmentMileageService; |
| | | import com.ruoyi.system.service.ISysConfigService; |
| | | |
| | | /** |
| | | * GPS分段里程Controller |
| | | */ |
| | | @RestController |
| | | @RequestMapping("/system/gpsSegment") |
| | | public class VehicleGpsSegmentMileageController extends BaseController { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(VehicleGpsSegmentMileageController.class); |
| | | |
| | | @Autowired |
| | | private VehicleGpsSegmentMileageMapper segmentMileageMapper; |
| | | |
| | | @Autowired |
| | | private IVehicleGpsSegmentMileageService segmentMileageService; |
| | | |
| | | @Autowired |
| | | private ISysConfigService configService; |
| | | |
| | | /** |
| | | * 查询GPS分段里程列表 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gpsSegment:list')") |
| | | @GetMapping("/list") |
| | | public TableDataInfo list(VehicleGpsSegmentMileage segmentMileage) { |
| | | startPage(); |
| | | List<VehicleGpsSegmentMileage> list = segmentMileageMapper.selectVehicleGpsSegmentMileageList(segmentMileage); |
| | | return getDataTable(list); |
| | | } |
| | | |
| | | /** |
| | | * 按任务ID查询GPS分段里程列表 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gpsSegment:query')") |
| | | @GetMapping("/task/{taskId}") |
| | | public AjaxResult getSegmentsByTask(@PathVariable("taskId") Long taskId) { |
| | | try { |
| | | List<VehicleGpsSegmentMileage> list = segmentMileageMapper.selectSegmentsByTaskId(taskId); |
| | | return success(list); |
| | | } catch (Exception e) { |
| | | logger.error("查询任务GPS里程失败 - taskId: {}", taskId, e); |
| | | return error("查询失败:" + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询任务的总里程 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gpsSegment:query')") |
| | | @GetMapping("/task/{taskId}/total") |
| | | public AjaxResult getTaskTotalMileage(@PathVariable("taskId") Long taskId) { |
| | | try { |
| | | BigDecimal totalMileage = segmentMileageMapper.selectTotalMileageByTaskId(taskId); |
| | | return success(totalMileage); |
| | | } catch (Exception e) { |
| | | logger.error("查询任务总里程失败 - taskId: {}", taskId, e); |
| | | return error("查询失败:" + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询车辆指定日期范围的GPS分段里程 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gpsSegment:query')") |
| | | @GetMapping("/range") |
| | | public AjaxResult getSegmentsByDateRange( |
| | | @RequestParam("vehicleId") Long vehicleId, |
| | | @RequestParam("startDate") String startDateStr, |
| | | @RequestParam("endDate") String endDateStr) { |
| | | try { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | Date startDate = sdf.parse(startDateStr + " 00:00:00"); |
| | | Date endDate = sdf.parse(endDateStr + " 23:59:59"); |
| | | |
| | | List<VehicleGpsSegmentMileage> list = segmentMileageMapper.selectSegmentsByDateRange(vehicleId, startDate, endDate); |
| | | return success(list); |
| | | } catch (Exception e) { |
| | | logger.error("查询GPS分段里程失败 - vehicleId: {}, dateRange: {} - {}", |
| | | vehicleId, startDateStr, endDateStr, e); |
| | | return error("查询失败:" + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 补偿计算:查找并计算未被处理的GPS数据 |
| | | * 用于服务重启后的数据修复 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gpsSegment:compensate')") |
| | | @Log(title = "GPS里程补偿计算", businessType = BusinessType.OTHER) |
| | | @PostMapping("/compensate") |
| | | public AjaxResult compensateCalculation() { |
| | | try { |
| | | // 从配置中获取回溯天数,默认2天 |
| | | String lookbackDaysConfig = configService.selectConfigByKey("gps.mileage.compensate.lookback.days"); |
| | | int lookbackDays = lookbackDaysConfig != null ? Integer.parseInt(lookbackDaysConfig) : 2; |
| | | |
| | | logger.info("开始执行GPS里程补偿计算 - 回溯天数: {}", lookbackDays); |
| | | |
| | | int successCount = segmentMileageService.compensateCalculation(lookbackDays); |
| | | |
| | | return success("补偿计算完成,处理 " + successCount + " 辆车"); |
| | | } catch (Exception e) { |
| | | logger.error("GPS里程补偿计算失败", e); |
| | | return error("补偿计算失败:" + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | */ |
| | | @GetMapping(value = "/{vehicleId}") |
| | | public AjaxResult getInfo(@PathVariable("vehicleId") Long vehicleId) { |
| | | return success(vehicleInfoService.selectVehicleInfoById(vehicleId)); |
| | | return success(vehicleInfoService.selectVehicleInfoWithDeptsById(vehicleId)); |
| | | } |
| | | |
| | | /** |
| | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.DeleteMapping; |
| | |
| | | @RestController |
| | | @RequestMapping("/system/mileageStats") |
| | | public class VehicleMileageStatsController extends BaseController { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(VehicleMileageStatsController.class); |
| | | |
| | | @Autowired |
| | | private IVehicleMileageStatsService vehicleMileageStatsService; |
| | |
| | | @PostMapping("/calculate") |
| | | public AjaxResult calculate(Long vehicleId, String statDate) { |
| | | try { |
| | | if (statDate == null || statDate.trim().isEmpty()) { |
| | | return error("统计日期不能为空"); |
| | | } |
| | | |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | Date date = sdf.parse(statDate); |
| | | sdf.setLenient(false); // 严格解析日期 |
| | | Date date = sdf.parse(statDate.trim()); |
| | | |
| | | VehicleMileageStats stats = vehicleMileageStatsService.calculateAndSaveMileageStats(vehicleId, date); |
| | | // 修复String到Date转换问题,添加更好的错误处理 |
| | | if (stats != null) { |
| | | return success("里程统计计算成功", stats); |
| | | return AjaxResult.success("里程统计计算成功", stats); |
| | | } else { |
| | | return error("该车辆在指定日期无GPS数据"); |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("里程统计计算失败 - 车辆ID: {}, 日期: {}", vehicleId, statDate, e); |
| | | return error("里程统计计算失败:" + e.getMessage()); |
| | | } |
| | | } |
| | |
| | | @PostMapping("/batchCalculate") |
| | | public AjaxResult batchCalculate(String statDate) { |
| | | try { |
| | | if (statDate == null || statDate.trim().isEmpty()) { |
| | | return error("统计日期不能为空"); |
| | | } |
| | | |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | Date date = sdf.parse(statDate); |
| | | sdf.setLenient(false); // 严格解析日期 |
| | | Date date = sdf.parse(statDate.trim()); |
| | | |
| | | int count = vehicleMileageStatsService.batchCalculateMileageStats(date); |
| | | return success("批量里程统计完成,成功统计 " + count + " 辆车"); |
| | | } catch (Exception e) { |
| | | logger.error("批量里程统计失败 - 日期: {}", statDate, e); |
| | | return error("批量里程统计失败:" + e.getMessage()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.framework.config; |
| | | |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.module.SimpleModule; |
| | | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; |
| | | import org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory; |
| | | import org.apache.ibatis.executor.loader.cglib.CglibProxyFactory; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
| | | |
| | | import java.math.BigInteger; |
| | | |
| | | /** |
| | | * Jackson配置类 |
| | | * 解决MyBatis延迟加载代理对象序列化问题 |
| | | */ |
| | | @Configuration |
| | | public class JacksonConfig { |
| | | |
| | | @Bean |
| | | public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { |
| | | ObjectMapper objectMapper = builder.createXmlMapper(false).build(); |
| | | |
| | | // 注册自定义模块 |
| | | SimpleModule module = new SimpleModule(); |
| | | module.addSerializer(BigInteger.class, ToStringSerializer.instance); |
| | | module.addSerializer(Long.class, ToStringSerializer.instance); |
| | | objectMapper.registerModule(module); |
| | | |
| | | // 禁用FAIL_ON_EMPTY_BEANS特性,避免代理对象序列化错误 |
| | | objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS); |
| | | |
| | | return objectMapper; |
| | | } |
| | | } |
| | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | |
| | | // 1. 获取所有车辆信息 |
| | | List<VehicleInfo> vehicleList = vehicleInfoService.selectVehicleInfoList(new VehicleInfo()); |
| | | |
| | | List<String> deviceIds = vehicleList.stream().map(VehicleInfo::getDeviceId).collect(Collectors.toList()); |
| | | // 2. 获取所有车辆的GPS最后位置 |
| | | GpsLastPositionResponse gpsLastPositionResponse = gpsCollectService.getLastPosition(new GpsLastPositionRequest()); |
| | | GpsLastPositionRequest request = new GpsLastPositionRequest(); |
| | | // request.setDeviceids(deviceIds); |
| | | GpsLastPositionResponse gpsLastPositionResponse = gpsCollectService.getLastPosition(request); |
| | | |
| | | // 3. 遍历车辆列表,获取每个车辆的GPS位置 |
| | | for (VehicleInfo vehicle : vehicleList) { |
| New file |
| | |
| | | package com.ruoyi.quartz.task; |
| | | |
| | | import java.util.Calendar; |
| | | import java.util.Date; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | | import com.ruoyi.system.service.IVehicleGpsSegmentMileageService; |
| | | import com.ruoyi.system.service.ISysConfigService; |
| | | |
| | | /** |
| | | * 车辆GPS分段里程计算定时任务 |
| | | */ |
| | | @Component("vehicleGpsSegmentMileageTask") |
| | | public class VehicleGpsSegmentMileageTask { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(VehicleGpsSegmentMileageTask.class); |
| | | |
| | | @Autowired |
| | | private IVehicleGpsSegmentMileageService segmentMileageService; |
| | | |
| | | @Autowired |
| | | private ISysConfigService configService; |
| | | |
| | | /** |
| | | * 计算最近一段时间的GPS分段里程 |
| | | * 默认计算最近1小时的数据 |
| | | */ |
| | | public void calculateRecentSegmentMileage() { |
| | | calculateRecentSegmentMileage("60"); |
| | | } |
| | | |
| | | /** |
| | | * 计算最近指定分钟数的GPS分段里程 |
| | | * |
| | | * @param params 参数字符串,格式:分钟数(如:60表示最近60分钟) |
| | | */ |
| | | public void calculateRecentSegmentMileage(String params) { |
| | | try { |
| | | // 解析参数:要计算的时间范围(分钟) |
| | | int minutes = 60; // 默认60分钟 |
| | | if (params != null && !params.trim().isEmpty()) { |
| | | try { |
| | | minutes = Integer.parseInt(params.trim()); |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("参数格式错误,使用默认值60分钟: {}", params); |
| | | } |
| | | } |
| | | |
| | | // 获取配置的时间间隔 |
| | | int segmentMinutes = 5; // 默认5分钟 |
| | | String segmentConfig = configService.selectConfigByKey("gps.mileage.segment.minutes"); |
| | | if (segmentConfig != null && !segmentConfig.isEmpty()) { |
| | | try { |
| | | segmentMinutes = Integer.parseInt(segmentConfig); |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("分段时间间隔配置错误,使用默认值5分钟"); |
| | | } |
| | | } |
| | | |
| | | // 计算时间范围 |
| | | Calendar cal = Calendar.getInstance(); |
| | | Date endTime = cal.getTime(); |
| | | cal.add(Calendar.MINUTE, -minutes); |
| | | Date startTime = cal.getTime(); |
| | | |
| | | logger.info("开始计算GPS分段里程 - 时间范围: {} 到 {}, 时间段间隔: {}分钟", |
| | | startTime, endTime, segmentMinutes); |
| | | |
| | | // 批量计算 |
| | | int successCount = segmentMileageService.batchCalculateSegmentMileage(startTime, endTime); |
| | | |
| | | logger.info("GPS分段里程计算完成 - 成功处理 {} 辆车", successCount); |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("GPS分段里程计算任务执行失败", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算指定日期的GPS分段里程 |
| | | * |
| | | * @param params 参数字符串,格式:yyyy-MM-dd(如:2025-01-15) |
| | | */ |
| | | public void calculateDateSegmentMileage(String params) { |
| | | try { |
| | | if (params == null || params.trim().isEmpty()) { |
| | | logger.error("日期参数不能为空"); |
| | | return; |
| | | } |
| | | |
| | | // 解析日期 |
| | | java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd"); |
| | | Date date = sdf.parse(params.trim()); |
| | | |
| | | // 计算当天的起止时间 |
| | | Calendar cal = Calendar.getInstance(); |
| | | cal.setTime(date); |
| | | cal.set(Calendar.HOUR_OF_DAY, 0); |
| | | cal.set(Calendar.MINUTE, 0); |
| | | cal.set(Calendar.SECOND, 0); |
| | | cal.set(Calendar.MILLISECOND, 0); |
| | | Date startTime = cal.getTime(); |
| | | |
| | | cal.add(Calendar.DAY_OF_MONTH, 1); |
| | | Date endTime = cal.getTime(); |
| | | |
| | | logger.info("开始计算指定日期GPS分段里程 - 日期: {}, 时间范围: {} 到 {}", |
| | | params, startTime, endTime); |
| | | |
| | | // 批量计算 |
| | | int successCount = segmentMileageService.batchCalculateSegmentMileage(startTime, endTime); |
| | | |
| | | logger.info("指定日期GPS分段里程计算完成 - 成功处理 {} 辆车", successCount); |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("指定日期GPS分段里程计算任务执行失败 - 参数: {}", params, e); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.quartz.task; |
| | | |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Calendar; |
| | | import java.util.Date; |
| | | import org.slf4j.Logger; |
| | |
| | | logger.info("开始执行车辆里程统计定时任务 - 统计日期: {}", dateStr); |
| | | |
| | | try { |
| | | // 解析日期字符串 |
| | | String[] parts = dateStr.split("-"); |
| | | if (parts.length != 3) { |
| | | throw new IllegalArgumentException("日期格式错误,应为: yyyy-MM-dd"); |
| | | if (dateStr == null || dateStr.trim().isEmpty()) { |
| | | throw new IllegalArgumentException("日期不能为空"); |
| | | } |
| | | |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.set(Calendar.YEAR, Integer.parseInt(parts[0])); |
| | | calendar.set(Calendar.MONTH, Integer.parseInt(parts[1]) - 1); // 月份从0开始 |
| | | calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(parts[2])); |
| | | calendar.set(Calendar.HOUR_OF_DAY, 0); |
| | | calendar.set(Calendar.MINUTE, 0); |
| | | calendar.set(Calendar.SECOND, 0); |
| | | calendar.set(Calendar.MILLISECOND, 0); |
| | | Date targetDate = calendar.getTime(); |
| | | // 使用SimpleDateFormat解析日期字符串 |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | sdf.setLenient(false); // 严格解析日期 |
| | | Date targetDate = sdf.parse(dateStr.trim()); |
| | | |
| | | // 批量计算里程统计 |
| | | int successCount = vehicleMileageStatsService.batchCalculateMileageStats(targetDate); |
| | | |
| | | logger.info("车辆里程统计定时任务执行完成 - 日期: {}, 成功统计: {} 辆车", dateStr, successCount); |
| | | |
| | | } catch (ParseException e) { |
| | | logger.error("车辆里程统计定时任务执行失败 - 日期格式错误: {}", dateStr, e); |
| | | throw new RuntimeException("定时任务执行失败: 日期格式错误,应为 yyyy-MM-dd"); |
| | | } catch (Exception e) { |
| | | logger.error("车辆里程统计定时任务执行失败 - 日期: {}", dateStr, e); |
| | | throw new RuntimeException("定时任务执行失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从GPS分段里程汇总生成昨日统计数据 |
| | | * (推荐使用此方法,基于已计算的分段里程数据汇总,性能更好) |
| | | */ |
| | | public void aggregateYesterdayFromSegments() { |
| | | logger.info("开始执行从分段里程汇总任务 - 统计昨日数据"); |
| | | |
| | | try { |
| | | // 获取昨天的日期 |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.add(Calendar.DAY_OF_MONTH, -1); |
| | | calendar.set(Calendar.HOUR_OF_DAY, 0); |
| | | calendar.set(Calendar.MINUTE, 0); |
| | | calendar.set(Calendar.SECOND, 0); |
| | | calendar.set(Calendar.MILLISECOND, 0); |
| | | Date yesterday = calendar.getTime(); |
| | | |
| | | // 从分段里程汇总生成统计 |
| | | int successCount = vehicleMileageStatsService.batchAggregateFromSegmentMileage(yesterday); |
| | | |
| | | logger.info("从分段里程汇总任务执行完成 - 成功统计: {} 辆车", successCount); |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("从分段里程汇总任务执行失败", e); |
| | | throw new RuntimeException("汇总任务执行失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从GPS分段里程汇总生成指定日期的统计数据 |
| | | * |
| | | * @param dateStr 日期字符串,格式:yyyy-MM-dd |
| | | */ |
| | | public void aggregateFromSegmentsByDate(String dateStr) { |
| | | logger.info("开始执行从分段里程汇总任务 - 统计日期: {}", dateStr); |
| | | |
| | | try { |
| | | if (dateStr == null || dateStr.trim().isEmpty()) { |
| | | throw new IllegalArgumentException("日期不能为空"); |
| | | } |
| | | |
| | | // 使用SimpleDateFormat解析日期字符串 |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | sdf.setLenient(false); // 严格解析日期 |
| | | Date targetDate = sdf.parse(dateStr.trim()); |
| | | |
| | | // 从分段里程汇总生成统计 |
| | | int successCount = vehicleMileageStatsService.batchAggregateFromSegmentMileage(targetDate); |
| | | |
| | | logger.info("从分段里程汇总任务执行完成 - 日期: {}, 成功统计: {} 辆车", dateStr, successCount); |
| | | |
| | | } catch (ParseException e) { |
| | | logger.error("从分段里程汇总任务执行失败 - 日期格式错误: {}", dateStr, e); |
| | | throw new RuntimeException("汇总任务执行失败: 日期格式错误,应为 yyyy-MM-dd"); |
| | | } catch (Exception e) { |
| | | logger.error("从分段里程汇总任务执行失败 - 日期: {}", dateStr, e); |
| | | throw new RuntimeException("汇总任务执行失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.domain; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Date; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import com.ruoyi.common.annotation.Excel; |
| | | import com.ruoyi.common.core.domain.BaseEntity; |
| | | |
| | | /** |
| | | * 车辆GPS分段里程对象 tb_vehicle_gps_segment_mileage |
| | | */ |
| | | public class VehicleGpsSegmentMileage extends BaseEntity { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** 分段ID */ |
| | | private Long segmentId; |
| | | |
| | | /** 车辆ID */ |
| | | @Excel(name = "车辆ID") |
| | | private Long vehicleId; |
| | | |
| | | /** 车牌号 */ |
| | | @Excel(name = "车牌号") |
| | | private String vehicleNo; |
| | | |
| | | /** 时间段开始时间 */ |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @Excel(name = "时间段开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") |
| | | private Date segmentStartTime; |
| | | |
| | | /** 时间段结束时间 */ |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @Excel(name = "时间段结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") |
| | | private Date segmentEndTime; |
| | | |
| | | /** 起点经度 */ |
| | | @Excel(name = "起点经度") |
| | | private BigDecimal startLongitude; |
| | | |
| | | /** 起点纬度 */ |
| | | @Excel(name = "起点纬度") |
| | | private BigDecimal startLatitude; |
| | | |
| | | /** 终点经度 */ |
| | | @Excel(name = "终点经度") |
| | | private BigDecimal endLongitude; |
| | | |
| | | /** 终点纬度 */ |
| | | @Excel(name = "终点纬度") |
| | | private BigDecimal endLatitude; |
| | | |
| | | /** 段距离(公里) */ |
| | | @Excel(name = "段距离(公里)") |
| | | private BigDecimal segmentDistance; |
| | | |
| | | /** GPS点数量 */ |
| | | @Excel(name = "GPS点数量") |
| | | private Integer gpsPointCount; |
| | | |
| | | /** 关联的GPS记录ID列表 */ |
| | | private String gpsIds; |
| | | |
| | | /** 关联的任务ID */ |
| | | @Excel(name = "关联任务ID") |
| | | private Long taskId; |
| | | |
| | | /** 任务编号 */ |
| | | @Excel(name = "任务编号") |
| | | private String taskCode; |
| | | |
| | | /** 计算方式 */ |
| | | @Excel(name = "计算方式") |
| | | private String calculateMethod; |
| | | |
| | | public Long getSegmentId() { |
| | | return segmentId; |
| | | } |
| | | |
| | | public void setSegmentId(Long segmentId) { |
| | | this.segmentId = segmentId; |
| | | } |
| | | |
| | | public Long getVehicleId() { |
| | | return vehicleId; |
| | | } |
| | | |
| | | public void setVehicleId(Long vehicleId) { |
| | | this.vehicleId = vehicleId; |
| | | } |
| | | |
| | | public String getVehicleNo() { |
| | | return vehicleNo; |
| | | } |
| | | |
| | | public void setVehicleNo(String vehicleNo) { |
| | | this.vehicleNo = vehicleNo; |
| | | } |
| | | |
| | | public Date getSegmentStartTime() { |
| | | return segmentStartTime; |
| | | } |
| | | |
| | | public void setSegmentStartTime(Date segmentStartTime) { |
| | | this.segmentStartTime = segmentStartTime; |
| | | } |
| | | |
| | | public Date getSegmentEndTime() { |
| | | return segmentEndTime; |
| | | } |
| | | |
| | | public void setSegmentEndTime(Date segmentEndTime) { |
| | | this.segmentEndTime = segmentEndTime; |
| | | } |
| | | |
| | | public BigDecimal getStartLongitude() { |
| | | return startLongitude; |
| | | } |
| | | |
| | | public void setStartLongitude(BigDecimal startLongitude) { |
| | | this.startLongitude = startLongitude; |
| | | } |
| | | |
| | | public BigDecimal getStartLatitude() { |
| | | return startLatitude; |
| | | } |
| | | |
| | | public void setStartLatitude(BigDecimal startLatitude) { |
| | | this.startLatitude = startLatitude; |
| | | } |
| | | |
| | | public BigDecimal getEndLongitude() { |
| | | return endLongitude; |
| | | } |
| | | |
| | | public void setEndLongitude(BigDecimal endLongitude) { |
| | | this.endLongitude = endLongitude; |
| | | } |
| | | |
| | | public BigDecimal getEndLatitude() { |
| | | return endLatitude; |
| | | } |
| | | |
| | | public void setEndLatitude(BigDecimal endLatitude) { |
| | | this.endLatitude = endLatitude; |
| | | } |
| | | |
| | | public BigDecimal getSegmentDistance() { |
| | | return segmentDistance; |
| | | } |
| | | |
| | | public void setSegmentDistance(BigDecimal segmentDistance) { |
| | | this.segmentDistance = segmentDistance; |
| | | } |
| | | |
| | | public Integer getGpsPointCount() { |
| | | return gpsPointCount; |
| | | } |
| | | |
| | | public void setGpsPointCount(Integer gpsPointCount) { |
| | | this.gpsPointCount = gpsPointCount; |
| | | } |
| | | |
| | | public String getGpsIds() { |
| | | return gpsIds; |
| | | } |
| | | |
| | | public void setGpsIds(String gpsIds) { |
| | | this.gpsIds = gpsIds; |
| | | } |
| | | |
| | | public Long getTaskId() { |
| | | return taskId; |
| | | } |
| | | |
| | | public void setTaskId(Long taskId) { |
| | | this.taskId = taskId; |
| | | } |
| | | |
| | | public String getTaskCode() { |
| | | return taskCode; |
| | | } |
| | | |
| | | public void setTaskCode(String taskCode) { |
| | | this.taskCode = taskCode; |
| | | } |
| | | |
| | | public String getCalculateMethod() { |
| | | return calculateMethod; |
| | | } |
| | | |
| | | public void setCalculateMethod(String calculateMethod) { |
| | | this.calculateMethod = calculateMethod; |
| | | } |
| | | } |
| | |
| | | import com.ruoyi.common.core.domain.BaseEntity; |
| | | |
| | | import java.util.List; |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * 车辆信息对象 tb_vehicle_info |
| | | */ |
| | | public class VehicleInfo extends BaseEntity { |
| | | public class VehicleInfo extends BaseEntity implements Serializable { |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** 车辆ID */ |
| | |
| | | .append("remark", getRemark()) |
| | | .toString(); |
| | | } |
| | | |
| | | /** |
| | | * 初始化延迟加载的属性,避免序列化问题 |
| | | */ |
| | | public void initializeLazyProperties() { |
| | | if (this.deptIds != null) { |
| | | this.deptIds.size(); // 触发加载 |
| | | } |
| | | if (this.deptNames != null) { |
| | | this.deptNames.size(); // 触发加载 |
| | | } |
| | | } |
| | | } |
| | |
| | | @Excel(name = "车牌号") |
| | | private String vehicleNo; |
| | | |
| | | /** 归属分公司 */ |
| | | @Excel(name = "归属分公司") |
| | | private String deptName; |
| | | |
| | | /** 分公司ID(用于查询,不导出) */ |
| | | private Long deptId; |
| | | |
| | | /** 统计日期 */ |
| | | @JsonFormat(pattern = "yyyy-MM-dd") |
| | | @Excel(name = "统计日期", width = 30, dateFormat = "yyyy-MM-dd") |
| | |
| | | @Excel(name = "任务数量") |
| | | private Integer taskCount; |
| | | |
| | | /** 关联的分段数量 */ |
| | | @Excel(name = "分段数量") |
| | | private Integer segmentCount; |
| | | |
| | | /** 数据来源(segment-从分段汇总,gps-直接计算) */ |
| | | private String dataSource; |
| | | |
| | | public Long getStatsId() { |
| | | return statsId; |
| | | } |
| | |
| | | |
| | | public void setVehicleNo(String vehicleNo) { |
| | | this.vehicleNo = vehicleNo; |
| | | } |
| | | |
| | | public String getDeptName() { |
| | | return deptName; |
| | | } |
| | | |
| | | public void setDeptName(String deptName) { |
| | | this.deptName = deptName; |
| | | } |
| | | |
| | | public Long getDeptId() { |
| | | return deptId; |
| | | } |
| | | |
| | | public void setDeptId(Long deptId) { |
| | | this.deptId = deptId; |
| | | } |
| | | |
| | | public Date getStatDate() { |
| | |
| | | public void setTaskCount(Integer taskCount) { |
| | | this.taskCount = taskCount; |
| | | } |
| | | |
| | | public Integer getSegmentCount() { |
| | | return segmentCount; |
| | | } |
| | | |
| | | public void setSegmentCount(Integer segmentCount) { |
| | | this.segmentCount = segmentCount; |
| | | } |
| | | |
| | | public String getDataSource() { |
| | | return dataSource; |
| | | } |
| | | |
| | | public void setDataSource(String dataSource) { |
| | | this.dataSource = dataSource; |
| | | } |
| | | } |
| | |
| | | * @return 车辆ID列表 |
| | | */ |
| | | public List<Long> selectActiveVehicleIds(); |
| | | |
| | | /** |
| | | * 查询未被计算的GPS坐标(不在tb_vehicle_gps_calculated表中的记录) |
| | | * |
| | | * @param vehicleId 车辆ID |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 未被计算的GPS坐标列表 |
| | | */ |
| | | public List<VehicleGps> selectUncalculatedGps(@Param("vehicleId") Long vehicleId, |
| | | @Param("startTime") Date startTime, |
| | | @Param("endTime") Date endTime); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.mapper; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import com.ruoyi.system.domain.VehicleGpsSegmentMileage; |
| | | |
| | | /** |
| | | * 车辆GPS分段里程Mapper接口 |
| | | */ |
| | | public interface VehicleGpsSegmentMileageMapper { |
| | | |
| | | /** |
| | | * 查询车辆GPS分段里程 |
| | | */ |
| | | public VehicleGpsSegmentMileage selectVehicleGpsSegmentMileageById(Long segmentId); |
| | | |
| | | /** |
| | | * 查询车辆GPS分段里程列表 |
| | | */ |
| | | public List<VehicleGpsSegmentMileage> selectVehicleGpsSegmentMileageList(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 新增车辆GPS分段里程 |
| | | */ |
| | | public int insertVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 修改车辆GPS分段里程 |
| | | */ |
| | | public int updateVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 删除车辆GPS分段里程 |
| | | */ |
| | | public int deleteVehicleGpsSegmentMileageById(Long segmentId); |
| | | |
| | | /** |
| | | * 批量删除车辆GPS分段里程 |
| | | */ |
| | | public int deleteVehicleGpsSegmentMileageByIds(Long[] segmentIds); |
| | | |
| | | /** |
| | | * 查询车辆在指定时间段是否已存在分段里程记录 |
| | | */ |
| | | public VehicleGpsSegmentMileage selectByVehicleIdAndTime(@Param("vehicleId") Long vehicleId, |
| | | @Param("segmentStartTime") Date segmentStartTime); |
| | | |
| | | /** |
| | | * 查询车辆在指定日期范围内的分段里程统计 |
| | | */ |
| | | public List<VehicleGpsSegmentMileage> selectSegmentsByDateRange(@Param("vehicleId") Long vehicleId, |
| | | @Param("startDate") Date startDate, |
| | | @Param("endDate") Date endDate); |
| | | |
| | | /** |
| | | * 按任务ID查询分段里程列表 |
| | | */ |
| | | public List<VehicleGpsSegmentMileage> selectSegmentsByTaskId(@Param("taskId") Long taskId); |
| | | |
| | | /** |
| | | * 查询任务的总里程(直接求和) |
| | | */ |
| | | public BigDecimal selectTotalMileageByTaskId(@Param("taskId") Long taskId); |
| | | |
| | | /** |
| | | * 记录GPS点已被计算(插入到tb_vehicle_gps_calculated表) |
| | | */ |
| | | public int insertGpsCalculated(@Param("gpsId") Long gpsId, |
| | | @Param("segmentId") Long segmentId, |
| | | @Param("vehicleId") Long vehicleId); |
| | | |
| | | /** |
| | | * 检查GPS点是否已被计算 |
| | | */ |
| | | public Long selectGpsCalculatedSegmentId(@Param("gpsId") Long gpsId); |
| | | } |
| | |
| | | */ |
| | | public interface VehicleInfoMapper { |
| | | /** |
| | | * 查询车辆信息(包含多分公司关联) |
| | | * |
| | | * @param vehicleId 车辆信息主键 |
| | | * @return 车辆信息(包含deptIds和deptNames) |
| | | */ |
| | | public VehicleInfo selectVehicleInfoWithDeptsById(Long vehicleId); |
| | | |
| | | /** |
| | | * 查询车辆信息列表(包含多分公司关联) |
| | | * |
| | | * @param vehicleInfo 车辆信息 |
| | | * @return 车辆信息集合(包含deptIds和deptNames) |
| | | */ |
| | | public List<VehicleInfo> selectVehicleInfoListWithDepts(VehicleInfo vehicleInfo); |
| | | |
| | | /** |
| | | * 查询车辆信息 |
| | | * |
| | | * @param vehicleId 车辆信息主键 |
| New file |
| | |
| | | package com.ruoyi.system.service; |
| | | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import com.ruoyi.system.domain.VehicleGpsSegmentMileage; |
| | | |
| | | /** |
| | | * 车辆GPS分段里程Service接口 |
| | | */ |
| | | public interface IVehicleGpsSegmentMileageService { |
| | | |
| | | /** |
| | | * 查询车辆GPS分段里程 |
| | | */ |
| | | public VehicleGpsSegmentMileage selectVehicleGpsSegmentMileageById(Long segmentId); |
| | | |
| | | /** |
| | | * 查询车辆GPS分段里程列表 |
| | | */ |
| | | public List<VehicleGpsSegmentMileage> selectVehicleGpsSegmentMileageList(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 新增车辆GPS分段里程 |
| | | */ |
| | | public int insertVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 修改车辆GPS分段里程 |
| | | */ |
| | | public int updateVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage); |
| | | |
| | | /** |
| | | * 批量删除车辆GPS分段里程 |
| | | */ |
| | | public int deleteVehicleGpsSegmentMileageByIds(Long[] segmentIds); |
| | | |
| | | /** |
| | | * 删除车辆GPS分段里程信息 |
| | | */ |
| | | public int deleteVehicleGpsSegmentMileageById(Long segmentId); |
| | | |
| | | /** |
| | | * 批量计算所有车辆的GPS分段里程 |
| | | * |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 成功计算的车辆数量 |
| | | */ |
| | | public int batchCalculateSegmentMileage(Date startTime, Date endTime); |
| | | |
| | | /** |
| | | * 计算单个车辆的GPS分段里程 |
| | | * |
| | | * @param vehicleId 车辆ID |
| | | * @param startTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 成功计算的分段数量 |
| | | */ |
| | | public int calculateVehicleSegmentMileage(Long vehicleId, Date startTime, Date endTime); |
| | | |
| | | /** |
| | | * 补偿计算:查找并计算未被处理的GPS数据 |
| | | * 用于服务重启后的数据修复 |
| | | * |
| | | * @param lookbackDays 回溯天数(查询多少天前的数据) |
| | | * @return 成功计算的车辆数量 |
| | | */ |
| | | public int compensateCalculation(int lookbackDays); |
| | | } |
| | |
| | | public List<VehicleInfo> selectVehicleInfoList(VehicleInfo vehicleInfo); |
| | | |
| | | /** |
| | | * 查询车辆信息(包含多分公司关联) |
| | | * |
| | | * @param vehicleId 车辆信息主键 |
| | | * @return 车辆信息(包含deptIds和deptNames) |
| | | */ |
| | | public VehicleInfo selectVehicleInfoWithDeptsById(Long vehicleId); |
| | | |
| | | /** |
| | | * 新增车辆信息 |
| | | * |
| | | * @param vehicleInfo 车辆信息 |
| | |
| | | * @return 成功统计的车辆数量 |
| | | */ |
| | | public int batchCalculateMileageStats(Date statDate); |
| | | |
| | | /** |
| | | * 从分段里程数据汇总生成按日统计 |
| | | * |
| | | * @param vehicleId 车辆ID |
| | | * @param statDate 统计日期 |
| | | * @return 统计结果 |
| | | */ |
| | | public VehicleMileageStats aggregateFromSegmentMileage(Long vehicleId, Date statDate); |
| | | |
| | | /** |
| | | * 批量从分段里程汇总生成按日统计 |
| | | * |
| | | * @param statDate 统计日期 |
| | | * @return 成功统计的车辆数量 |
| | | */ |
| | | public int batchAggregateFromSegmentMileage(Date statDate); |
| | | } |
| | |
| | | import com.ruoyi.system.service.IGpsCollectService; |
| | | import com.ruoyi.system.config.GpsServiceConfig; |
| | | import com.ruoyi.common.utils.MD5Util; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.web.client.RestTemplate; |
| | | import org.springframework.http.HttpEntity; |
| | |
| | | /** |
| | | * GPS采集服务实现 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class GpsCollectServiceImpl implements IGpsCollectService { |
| | | |
| | |
| | | // 解析位置记录列表 |
| | | JSONArray recordsArray = jsonResult.getJSONArray("records"); |
| | | List<GpsLastPosition> records = new ArrayList<>(); |
| | | |
| | | log.info("recordsArray length:{}",recordsArray.size()); |
| | | for (int i = 0; i < recordsArray.size(); i++) { |
| | | JSONObject recordJson = recordsArray.getJSONObject(i); |
| | | GpsLastPosition record = new GpsLastPosition(); |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.*; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.alibaba.fastjson2.JSONArray; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.ruoyi.common.config.TiandituMapConfig; |
| | | import com.ruoyi.common.core.redis.RedisCache; |
| | | import com.ruoyi.common.utils.http.HttpUtils; |
| | | import com.ruoyi.system.domain.VehicleGps; |
| | | import com.ruoyi.system.domain.VehicleGpsSegmentMileage; |
| | | import com.ruoyi.system.domain.SysTask; |
| | | import com.ruoyi.system.domain.VehicleInfo; |
| | | import com.ruoyi.system.mapper.VehicleGpsMapper; |
| | | import com.ruoyi.system.mapper.VehicleGpsSegmentMileageMapper; |
| | | import com.ruoyi.system.mapper.SysTaskMapper; |
| | | import com.ruoyi.system.mapper.VehicleInfoMapper; |
| | | import com.ruoyi.system.service.IVehicleGpsSegmentMileageService; |
| | | import com.ruoyi.system.service.IVehicleMileageStatsService; |
| | | import com.ruoyi.system.service.ISysConfigService; |
| | | |
| | | /** |
| | | * 车辆GPS分段里程Service业务层处理 |
| | | */ |
| | | @Service |
| | | public class VehicleGpsSegmentMileageServiceImpl implements IVehicleGpsSegmentMileageService { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(VehicleGpsSegmentMileageServiceImpl.class); |
| | | |
| | | /** 地球半径(公里) */ |
| | | private static final double EARTH_RADIUS_KM = 6371.0; |
| | | |
| | | /** 天地图批量路径规划API */ |
| | | private static final String TIANDITU_ROUTE_API = "http://api.tianditu.gov.cn/drive"; |
| | | |
| | | @Autowired |
| | | private VehicleGpsSegmentMileageMapper segmentMileageMapper; |
| | | |
| | | @Autowired |
| | | private VehicleGpsMapper vehicleGpsMapper; |
| | | |
| | | @Autowired |
| | | private SysTaskMapper sysTaskMapper; |
| | | |
| | | @Autowired |
| | | private TiandituMapConfig tiandituMapConfig; |
| | | |
| | | @Autowired |
| | | private ISysConfigService configService; |
| | | |
| | | @Autowired |
| | | private RedisCache redisCache; |
| | | |
| | | @Autowired |
| | | private IVehicleMileageStatsService mileageStatsService; |
| | | |
| | | @Autowired |
| | | private VehicleInfoMapper vehicleInfoMapper; |
| | | |
| | | @Override |
| | | public VehicleGpsSegmentMileage selectVehicleGpsSegmentMileageById(Long segmentId) { |
| | | return segmentMileageMapper.selectVehicleGpsSegmentMileageById(segmentId); |
| | | } |
| | | |
| | | @Override |
| | | public List<VehicleGpsSegmentMileage> selectVehicleGpsSegmentMileageList(VehicleGpsSegmentMileage vehicleGpsSegmentMileage) { |
| | | return segmentMileageMapper.selectVehicleGpsSegmentMileageList(vehicleGpsSegmentMileage); |
| | | } |
| | | |
| | | @Override |
| | | public int insertVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage) { |
| | | return segmentMileageMapper.insertVehicleGpsSegmentMileage(vehicleGpsSegmentMileage); |
| | | } |
| | | |
| | | @Override |
| | | public int updateVehicleGpsSegmentMileage(VehicleGpsSegmentMileage vehicleGpsSegmentMileage) { |
| | | return segmentMileageMapper.updateVehicleGpsSegmentMileage(vehicleGpsSegmentMileage); |
| | | } |
| | | |
| | | @Override |
| | | public int deleteVehicleGpsSegmentMileageByIds(Long[] segmentIds) { |
| | | return segmentMileageMapper.deleteVehicleGpsSegmentMileageByIds(segmentIds); |
| | | } |
| | | |
| | | @Override |
| | | public int deleteVehicleGpsSegmentMileageById(Long segmentId) { |
| | | return segmentMileageMapper.deleteVehicleGpsSegmentMileageById(segmentId); |
| | | } |
| | | |
| | | @Override |
| | | public int batchCalculateSegmentMileage(Date startTime, Date endTime) { |
| | | try { |
| | | // 查询所有活跃车辆 |
| | | List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(); |
| | | |
| | | if (vehicleIds == null || vehicleIds.isEmpty()) { |
| | | logger.info("没有找到活跃车辆"); |
| | | return 0; |
| | | } |
| | | |
| | | int successCount = 0; |
| | | for (Long vehicleId : vehicleIds) { |
| | | try { |
| | | int segmentCount = calculateVehicleSegmentMileage(vehicleId, startTime, endTime); |
| | | if (segmentCount > 0) { |
| | | successCount++; |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("计算车辆 {} 的分段里程失败", vehicleId, e); |
| | | } |
| | | } |
| | | |
| | | logger.info("批量分段里程计算完成 - 时间范围: {} 到 {}, 总车辆数: {}, 成功: {}", |
| | | startTime, endTime, vehicleIds.size(), successCount); |
| | | return successCount; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("批量计算分段里程失败", e); |
| | | throw new RuntimeException("批量计算失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public int compensateCalculation(int lookbackDays) { |
| | | try { |
| | | // 计算时间范围 |
| | | Calendar cal = Calendar.getInstance(); |
| | | Date endTime = cal.getTime(); |
| | | cal.add(Calendar.DAY_OF_MONTH, -lookbackDays); |
| | | Date startTime = cal.getTime(); |
| | | |
| | | logger.info("开始补偿计算 - 回溯天数: {}, 时间范围: {} 到 {}", lookbackDays, startTime, endTime); |
| | | |
| | | // 查询所有活跃车辆 |
| | | List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(); |
| | | |
| | | if (vehicleIds == null || vehicleIds.isEmpty()) { |
| | | logger.info("没有找到活跃车辆"); |
| | | return 0; |
| | | } |
| | | |
| | | int successCount = 0; |
| | | int totalUncalculated = 0; |
| | | |
| | | for (Long vehicleId : vehicleIds) { |
| | | try { |
| | | // 查询该车辆未被计算的GPS数据 |
| | | List<VehicleGps> uncalculatedGps = vehicleGpsMapper.selectUncalculatedGps(vehicleId, startTime, endTime); |
| | | |
| | | if (uncalculatedGps == null || uncalculatedGps.isEmpty()) { |
| | | logger.debug("车辆 {} 没有未计算的GPS数据", vehicleId); |
| | | continue; |
| | | } |
| | | |
| | | totalUncalculated += uncalculatedGps.size(); |
| | | logger.info("车辆 {} 发现 {} 个未计算的GPS点,开始补偿计算...", |
| | | vehicleId, uncalculatedGps.size()); |
| | | |
| | | // 重新计算该车辆在该时间范围的分段里程 |
| | | // 注意:这里会重新计算整个时间范围,确保边缘节点被正确处理 |
| | | int segmentCount = calculateVehicleSegmentMileage(vehicleId, startTime, endTime); |
| | | |
| | | if (segmentCount > 0) { |
| | | successCount++; |
| | | logger.info("车辆 {} 补偿计算完成,生成 {} 个分段记录", vehicleId, segmentCount); |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("车辆 {} 补偿计算失败", vehicleId, e); |
| | | } |
| | | } |
| | | |
| | | logger.info("补偿计算完成 - 总车辆数: {}, 未计算GPS点数: {}, 成功车辆数: {}", |
| | | vehicleIds.size(), totalUncalculated, successCount); |
| | | return successCount; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("补偿计算失败", e); |
| | | throw new RuntimeException("补偿计算失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public int calculateVehicleSegmentMileage(Long vehicleId, Date startTime, Date endTime) { |
| | | try { |
| | | // 获取配置的时间间隔(分钟) |
| | | int segmentMinutes = configService.selectConfigByKey("gps.mileage.segment.minutes") != null |
| | | ? Integer.parseInt(configService.selectConfigByKey("gps.mileage.segment.minutes")) |
| | | : 5; |
| | | |
| | | // 获取计算方式配置 |
| | | String calculateMethod = configService.selectConfigByKey("gps.mileage.calculate.method"); |
| | | if (calculateMethod == null || calculateMethod.isEmpty()) { |
| | | calculateMethod = "tianditu"; |
| | | } |
| | | |
| | | // 获取是否跳过已计算GPS点的配置 |
| | | String skipCalculatedConfig = configService.selectConfigByKey("gps.mileage.skip.calculated"); |
| | | boolean skipCalculated = skipCalculatedConfig == null || "true".equalsIgnoreCase(skipCalculatedConfig); |
| | | |
| | | // 查询车辆在时间范围内的GPS数据 |
| | | List<VehicleGps> gpsList = vehicleGpsMapper.selectGpsDataByTimeRange(vehicleId, startTime, endTime); |
| | | |
| | | if (gpsList == null || gpsList.isEmpty()) { |
| | | logger.debug("车辆ID: {} 在时间范围 {} 到 {} 内无GPS数据", vehicleId, startTime, endTime); |
| | | return 0; |
| | | } |
| | | |
| | | logger.info("车辆ID: {} 查询到 {} 条GPS数据", vehicleId, gpsList.size()); |
| | | |
| | | // 按时间段分组GPS数据 |
| | | Map<Date, List<VehicleGps>> segmentedData = segmentGpsDataByTime(gpsList, segmentMinutes); |
| | | |
| | | int savedCount = 0; |
| | | VehicleGps previousSegmentLastPoint = null; // 记录上一个时间段的最后一个点 |
| | | |
| | | // 遍历每个时间段,计算里程 |
| | | for (Map.Entry<Date, List<VehicleGps>> entry : segmentedData.entrySet()) { |
| | | Date segmentStartTime = entry.getKey(); |
| | | List<VehicleGps> segmentGpsList = entry.getValue(); |
| | | |
| | | if (segmentGpsList.size() < 2) { |
| | | // 如果本段只有1个点,但有上一段的最后一个点,仍可计算跨段距离 |
| | | if (segmentGpsList.size() == 1 && previousSegmentLastPoint != null) { |
| | | // 保留当前点作为下一段的前置点,但不创建记录 |
| | | previousSegmentLastPoint = segmentGpsList.get(0); |
| | | } |
| | | continue; // 至少需要2个点才能计算距离 |
| | | } |
| | | |
| | | // 检查是否已存在该时间段的记录 |
| | | VehicleGpsSegmentMileage existing = segmentMileageMapper.selectByVehicleIdAndTime(vehicleId, segmentStartTime); |
| | | if (existing != null) { |
| | | logger.debug("车辆 {} 时间段 {} 的分段里程已存在,跳过", vehicleId, segmentStartTime); |
| | | // 更新上一段最后一个点 |
| | | previousSegmentLastPoint = segmentGpsList.get(segmentGpsList.size() - 1); |
| | | continue; |
| | | } |
| | | |
| | | // 计算时间段的结束时间 |
| | | Calendar cal = Calendar.getInstance(); |
| | | cal.setTime(segmentStartTime); |
| | | cal.add(Calendar.MINUTE, segmentMinutes); |
| | | Date segmentEndTime = cal.getTime(); |
| | | |
| | | // 计算该时间段的里程(包括跨段距离) |
| | | BigDecimal distance = calculateSegmentDistanceWithGap(segmentGpsList, calculateMethod, previousSegmentLastPoint); |
| | | |
| | | // 收集GPS ID列表(包括上一段的最后一个点,因为跨段间隙距离也用到了它) |
| | | List<Long> gpsIdList = new ArrayList<>(); |
| | | // 如果有上一段的最后一个点,先添加它的ID |
| | | if (previousSegmentLastPoint != null && previousSegmentLastPoint.getGpsId() != null) { |
| | | gpsIdList.add(previousSegmentLastPoint.getGpsId()); |
| | | } |
| | | // 再添加当前段的所有GPS点ID |
| | | for (VehicleGps gps : segmentGpsList) { |
| | | if (gps.getGpsId() != null) { |
| | | gpsIdList.add(gps.getGpsId()); |
| | | } |
| | | } |
| | | String gpsIds = gpsIdList.stream() |
| | | .map(String::valueOf) |
| | | .collect(java.util.stream.Collectors.joining(",")); |
| | | |
| | | // 创建分段里程记录 |
| | | VehicleGpsSegmentMileage segment = new VehicleGpsSegmentMileage(); |
| | | segment.setVehicleId(vehicleId); |
| | | |
| | | // 从GPS数据或车辆表获取车牌号 |
| | | String vehicleNo = segmentGpsList.get(0).getVehicleNo(); |
| | | if (vehicleNo == null || vehicleNo.trim().isEmpty()) { |
| | | // GPS数据中没有车牌号,从车辆表查询 |
| | | VehicleInfo vehicleInfo = vehicleInfoMapper.selectVehicleInfoById(vehicleId); |
| | | if (vehicleInfo != null) { |
| | | vehicleNo = vehicleInfo.getVehicleNo(); |
| | | } |
| | | } |
| | | segment.setVehicleNo(vehicleNo); |
| | | |
| | | segment.setSegmentStartTime(segmentStartTime); |
| | | segment.setSegmentEndTime(segmentEndTime); |
| | | |
| | | // 起点坐标 |
| | | VehicleGps firstPoint = segmentGpsList.get(0); |
| | | segment.setStartLongitude(BigDecimal.valueOf(firstPoint.getLongitude())); |
| | | segment.setStartLatitude(BigDecimal.valueOf(firstPoint.getLatitude())); |
| | | |
| | | // 终点坐标 |
| | | VehicleGps lastPoint = segmentGpsList.get(segmentGpsList.size() - 1); |
| | | segment.setEndLongitude(BigDecimal.valueOf(lastPoint.getLongitude())); |
| | | segment.setEndLatitude(BigDecimal.valueOf(lastPoint.getLatitude())); |
| | | |
| | | segment.setSegmentDistance(distance); |
| | | segment.setGpsPointCount(gpsIdList.size()); // GPS点数:包括边缘点 + 当前段的点 |
| | | segment.setGpsIds(gpsIds); // 设置GPS ID列表 |
| | | segment.setCalculateMethod(calculateMethod); |
| | | |
| | | // 查询并关联正在执行的任务 |
| | | associateActiveTask(segment, vehicleId, segmentStartTime, segmentEndTime); |
| | | |
| | | // 保存到数据库 |
| | | segmentMileageMapper.insertVehicleGpsSegmentMileage(segment); |
| | | |
| | | // 更新上一段最后一个点,供下一段使用 |
| | | previousSegmentLastPoint = segmentGpsList.get(segmentGpsList.size() - 1); |
| | | |
| | | // 记录已计算的GPS点到状态表(如果开启了重复计算控制) |
| | | if (skipCalculated && segment.getSegmentId() != null) { |
| | | for (Long gpsId : gpsIdList) { |
| | | try { |
| | | segmentMileageMapper.insertGpsCalculated(gpsId, segment.getSegmentId(), vehicleId); |
| | | } catch (Exception e) { |
| | | // 忽略重复键异常,继续处理 |
| | | logger.debug("记录GPS计算状态失败,可能已存在: gpsId={}", gpsId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | savedCount++; |
| | | |
| | | logger.debug("车辆 {} 时间段 {} 到 {} 里程: {}km, GPS点数: {}, GPS IDs: {}", |
| | | vehicleId, segmentStartTime, segmentEndTime, distance, segmentGpsList.size(), |
| | | gpsIds.length() > 50 ? gpsIds.substring(0, 50) + "..." : gpsIds); |
| | | } |
| | | |
| | | logger.info("车辆 {} 计算完成,保存了 {} 个时间段的里程数据", vehicleId, savedCount); |
| | | |
| | | // 自动触发汇总生成每日统计(如果有数据被保存) |
| | | if (savedCount > 0) { |
| | | try { |
| | | // 获取涉及的日期范围,触发汇总 |
| | | Set<Date> affectedDates = new HashSet<>(); |
| | | Calendar cal = Calendar.getInstance(); |
| | | |
| | | for (Map.Entry<Date, List<VehicleGps>> entry : segmentedData.entrySet()) { |
| | | cal.setTime(entry.getKey()); |
| | | cal.set(Calendar.HOUR_OF_DAY, 0); |
| | | cal.set(Calendar.MINUTE, 0); |
| | | cal.set(Calendar.SECOND, 0); |
| | | cal.set(Calendar.MILLISECOND, 0); |
| | | affectedDates.add(cal.getTime()); |
| | | } |
| | | |
| | | // 对每个涉及的日期,触发汇总 |
| | | for (Date statDate : affectedDates) { |
| | | try { |
| | | mileageStatsService.aggregateFromSegmentMileage(vehicleId, statDate); |
| | | logger.info("车辆 {} 日期 {} 的统计数据已自动汇总生成", vehicleId, statDate); |
| | | } catch (Exception e) { |
| | | logger.error("车辆 {} 日期 {} 自动汇总统计失败", vehicleId, statDate, e); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | logger.error("触发自动汇总失败", e); |
| | | } |
| | | } |
| | | |
| | | return savedCount; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("计算车辆 {} 分段里程失败", vehicleId, e); |
| | | throw new RuntimeException("计算分段里程失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将GPS数据按时间段分组 |
| | | */ |
| | | private Map<Date, List<VehicleGps>> segmentGpsDataByTime(List<VehicleGps> gpsList, int segmentMinutes) { |
| | | Map<Date, List<VehicleGps>> segmentedData = new LinkedHashMap<>(); |
| | | |
| | | for (VehicleGps gps : gpsList) { |
| | | // 解析GPS采集时间 |
| | | Date collectTime = parseDateTime(gps.getCollectTime()); |
| | | |
| | | // 计算该GPS点所属的时间段起始时间(向下取整到最近的时间段) |
| | | Date segmentStart = getSegmentStartTime(collectTime, segmentMinutes); |
| | | |
| | | // 添加到对应时间段 |
| | | segmentedData.computeIfAbsent(segmentStart, k -> new ArrayList<>()).add(gps); |
| | | } |
| | | |
| | | return segmentedData; |
| | | } |
| | | |
| | | /** |
| | | * 获取时间段的起始时间(向下取整) |
| | | */ |
| | | private Date getSegmentStartTime(Date time, int segmentMinutes) { |
| | | Calendar cal = Calendar.getInstance(); |
| | | cal.setTime(time); |
| | | |
| | | // 将分钟数向下取整到最近的分段 |
| | | int minute = cal.get(Calendar.MINUTE); |
| | | int segmentIndex = minute / segmentMinutes; |
| | | int alignedMinute = segmentIndex * segmentMinutes; |
| | | |
| | | cal.set(Calendar.MINUTE, alignedMinute); |
| | | cal.set(Calendar.SECOND, 0); |
| | | cal.set(Calendar.MILLISECOND, 0); |
| | | |
| | | return cal.getTime(); |
| | | } |
| | | |
| | | /** |
| | | * 计算一个时间段内的总里程(包括与上一段的间隙距离) |
| | | * @param gpsList 当前时间段的GPS点列表 |
| | | * @param calculateMethod 计算方式 |
| | | * @param previousLastPoint 上一个时间段的最后一个点(可为null) |
| | | */ |
| | | private BigDecimal calculateSegmentDistanceWithGap(List<VehicleGps> gpsList, String calculateMethod, VehicleGps previousLastPoint) { |
| | | if (gpsList == null || gpsList.size() < 2) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | BigDecimal totalDistance = BigDecimal.ZERO; |
| | | |
| | | // 1. 先计算跨段间隙距离(上一段最后一个点 -> 当前段第一个点) |
| | | if (previousLastPoint != null) { |
| | | VehicleGps currentFirstPoint = gpsList.get(0); |
| | | double gapDistance = calculateHaversineDistance( |
| | | previousLastPoint.getLatitude().doubleValue(), |
| | | previousLastPoint.getLongitude().doubleValue(), |
| | | currentFirstPoint.getLatitude().doubleValue(), |
| | | currentFirstPoint.getLongitude().doubleValue() |
| | | ); |
| | | totalDistance = totalDistance.add(BigDecimal.valueOf(gapDistance)); |
| | | |
| | | logger.debug("跨段间隙距离: {}km (上一段末点 -> 当前段首点)", |
| | | String.format("%.3f", gapDistance)); |
| | | } |
| | | |
| | | // 2. 再计算当前段内部的距离 |
| | | BigDecimal segmentInternalDistance; |
| | | if ("tianditu".equalsIgnoreCase(calculateMethod)) { |
| | | segmentInternalDistance = calculateDistanceByTianditu(gpsList); |
| | | } else { |
| | | segmentInternalDistance = calculateDistanceByHaversine(gpsList); |
| | | } |
| | | totalDistance = totalDistance.add(segmentInternalDistance); |
| | | |
| | | return totalDistance.setScale(3, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 计算一个时间段内的总里程(仅段内距离) |
| | | */ |
| | | private BigDecimal calculateSegmentDistance(List<VehicleGps> gpsList, String calculateMethod) { |
| | | if (gpsList == null || gpsList.size() < 2) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | | BigDecimal totalDistance = BigDecimal.ZERO; |
| | | |
| | | if ("tianditu".equalsIgnoreCase(calculateMethod)) { |
| | | // 使用天地图API计算(批量计算更精确) |
| | | totalDistance = calculateDistanceByTianditu(gpsList); |
| | | } else { |
| | | // 使用Haversine公式计算(直线距离,更快但不够精确) |
| | | totalDistance = calculateDistanceByHaversine(gpsList); |
| | | } |
| | | |
| | | return totalDistance.setScale(3, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 使用天地图API计算距离 |
| | | */ |
| | | private BigDecimal calculateDistanceByTianditu(List<VehicleGps> gpsList) { |
| | | try { |
| | | // 天地图路径规划API有点数限制,如果点太多需要分批处理 |
| | | int maxPointsPerRequest = 50; // 天地图API建议不超过50个点 |
| | | BigDecimal totalDistance = BigDecimal.ZERO; |
| | | |
| | | // 如果GPS点数较少,直接使用Haversine公式(避免频繁调用API) |
| | | if (gpsList.size() <= 3) { |
| | | return calculateDistanceByHaversine(gpsList); |
| | | } |
| | | |
| | | // 分批处理 |
| | | for (int i = 0; i < gpsList.size() - 1; i += maxPointsPerRequest) { |
| | | int endIndex = Math.min(i + maxPointsPerRequest, gpsList.size()); |
| | | List<VehicleGps> batchList = gpsList.subList(i, endIndex); |
| | | |
| | | BigDecimal batchDistance = calculateBatchDistanceByTianditu(batchList); |
| | | totalDistance = totalDistance.add(batchDistance); |
| | | } |
| | | |
| | | return totalDistance; |
| | | |
| | | } catch (Exception e) { |
| | | logger.warn("天地图API计算距离失败,降级使用Haversine公式: {}", e.getMessage()); |
| | | return calculateDistanceByHaversine(gpsList); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 使用天地图API计算一批GPS点的距离 |
| | | */ |
| | | private BigDecimal calculateBatchDistanceByTianditu(List<VehicleGps> gpsList) { |
| | | try { |
| | | // 简化处理:计算相邻点之间的直线距离总和 |
| | | // 注:天地图的路径规划API主要用于导航,这里用简化的距离计算 |
| | | BigDecimal totalDistance = BigDecimal.ZERO; |
| | | |
| | | for (int i = 0; i < gpsList.size() - 1; i++) { |
| | | VehicleGps p1 = gpsList.get(i); |
| | | VehicleGps p2 = gpsList.get(i + 1); |
| | | |
| | | double distance = calculateHaversineDistance( |
| | | p1.getLatitude().doubleValue(), |
| | | p1.getLongitude().doubleValue(), |
| | | p2.getLatitude().doubleValue(), |
| | | p2.getLongitude().doubleValue() |
| | | ); |
| | | |
| | | totalDistance = totalDistance.add(BigDecimal.valueOf(distance)); |
| | | } |
| | | |
| | | return totalDistance; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("天地图批量距离计算失败", e); |
| | | throw e; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 使用Haversine公式计算距离 |
| | | */ |
| | | private BigDecimal calculateDistanceByHaversine(List<VehicleGps> gpsList) { |
| | | BigDecimal totalDistance = BigDecimal.ZERO; |
| | | |
| | | for (int i = 0; i < gpsList.size() - 1; i++) { |
| | | VehicleGps p1 = gpsList.get(i); |
| | | VehicleGps p2 = gpsList.get(i + 1); |
| | | |
| | | double distance = calculateHaversineDistance( |
| | | p1.getLatitude().doubleValue(), |
| | | p1.getLongitude().doubleValue(), |
| | | p2.getLatitude().doubleValue(), |
| | | p2.getLongitude().doubleValue() |
| | | ); |
| | | |
| | | totalDistance = totalDistance.add(BigDecimal.valueOf(distance)); |
| | | } |
| | | |
| | | return totalDistance; |
| | | } |
| | | |
| | | /** |
| | | * 使用Haversine公式计算两点之间的距离(公里) |
| | | */ |
| | | private double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) { |
| | | // 如果起点和终点经纬度相同,直接返回0,避免不必要的计算 |
| | | if (lat1 == lat2 && lon1 == lon2) { |
| | | return 0.0; |
| | | } |
| | | |
| | | // 将角度转换为弧度 |
| | | double dLat = Math.toRadians(lat2 - lat1); |
| | | double dLon = Math.toRadians(lon2 - lon1); |
| | | double rLat1 = Math.toRadians(lat1); |
| | | double rLat2 = Math.toRadians(lat2); |
| | | |
| | | // Haversine公式 |
| | | double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + |
| | | Math.cos(rLat1) * Math.cos(rLat2) * |
| | | Math.sin(dLon / 2) * Math.sin(dLon / 2); |
| | | |
| | | double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
| | | |
| | | return EARTH_RADIUS_KM * c; |
| | | } |
| | | |
| | | /** |
| | | * 解析日期时间字符串 |
| | | */ |
| | | private Date parseDateTime(String dateTimeStr) { |
| | | if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) { |
| | | throw new RuntimeException("日期时间字符串不能为空"); |
| | | } |
| | | |
| | | try { |
| | | java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | sdf.setLenient(false); |
| | | return sdf.parse(dateTimeStr.trim()); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException("日期时间格式错误: " + dateTimeStr + ", 应为 yyyy-MM-dd HH:mm:ss", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询并关联车辆正在执行的任务 |
| | | * @param segment 分段里程记录 |
| | | * @param vehicleId 车辆ID |
| | | * @param segmentStartTime 时间段开始时间 |
| | | * @param segmentEndTime 时间段结束时间 |
| | | */ |
| | | private void associateActiveTask(VehicleGpsSegmentMileage segment, Long vehicleId, |
| | | Date segmentStartTime, Date segmentEndTime) { |
| | | try { |
| | | // 查询该车辆正在执行的任务列表 |
| | | List<SysTask> activeTasks = sysTaskMapper.selectActiveTasksByVehicleId(vehicleId); |
| | | |
| | | if (activeTasks == null || activeTasks.isEmpty()) { |
| | | logger.debug("车辆 {} 在时间段 {} - {} 没有正在执行的任务", vehicleId, segmentStartTime, segmentEndTime); |
| | | return; |
| | | } |
| | | |
| | | // 遍历任务,查找与当前时间段有重叠的任务 |
| | | for (SysTask task : activeTasks) { |
| | | // 获取任务的实际执行时间,如果没有实际时间则使用计划时间 |
| | | Date taskStart = task.getActualStartTime() != null ? task.getActualStartTime() : task.getPlannedStartTime(); |
| | | Date taskEnd = task.getActualEndTime() != null ? task.getActualEndTime() : task.getPlannedEndTime(); |
| | | |
| | | // 判断时间段是否有重叠 |
| | | if (isTimeOverlap(segmentStartTime, segmentEndTime, taskStart, taskEnd)) { |
| | | // 关联任务ID和任务编号 |
| | | segment.setTaskId(task.getTaskId()); |
| | | segment.setTaskCode(task.getTaskCode()); |
| | | |
| | | logger.debug("车辆 {} 时间段 {} - {} 关联任务: taskId={}, taskCode={}", |
| | | vehicleId, segmentStartTime, segmentEndTime, task.getTaskId(), task.getTaskCode()); |
| | | break; // 找到一个匹配的任务即可 |
| | | } |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | // 关联任务失败不影响主流程,只记录日志 |
| | | logger.warn("关联车辆 {} 的任务信息失败", vehicleId, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断两个时间段是否有重叠 |
| | | * @param start1 时间段1开始 |
| | | * @param end1 时间段1结束 |
| | | * @param start2 时间段2开始 |
| | | * @param end2 时间段2结束 |
| | | * @return true-有重叠, false-无重叠 |
| | | */ |
| | | private boolean isTimeOverlap(Date start1, Date end1, Date start2, Date end2) { |
| | | // 任何时间为null,返回false |
| | | if (start1 == null || end1 == null || start2 == null || end2 == null) { |
| | | return false; |
| | | } |
| | | |
| | | // 两个时间段有重叠的条件: |
| | | // start1 < end2 && end1 > start2 |
| | | return start1.before(end2) && end1.after(start2); |
| | | } |
| | | } |
| | |
| | | */ |
| | | @Override |
| | | public VehicleInfo selectVehicleInfoById(Long vehicleId) { |
| | | return vehicleInfoMapper.selectVehicleInfoById(vehicleId); |
| | | return vehicleInfoMapper.selectVehicleInfoWithDeptsById(vehicleId); |
| | | } |
| | | |
| | | /** |
| | | * 查询车辆信息(包含多分公司关联) |
| | | * |
| | | * @param vehicleId 车辆信息主键 |
| | | * @return 车辆信息(包含deptIds和deptNames) |
| | | */ |
| | | @Override |
| | | public VehicleInfo selectVehicleInfoWithDeptsById(Long vehicleId) { |
| | | VehicleInfo vehicle = vehicleInfoMapper.selectVehicleInfoWithDeptsById(vehicleId); |
| | | // 初始化延迟加载的属性,避免序列化问题 |
| | | if (vehicle != null) { |
| | | vehicle.initializeLazyProperties(); |
| | | } |
| | | return vehicle; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | @Override |
| | | public List<VehicleInfo> selectVehicleInfoList(VehicleInfo vehicleInfo) { |
| | | return vehicleInfoMapper.selectVehicleInfoList(vehicleInfo); |
| | | List<VehicleInfo> list = vehicleInfoMapper.selectVehicleInfoListWithDepts(vehicleInfo); |
| | | // 初始化延迟加载的属性,避免序列化问题 |
| | | for (VehicleInfo vehicle : list) { |
| | | vehicle.initializeLazyProperties(); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.text.ParseException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Calendar; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import com.ruoyi.system.domain.VehicleGps; |
| | | import com.ruoyi.system.domain.VehicleMileageStats; |
| | | import com.ruoyi.system.domain.VehicleGpsSegmentMileage; |
| | | import com.ruoyi.system.domain.TaskTimeInterval; |
| | | import com.ruoyi.system.domain.VehicleInfo; |
| | | import com.ruoyi.system.mapper.VehicleGpsMapper; |
| | | import com.ruoyi.system.mapper.VehicleMileageStatsMapper; |
| | | import com.ruoyi.system.mapper.VehicleGpsSegmentMileageMapper; |
| | | import com.ruoyi.system.mapper.VehicleInfoMapper; |
| | | import com.ruoyi.system.service.IVehicleMileageStatsService; |
| | | |
| | | /** |
| | |
| | | |
| | | @Autowired |
| | | private VehicleGpsMapper vehicleGpsMapper; |
| | | |
| | | @Autowired |
| | | private VehicleGpsSegmentMileageMapper segmentMileageMapper; |
| | | |
| | | @Autowired |
| | | private VehicleInfoMapper vehicleInfoMapper; |
| | | |
| | | /** |
| | | * 查询车辆里程统计 |
| | |
| | | stats.setVehicleId(vehicleId); |
| | | stats.setStatDate(statDate); |
| | | |
| | | // 获取车牌号 |
| | | // 获取车牌号:优先从GPS数据,如果没有则从车辆表查询 |
| | | String vehicleNo = null; |
| | | if (!gpsList.isEmpty() && gpsList.get(0).getVehicleNo() != null) { |
| | | stats.setVehicleNo(gpsList.get(0).getVehicleNo()); |
| | | vehicleNo = gpsList.get(0).getVehicleNo(); |
| | | } |
| | | if (vehicleNo == null || vehicleNo.trim().isEmpty()) { |
| | | VehicleInfo vehicleInfo = vehicleInfoMapper.selectVehicleInfoById(vehicleId); |
| | | if (vehicleInfo != null) { |
| | | vehicleNo = vehicleInfo.getVehicleNo(); |
| | | } |
| | | } |
| | | stats.setVehicleNo(vehicleNo); |
| | | } |
| | | |
| | | // 6. 设置统计数据 |
| | |
| | | ); |
| | | |
| | | // 获取这段距离的时间区间 |
| | | Date segmentStart = p1.getCollectTime(); |
| | | Date segmentEnd = p2.getCollectTime(); |
| | | Date segmentStart = parseDateTime(p1.getCollectTime()); |
| | | Date segmentEnd = parseDateTime(p2.getCollectTime()); |
| | | |
| | | // 计算这段距离在任务时段的占比 |
| | | double taskRatio = calculateTaskOverlapRatio(segmentStart, segmentEnd, taskIntervals); |
| | |
| | | * 使用Haversine公式计算两个GPS坐标之间的距离(公里) |
| | | */ |
| | | private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { |
| | | // 如果起点和终点经纬度相同,直接返回0,避免不必要的计算 |
| | | if (lat1 == lat2 && lon1 == lon2) { |
| | | return 0.0; |
| | | } |
| | | |
| | | // 将角度转换为弧度 |
| | | double dLat = Math.toRadians(lat2 - lat1); |
| | | double dLon = Math.toRadians(lon2 - lon1); |
| | |
| | | BigDecimal nonTaskMileage = BigDecimal.ZERO; |
| | | BigDecimal taskRatio = BigDecimal.ZERO; |
| | | } |
| | | |
| | | /** |
| | | * 解析日期时间字符串 |
| | | * |
| | | * @param dateTimeStr 日期时间字符串,格式:yyyy-MM-dd HH:mm:ss |
| | | * @return Date对象 |
| | | * @throws RuntimeException 如果解析失败 |
| | | */ |
| | | private Date parseDateTime(String dateTimeStr) { |
| | | if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) { |
| | | throw new RuntimeException("日期时间字符串不能为空"); |
| | | } |
| | | |
| | | try { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | sdf.setLenient(false); |
| | | return sdf.parse(dateTimeStr.trim()); |
| | | } catch (ParseException e) { |
| | | throw new RuntimeException("日期时间格式错误: " + dateTimeStr + ", 应为 yyyy-MM-dd HH:mm:ss", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从分段里程数据汇总生成按日统计 |
| | | */ |
| | | @Override |
| | | public VehicleMileageStats aggregateFromSegmentMileage(Long vehicleId, Date statDate) { |
| | | try { |
| | | // 1. 获取统计日期的开始和结束时间 |
| | | Calendar calendar = Calendar.getInstance(); |
| | | calendar.setTime(statDate); |
| | | calendar.set(Calendar.HOUR_OF_DAY, 0); |
| | | calendar.set(Calendar.MINUTE, 0); |
| | | calendar.set(Calendar.SECOND, 0); |
| | | calendar.set(Calendar.MILLISECOND, 0); |
| | | Date dayStart = calendar.getTime(); |
| | | |
| | | calendar.add(Calendar.DAY_OF_MONTH, 1); |
| | | Date dayEnd = calendar.getTime(); |
| | | |
| | | // 2. 查询该日期范围内的所有分段里程数据 |
| | | List<VehicleGpsSegmentMileage> segments = segmentMileageMapper.selectSegmentsByDateRange(vehicleId, dayStart, dayEnd); |
| | | |
| | | if (segments == null || segments.isEmpty()) { |
| | | logger.info("车辆ID: {} 在日期: {} 无分段里程数据", vehicleId, statDate); |
| | | return null; |
| | | } |
| | | |
| | | // 3. 汇总里程数据 |
| | | BigDecimal totalMileage = BigDecimal.ZERO; |
| | | int totalGpsPoints = 0; |
| | | |
| | | for (VehicleGpsSegmentMileage segment : segments) { |
| | | if (segment.getSegmentDistance() != null) { |
| | | totalMileage = totalMileage.add(segment.getSegmentDistance()); |
| | | } |
| | | if (segment.getGpsPointCount() != null) { |
| | | totalGpsPoints += segment.getGpsPointCount(); |
| | | } |
| | | } |
| | | |
| | | // 4. 查询该日期的任务时间区间,计算任务里程和非任务里程 |
| | | List<TaskTimeInterval> taskIntervals = vehicleMileageStatsMapper.selectTaskTimeIntervals(vehicleId, dayStart, dayEnd); |
| | | |
| | | BigDecimal taskMileage = BigDecimal.ZERO; |
| | | BigDecimal nonTaskMileage = BigDecimal.ZERO; |
| | | |
| | | for (VehicleGpsSegmentMileage segment : segments) { |
| | | Date segStart = segment.getSegmentStartTime(); |
| | | Date segEnd = segment.getSegmentEndTime(); |
| | | BigDecimal segDistance = segment.getSegmentDistance() != null ? segment.getSegmentDistance() : BigDecimal.ZERO; |
| | | |
| | | // 计算该分段与任务时段的重叠比例 |
| | | double taskRatio = calculateTaskOverlapRatio(segStart, segEnd, taskIntervals); |
| | | |
| | | // 分摇里程 |
| | | BigDecimal taskDist = segDistance.multiply(BigDecimal.valueOf(taskRatio)); |
| | | BigDecimal nonTaskDist = segDistance.multiply(BigDecimal.valueOf(1 - taskRatio)); |
| | | |
| | | taskMileage = taskMileage.add(taskDist); |
| | | nonTaskMileage = nonTaskMileage.add(nonTaskDist); |
| | | } |
| | | |
| | | // 计算任务里程占比 |
| | | BigDecimal taskRatio = BigDecimal.ZERO; |
| | | if (totalMileage.compareTo(BigDecimal.ZERO) > 0) { |
| | | taskRatio = taskMileage.divide(totalMileage, 4, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | // 5. 查询或创建统计记录 |
| | | VehicleMileageStats stats = vehicleMileageStatsMapper.selectByVehicleIdAndDate(vehicleId, statDate); |
| | | boolean isNew = (stats == null); |
| | | |
| | | if (isNew) { |
| | | stats = new VehicleMileageStats(); |
| | | stats.setVehicleId(vehicleId); |
| | | stats.setStatDate(statDate); |
| | | |
| | | // 获取车牌号:优先从分段数据,如果没有则从车辆表查询 |
| | | String vehicleNo = null; |
| | | if (!segments.isEmpty() && segments.get(0).getVehicleNo() != null) { |
| | | vehicleNo = segments.get(0).getVehicleNo(); |
| | | } |
| | | if (vehicleNo == null || vehicleNo.trim().isEmpty()) { |
| | | VehicleInfo vehicleInfo = vehicleInfoMapper.selectVehicleInfoById(vehicleId); |
| | | if (vehicleInfo != null) { |
| | | vehicleNo = vehicleInfo.getVehicleNo(); |
| | | } |
| | | } |
| | | stats.setVehicleNo(vehicleNo); |
| | | } |
| | | |
| | | // 6. 设置统计数据 |
| | | stats.setTotalMileage(totalMileage.setScale(2, RoundingMode.HALF_UP)); |
| | | stats.setTaskMileage(taskMileage.setScale(2, RoundingMode.HALF_UP)); |
| | | stats.setNonTaskMileage(nonTaskMileage.setScale(2, RoundingMode.HALF_UP)); |
| | | stats.setTaskRatio(taskRatio); |
| | | stats.setGpsPointCount(totalGpsPoints); |
| | | stats.setTaskCount(taskIntervals == null ? 0 : taskIntervals.size()); |
| | | stats.setSegmentCount(segments.size()); |
| | | stats.setDataSource("segment"); // 标记数据来源为分段汇总 |
| | | |
| | | // 7. 保存到数据库 |
| | | if (isNew) { |
| | | vehicleMileageStatsMapper.insertVehicleMileageStats(stats); |
| | | } else { |
| | | vehicleMileageStatsMapper.updateVehicleMileageStats(stats); |
| | | } |
| | | |
| | | logger.info("车辆ID: {} 日期: {} 从分段汇总完成 - 总里程: {}km, 任务里程: {}km, 非任务里程: {}km, 分段数: {}", |
| | | vehicleId, statDate, totalMileage, taskMileage, nonTaskMileage, segments.size()); |
| | | |
| | | return stats; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("从分段汇总里程统计失败 - 车辆ID: {}, 日期: {}", vehicleId, statDate, e); |
| | | throw new RuntimeException("汇总里程统计失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 批量从分段里程汇总生成按日统计 |
| | | */ |
| | | @Override |
| | | public int batchAggregateFromSegmentMileage(Date statDate) { |
| | | try { |
| | | // 查询所有活跃车辆 |
| | | List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(); |
| | | |
| | | if (vehicleIds == null || vehicleIds.isEmpty()) { |
| | | logger.info("没有找到活跃车辆"); |
| | | return 0; |
| | | } |
| | | |
| | | int successCount = 0; |
| | | for (Long vehicleId : vehicleIds) { |
| | | try { |
| | | aggregateFromSegmentMileage(vehicleId, statDate); |
| | | successCount++; |
| | | } catch (Exception e) { |
| | | logger.error("汇总车辆 {} 的里程统计失败", vehicleId, e); |
| | | } |
| | | } |
| | | |
| | | logger.info("批量里程汇总完成 - 日期: {}, 总车辆数: {}, 成功: {}", statDate, vehicleIds.size(), successCount); |
| | | return successCount; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("批量汇总里程统计失败 - 日期: {}", statDate, e); |
| | | throw new RuntimeException("批量汇总失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | where collect_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) |
| | | order by vehicle_id |
| | | </select> |
| | | |
| | | <!-- 查询未被计算的GPS坐标(不在tb_vehicle_gps_calculated表中的记录) --> |
| | | <select id="selectUncalculatedGps" resultMap="VehicleGpsResult"> |
| | | SELECT g.gps_id, g.vehicle_id, g.device_id, g.longitude, g.latitude, g.altitude, |
| | | g.speed, g.direction, g.collect_time, g.device_report_time, |
| | | g.platform_process_time, g.create_time, v.vehicle_no |
| | | FROM tb_vehicle_gps g |
| | | LEFT JOIN tb_vehicle_info v ON g.vehicle_id = v.vehicle_id |
| | | LEFT JOIN tb_vehicle_gps_calculated c ON g.gps_id = c.gps_id |
| | | WHERE g.vehicle_id = #{vehicleId} |
| | | AND g.collect_time >= #{startTime} |
| | | AND g.collect_time <= #{endTime} |
| | | AND c.gps_id IS NULL -- 未被计算的GPS点 |
| | | ORDER BY g.collect_time |
| | | </select> |
| | | </mapper> |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8" ?> |
| | | <!DOCTYPE mapper |
| | | PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
| | | "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.ruoyi.system.mapper.VehicleGpsSegmentMileageMapper"> |
| | | |
| | | <resultMap type="VehicleGpsSegmentMileage" id="VehicleGpsSegmentMileageResult"> |
| | | <result property="segmentId" column="segment_id" /> |
| | | <result property="vehicleId" column="vehicle_id" /> |
| | | <result property="vehicleNo" column="vehicle_no" /> |
| | | <result property="segmentStartTime" column="segment_start_time" /> |
| | | <result property="segmentEndTime" column="segment_end_time" /> |
| | | <result property="startLongitude" column="start_longitude" /> |
| | | <result property="startLatitude" column="start_latitude" /> |
| | | <result property="endLongitude" column="end_longitude" /> |
| | | <result property="endLatitude" column="end_latitude" /> |
| | | <result property="segmentDistance" column="segment_distance" /> |
| | | <result property="gpsPointCount" column="gps_point_count" /> |
| | | <result property="gpsIds" column="gps_ids" /> |
| | | <result property="taskId" column="task_id" /> |
| | | <result property="taskCode" column="task_code" /> |
| | | <result property="calculateMethod" column="calculate_method" /> |
| | | <result property="createTime" column="create_time" /> |
| | | <result property="updateTime" column="update_time" /> |
| | | </resultMap> |
| | | |
| | | <sql id="selectVehicleGpsSegmentMileageVo"> |
| | | SELECT segment_id, vehicle_id, vehicle_no, segment_start_time, segment_end_time, |
| | | start_longitude, start_latitude, end_longitude, end_latitude, |
| | | segment_distance, gps_point_count, gps_ids, task_id, task_code, |
| | | calculate_method, create_time, update_time |
| | | FROM tb_vehicle_gps_segment_mileage |
| | | </sql> |
| | | |
| | | <select id="selectVehicleGpsSegmentMileageList" parameterType="VehicleGpsSegmentMileage" resultMap="VehicleGpsSegmentMileageResult"> |
| | | <include refid="selectVehicleGpsSegmentMileageVo"/> |
| | | <where> |
| | | <if test="vehicleId != null"> |
| | | AND vehicle_id = #{vehicleId} |
| | | </if> |
| | | <if test="vehicleNo != null and vehicleNo != ''"> |
| | | AND vehicle_no = #{vehicleNo} |
| | | </if> |
| | | <if test="params.beginTime != null and params.beginTime != ''"> |
| | | AND segment_start_time >= #{params.beginTime} |
| | | </if> |
| | | <if test="params.endTime != null and params.endTime != ''"> |
| | | AND segment_end_time <= #{params.endTime} |
| | | </if> |
| | | </where> |
| | | ORDER BY segment_start_time DESC |
| | | </select> |
| | | |
| | | <select id="selectVehicleGpsSegmentMileageById" parameterType="Long" resultMap="VehicleGpsSegmentMileageResult"> |
| | | <include refid="selectVehicleGpsSegmentMileageVo"/> |
| | | WHERE segment_id = #{segmentId} |
| | | </select> |
| | | |
| | | <select id="selectByVehicleIdAndTime" resultMap="VehicleGpsSegmentMileageResult"> |
| | | <include refid="selectVehicleGpsSegmentMileageVo"/> |
| | | WHERE vehicle_id = #{vehicleId} AND segment_start_time = #{segmentStartTime} |
| | | </select> |
| | | |
| | | <select id="selectSegmentsByDateRange" resultMap="VehicleGpsSegmentMileageResult"> |
| | | <include refid="selectVehicleGpsSegmentMileageVo"/> |
| | | WHERE vehicle_id = #{vehicleId} |
| | | AND segment_start_time >= #{startDate} |
| | | AND segment_end_time <= #{endDate} |
| | | ORDER BY segment_start_time |
| | | </select> |
| | | |
| | | <!-- 按任务ID查询分段里程列表 --> |
| | | <select id="selectSegmentsByTaskId" resultMap="VehicleGpsSegmentMileageResult"> |
| | | <include refid="selectVehicleGpsSegmentMileageVo"/> |
| | | WHERE task_id = #{taskId} |
| | | ORDER BY segment_start_time |
| | | </select> |
| | | |
| | | <!-- 查询任务的总里程(直接求和) --> |
| | | <select id="selectTotalMileageByTaskId" resultType="java.math.BigDecimal"> |
| | | SELECT COALESCE(SUM(segment_distance), 0) |
| | | FROM tb_vehicle_gps_segment_mileage |
| | | WHERE task_id = #{taskId} |
| | | </select> |
| | | |
| | | <insert id="insertVehicleGpsSegmentMileage" parameterType="VehicleGpsSegmentMileage"> |
| | | INSERT INTO tb_vehicle_gps_segment_mileage |
| | | <trim prefix="(" suffix=")" suffixOverrides=","> |
| | | <if test="vehicleId != null">vehicle_id,</if> |
| | | <if test="vehicleNo != null">vehicle_no,</if> |
| | | <if test="segmentStartTime != null">segment_start_time,</if> |
| | | <if test="segmentEndTime != null">segment_end_time,</if> |
| | | <if test="startLongitude != null">start_longitude,</if> |
| | | <if test="startLatitude != null">start_latitude,</if> |
| | | <if test="endLongitude != null">end_longitude,</if> |
| | | <if test="endLatitude != null">end_latitude,</if> |
| | | <if test="segmentDistance != null">segment_distance,</if> |
| | | <if test="gpsPointCount != null">gps_point_count,</if> |
| | | <if test="gpsIds != null">gps_ids,</if> |
| | | <if test="taskId != null">task_id,</if> |
| | | <if test="taskCode != null">task_code,</if> |
| | | <if test="calculateMethod != null">calculate_method,</if> |
| | | </trim> |
| | | <trim prefix="values (" suffix=")" suffixOverrides=","> |
| | | <if test="vehicleId != null">#{vehicleId},</if> |
| | | <if test="vehicleNo != null">#{vehicleNo},</if> |
| | | <if test="segmentStartTime != null">#{segmentStartTime},</if> |
| | | <if test="segmentEndTime != null">#{segmentEndTime},</if> |
| | | <if test="startLongitude != null">#{startLongitude},</if> |
| | | <if test="startLatitude != null">#{startLatitude},</if> |
| | | <if test="endLongitude != null">#{endLongitude},</if> |
| | | <if test="endLatitude != null">#{endLatitude},</if> |
| | | <if test="segmentDistance != null">#{segmentDistance},</if> |
| | | <if test="gpsPointCount != null">#{gpsPointCount},</if> |
| | | <if test="gpsIds != null">#{gpsIds},</if> |
| | | <if test="taskId != null">#{taskId},</if> |
| | | <if test="taskCode != null">#{taskCode},</if> |
| | | <if test="calculateMethod != null">#{calculateMethod},</if> |
| | | </trim> |
| | | </insert> |
| | | |
| | | <update id="updateVehicleGpsSegmentMileage" parameterType="VehicleGpsSegmentMileage"> |
| | | UPDATE tb_vehicle_gps_segment_mileage |
| | | <trim prefix="SET" suffixOverrides=","> |
| | | <if test="vehicleNo != null">vehicle_no = #{vehicleNo},</if> |
| | | <if test="segmentEndTime != null">segment_end_time = #{segmentEndTime},</if> |
| | | <if test="endLongitude != null">end_longitude = #{endLongitude},</if> |
| | | <if test="endLatitude != null">end_latitude = #{endLatitude},</if> |
| | | <if test="segmentDistance != null">segment_distance = #{segmentDistance},</if> |
| | | <if test="gpsPointCount != null">gps_point_count = #{gpsPointCount},</if> |
| | | <if test="calculateMethod != null">calculate_method = #{calculateMethod},</if> |
| | | </trim> |
| | | WHERE segment_id = #{segmentId} |
| | | </update> |
| | | |
| | | <delete id="deleteVehicleGpsSegmentMileageById" parameterType="Long"> |
| | | DELETE FROM tb_vehicle_gps_segment_mileage WHERE segment_id = #{segmentId} |
| | | </delete> |
| | | |
| | | <delete id="deleteVehicleGpsSegmentMileageByIds" parameterType="String"> |
| | | DELETE FROM tb_vehicle_gps_segment_mileage WHERE segment_id IN |
| | | <foreach item="segmentId" collection="array" open="(" separator="," close=")"> |
| | | #{segmentId} |
| | | </foreach> |
| | | </delete> |
| | | |
| | | <!-- 记录GPS点已被计算 --> |
| | | <insert id="insertGpsCalculated"> |
| | | INSERT INTO tb_vehicle_gps_calculated (gps_id, segment_id, vehicle_id, create_time) |
| | | VALUES (#{gpsId}, #{segmentId}, #{vehicleId}, NOW()) |
| | | </insert> |
| | | |
| | | <!-- 检查GPS点是否已被计算 --> |
| | | <select id="selectGpsCalculatedSegmentId" resultType="Long"> |
| | | SELECT segment_id FROM tb_vehicle_gps_calculated WHERE gps_id = #{gpsId} LIMIT 1 |
| | | </select> |
| | | </mapper> |
| | |
| | | <result property="vehicleModel" column="vehicle_model" /> |
| | | <result property="status" column="status" /> |
| | | <result property="platformCode" column="platform_code" /> |
| | | <result property="deptId" column="dept_id" /> |
| | | <result property="deptName" column="dept_name" /> |
| | | <result property="createBy" column="create_by" /> |
| | | <result property="createTime" column="create_time" /> |
| | | <result property="updateBy" column="update_by" /> |
| | | <result property="updateTime" column="update_time" /> |
| | | <result property="remark" column="remark" /> |
| | | <!-- 多个分公司关联 --> |
| | | </resultMap> |
| | | |
| | | <!-- 包含多分公司关联的完整结果映射(仅在需要时使用) --> |
| | | <resultMap type="com.ruoyi.system.domain.VehicleInfo" id="VehicleInfoWithDeptsResult" extends="VehicleInfoResult"> |
| | | <!-- 多个分公司关联(立即加载,避免延迟加载导致的序列化问题) --> |
| | | <collection property="deptIds" ofType="Long" |
| | | select="selectVehicleDeptIds" |
| | | column="vehicle_id"/> |
| | | column="vehicle_id" |
| | | fetchType="eager"/> |
| | | <collection property="deptNames" ofType="String" |
| | | select="selectVehicleDeptNames" |
| | | column="vehicle_id"/> |
| | | column="vehicle_id" |
| | | fetchType="eager"/> |
| | | </resultMap> |
| | | |
| | | <sql id="selectVehicleInfoVo"> |
| | | select v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, v.vehicle_model, v.status, v.platform_code, v.dept_id, d.dept_name, v.create_by, v.create_time, v.update_by, v.update_time, v.remark |
| | | select v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, v.vehicle_model, v.status, v.platform_code, v.create_by, v.create_time, v.update_by, v.update_time, v.remark |
| | | from tb_vehicle_info v |
| | | left join sys_dept d on v.dept_id = d.dept_id |
| | | </sql> |
| | | |
| | | <!-- 查询车辆关联的所有分公司ID --> |
| | |
| | | WHERE vd.vehicle_id = #{vehicle_id} |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoList" parameterType="VehicleInfo" resultMap="VehicleInfoResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | <select id="selectVehicleInfoListWithDepts" parameterType="VehicleInfo" resultMap="VehicleInfoWithDeptsResult"> |
| | | select v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, |
| | | v.vehicle_model, v.status, v.platform_code, v.create_by, v.create_time, |
| | | v.update_by, v.update_time, v.remark |
| | | from tb_vehicle_info v |
| | | <where> |
| | | <if test="vehicleNo != null and vehicleNo != ''"> and v.vehicle_no = #{vehicleNo}</if> |
| | | <if test="vehicleNo != null and vehicleNo != ''"> and v.vehicle_no LIKE concat('%', #{vehicleNo}, '%')</if> |
| | | <if test="deviceId != null and deviceId != ''"> and v.device_id = #{deviceId}</if> |
| | | <if test="vehicleType != null and vehicleType != ''"> and v.vehicle_type = #{vehicleType}</if> |
| | | <if test="vehicleBrand != null and vehicleBrand != ''"> and v.vehicle_brand = #{vehicleBrand}</if> |
| | |
| | | <!-- 部门过滤:根据分公司ID查询车辆(通过关联表) --> |
| | | <if test="deptId != null"> |
| | | and EXISTS ( |
| | | SELECT 1 FROM tb_vehicle_dept vd |
| | | WHERE vd.vehicle_id = v.vehicle_id |
| | | AND vd.dept_id = #{deptId} |
| | | SELECT 1 FROM tb_vehicle_dept vd2 |
| | | WHERE vd2.vehicle_id = v.vehicle_id |
| | | AND vd2.dept_id = #{deptId} |
| | | ) |
| | | </if> |
| | | <!-- 任务车辆选择必须过滤:只显示car_id和dept_id都不为空的车辆 --> |
| | | and v.car_id is not null and v.car_id != '' |
| | | and v.dept_id is not null |
| | | and v.status=0 |
| | | </where> |
| | | group by v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, |
| | | v.vehicle_model, v.status, v.platform_code, v.create_by, v.create_time, |
| | | v.update_by, v.update_time, v.remark |
| | | order by v.create_time desc |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoList" parameterType="VehicleInfo" resultMap="VehicleInfoResult"> |
| | | select v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, |
| | | v.vehicle_model, v.status, v.platform_code, v.create_by, v.create_time, |
| | | v.update_by, v.update_time, v.remark, |
| | | GROUP_CONCAT(DISTINCT d.dept_name ORDER BY d.dept_name SEPARATOR ',') as dept_name |
| | | from tb_vehicle_info v |
| | | left join tb_vehicle_dept vd on v.vehicle_id = vd.vehicle_id |
| | | left join sys_dept d on vd.dept_id = d.dept_id |
| | | <where> |
| | | <if test="vehicleNo != null and vehicleNo != ''"> and v.vehicle_no LIKE concat('%', #{vehicleNo}, '%')</if> |
| | | <if test="deviceId != null and deviceId != ''"> and v.device_id = #{deviceId}</if> |
| | | <if test="vehicleType != null and vehicleType != ''"> and v.vehicle_type = #{vehicleType}</if> |
| | | <if test="vehicleBrand != null and vehicleBrand != ''"> and v.vehicle_brand = #{vehicleBrand}</if> |
| | | <if test="vehicleModel != null and vehicleModel != ''"> and v.vehicle_model = #{vehicleModel}</if> |
| | | <if test="status != null and status != ''"> and v.status = #{status}</if> |
| | | <if test="platformCode != null and platformCode != ''"> and v.platform_code = #{platformCode}</if> |
| | | <!-- 部门过滤:根据分公司ID查询车辆(通过关联表) --> |
| | | <if test="deptId != null"> |
| | | and EXISTS ( |
| | | SELECT 1 FROM tb_vehicle_dept vd2 |
| | | WHERE vd2.vehicle_id = v.vehicle_id |
| | | AND vd2.dept_id = #{deptId} |
| | | ) |
| | | </if> |
| | | <!-- 任务车辆选择必须过滤:只显示car_id不为空且已关联分公司的车辆 --> |
| | | <!-- and v.car_id is not null and v.car_id != ''--> |
| | | <!-- and EXISTS (SELECT 1 FROM tb_vehicle_dept vd WHERE vd.vehicle_id = v.vehicle_id)--> |
| | | and v.status=0 |
| | | |
| | | </where> |
| | | group by v.vehicle_id, v.car_id, v.device_id, v.vehicle_no, v.vehicle_type, v.vehicle_brand, |
| | | v.vehicle_model, v.status, v.platform_code, v.create_by, v.create_time, |
| | | v.update_by, v.update_time, v.remark |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoById" parameterType="Long" resultMap="VehicleInfoResult"> |
| | | <select id="selectVehicleInfoById" parameterType="Long" resultMap="VehicleInfoWithDeptsResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | where v.vehicle_id = #{vehicleId} |
| | | </select> |
| | | |
| | | <!-- 查询车辆信息(包含多分公司关联) --> |
| | | <select id="selectVehicleInfoWithDeptsById" parameterType="Long" resultMap="VehicleInfoWithDeptsResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | where v.vehicle_id = #{vehicleId} |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoByPlateNumber" parameterType="String" resultMap="VehicleInfoResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | where v.vehicle_no = #{plateNumber} |
| | | where v.vehicle_no LIKE concat('%', #{plateNumber}, '%') |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoByVehicleNo" parameterType="String" resultMap="VehicleInfoResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | where v.vehicle_no = #{vehicleNo} |
| | | where v.vehicle_no LIKE concat('%', #{vehicleNo}, '%') |
| | | </select> |
| | | |
| | | <insert id="insertVehicleInfo" parameterType="VehicleInfo" useGeneratedKeys="true" keyProperty="vehicleId"> |
| | |
| | | <if test="vehicleModel != null">vehicle_model,</if> |
| | | <if test="status != null">status,</if> |
| | | <if test="platformCode != null">platform_code,</if> |
| | | <if test="deptId != null">dept_id,</if> |
| | | <if test="createBy != null">create_by,</if> |
| | | <if test="createTime != null">create_time,</if> |
| | | <if test="updateBy != null">update_by,</if> |
| | |
| | | <if test="vehicleModel != null">#{vehicleModel},</if> |
| | | <if test="status != null">#{status},</if> |
| | | <if test="platformCode != null">#{platformCode},</if> |
| | | <if test="deptId != null">#{deptId},</if> |
| | | <if test="createBy != null">#{createBy},</if> |
| | | <if test="createTime != null">#{createTime},</if> |
| | | <if test="updateBy != null">#{updateBy},</if> |
| | |
| | | <if test="vehicleModel != null">vehicle_model = #{vehicleModel},</if> |
| | | <if test="status != null">status = #{status},</if> |
| | | <if test="platformCode != null">platform_code = #{platformCode},</if> |
| | | <if test="deptId != null">dept_id = #{deptId},</if> |
| | | <if test="updateBy != null">update_by = #{updateBy},</if> |
| | | <if test="updateTime != null">update_time = #{updateTime},</if> |
| | | <if test="remark != null">remark = #{remark},</if> |
| | |
| | | <id property="statsId" column="stats_id" /> |
| | | <result property="vehicleId" column="vehicle_id" /> |
| | | <result property="vehicleNo" column="vehicle_no" /> |
| | | <result property="deptName" column="dept_name" /> |
| | | <result property="deptId" column="dept_id" /> |
| | | <result property="statDate" column="stat_date" /> |
| | | <result property="totalMileage" column="total_mileage" /> |
| | | <result property="taskMileage" column="task_mileage" /> |
| | |
| | | <result property="taskRatio" column="task_ratio" /> |
| | | <result property="gpsPointCount" column="gps_point_count" /> |
| | | <result property="taskCount" column="task_count" /> |
| | | <result property="segmentCount" column="segment_count" /> |
| | | <result property="dataSource" column="data_source" /> |
| | | <result property="createTime" column="create_time" /> |
| | | <result property="updateTime" column="update_time" /> |
| | | </resultMap> |
| | |
| | | </resultMap> |
| | | |
| | | <sql id="selectVehicleMileageStatsVo"> |
| | | select stats_id, vehicle_id, vehicle_no, stat_date, total_mileage, task_mileage, |
| | | non_task_mileage, task_ratio, gps_point_count, task_count, create_time, update_time |
| | | from tb_vehicle_mileage_stats |
| | | select s.stats_id, s.vehicle_id, s.vehicle_no, s.stat_date, s.total_mileage, s.task_mileage, |
| | | s.non_task_mileage, s.task_ratio, s.gps_point_count, s.task_count, s.segment_count, |
| | | s.data_source, s.create_time, s.update_time, |
| | | GROUP_CONCAT(DISTINCT d.dept_name ORDER BY d.dept_name SEPARATOR ',') as dept_name, |
| | | vd.dept_id |
| | | from tb_vehicle_mileage_stats s |
| | | left join tb_vehicle_info v on s.vehicle_id = v.vehicle_id |
| | | left join tb_vehicle_dept vd on v.vehicle_id = vd.vehicle_id |
| | | left join sys_dept d on vd.dept_id = d.dept_id |
| | | </sql> |
| | | |
| | | <select id="selectVehicleMileageStatsList" parameterType="VehicleMileageStats" resultMap="VehicleMileageStatsResult"> |
| | | <include refid="selectVehicleMileageStatsVo"/> |
| | | <where> |
| | | <if test="vehicleId != null"> |
| | | and vehicle_id = #{vehicleId} |
| | | and s.vehicle_id = #{vehicleId} |
| | | </if> |
| | | <if test="vehicleNo != null and vehicleNo != ''"> |
| | | and vehicle_no = #{vehicleNo} |
| | | and s.vehicle_no like concat('%', #{vehicleNo}, '%') |
| | | </if> |
| | | <if test="deptId != null"> |
| | | and vd.dept_id = #{deptId} |
| | | </if> |
| | | <if test="statDate != null"> |
| | | and stat_date = #{statDate} |
| | | and s.stat_date = #{statDate} |
| | | </if> |
| | | <if test="params.beginStatDate != null and params.beginStatDate != ''"> |
| | | and stat_date >= #{params.beginStatDate} |
| | | and s.stat_date >= #{params.beginStatDate} |
| | | </if> |
| | | <if test="params.endStatDate != null and params.endStatDate != ''"> |
| | | and stat_date <= #{params.endStatDate} |
| | | and s.stat_date <= #{params.endStatDate} |
| | | </if> |
| | | </where> |
| | | order by stat_date desc, vehicle_id |
| | | group by s.stats_id, s.vehicle_id, s.vehicle_no, s.stat_date, s.total_mileage, s.task_mileage, |
| | | s.non_task_mileage, s.task_ratio, s.gps_point_count, s.task_count, s.segment_count, |
| | | s.data_source, s.create_time, s.update_time, vd.dept_id |
| | | order by s.stat_date desc, s.vehicle_id |
| | | </select> |
| | | |
| | | <select id="selectVehicleMileageStatsById" parameterType="Long" resultMap="VehicleMileageStatsResult"> |
| | | <include refid="selectVehicleMileageStatsVo"/> |
| | | where stats_id = #{statsId} |
| | | where s.stats_id = #{statsId} |
| | | group by s.stats_id, s.vehicle_id, s.vehicle_no, s.stat_date, s.total_mileage, s.task_mileage, |
| | | s.non_task_mileage, s.task_ratio, s.gps_point_count, s.task_count, s.segment_count, |
| | | s.data_source, s.create_time, s.update_time, vd.dept_id |
| | | </select> |
| | | |
| | | <select id="selectByVehicleIdAndDate" resultMap="VehicleMileageStatsResult"> |
| | | <include refid="selectVehicleMileageStatsVo"/> |
| | | where vehicle_id = #{vehicleId} and stat_date = #{statDate} |
| | | where s.vehicle_id = #{vehicleId} and s.stat_date = #{statDate} |
| | | group by s.stats_id, s.vehicle_id, s.vehicle_no, s.stat_date, s.total_mileage, s.task_mileage, |
| | | s.non_task_mileage, s.task_ratio, s.gps_point_count, s.task_count, s.segment_count, |
| | | s.data_source, s.create_time, s.update_time, vd.dept_id |
| | | </select> |
| | | |
| | | <select id="selectTaskTimeIntervals" resultMap="TaskTimeIntervalResult"> |
| | |
| | | <if test="taskRatio != null">task_ratio,</if> |
| | | <if test="gpsPointCount != null">gps_point_count,</if> |
| | | <if test="taskCount != null">task_count,</if> |
| | | <if test="segmentCount != null">segment_count,</if> |
| | | <if test="dataSource != null">data_source,</if> |
| | | create_time |
| | | </trim> |
| | | <trim prefix="values (" suffix=")" suffixOverrides=","> |
| | |
| | | <if test="taskRatio != null">#{taskRatio},</if> |
| | | <if test="gpsPointCount != null">#{gpsPointCount},</if> |
| | | <if test="taskCount != null">#{taskCount},</if> |
| | | <if test="segmentCount != null">#{segmentCount},</if> |
| | | <if test="dataSource != null">#{dataSource},</if> |
| | | NOW() |
| | | </trim> |
| | | </insert> |
| | |
| | | <if test="taskRatio != null">task_ratio = #{taskRatio},</if> |
| | | <if test="gpsPointCount != null">gps_point_count = #{gpsPointCount},</if> |
| | | <if test="taskCount != null">task_count = #{taskCount},</if> |
| | | <if test="segmentCount != null">segment_count = #{segmentCount},</if> |
| | | <if test="dataSource != null">data_source = #{dataSource},</if> |
| | | update_time = NOW() |
| | | </trim> |
| | | where stats_id = #{statsId} |
| New file |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // 查询GPS分段里程列表 |
| | | export function listSegmentMileage(query) { |
| | | return request({ |
| | | url: '/system/gpsSegment/list', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // 按任务ID查询GPS分段里程列表 |
| | | export function getSegmentsByTaskId(taskId) { |
| | | return request({ |
| | | url: '/system/gpsSegment/task/' + taskId, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 查询任务总里程 |
| | | export function getTaskTotalMileage(taskId) { |
| | | return request({ |
| | | url: '/system/gpsSegment/task/' + taskId + '/total', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 查询车辆指定日期范围的GPS分段里程 |
| | | export function getSegmentsByDateRange(vehicleId, startDate, endDate) { |
| | | return request({ |
| | | url: '/system/gpsSegment/range', |
| | | method: 'get', |
| | | params: { |
| | | vehicleId: vehicleId, |
| | | startDate: startDate, |
| | | endDate: endDate |
| | | } |
| | | }) |
| | | } |
| New file |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // 查询车辆里程统计列表 |
| | | export function listMileageStats(query) { |
| | | return request({ |
| | | url: '/system/mileageStats/list', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // 查询车辆里程统计详细 |
| | | export function getMileageStats(statsId) { |
| | | return request({ |
| | | url: '/system/mileageStats/' + statsId, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 新增车辆里程统计 |
| | | export function addMileageStats(data) { |
| | | return request({ |
| | | url: '/system/mileageStats', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 修改车辆里程统计 |
| | | export function updateMileageStats(data) { |
| | | return request({ |
| | | url: '/system/mileageStats', |
| | | method: 'put', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 删除车辆里程统计 |
| | | export function delMileageStats(statsId) { |
| | | return request({ |
| | | url: '/system/mileageStats/' + statsId, |
| | | method: 'delete' |
| | | }) |
| | | } |
| | | |
| | | // 手动计算指定车辆指定日期的里程统计 |
| | | export function calculateMileageStats(vehicleId, statDate) { |
| | | return request({ |
| | | url: '/system/mileageStats/calculate', |
| | | method: 'post', |
| | | params: { |
| | | vehicleId: vehicleId, |
| | | statDate: statDate |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 批量计算指定日期所有车辆的里程统计 |
| | | export function batchCalculateMileageStats(statDate) { |
| | | return request({ |
| | | url: '/system/mileageStats/batchCalculate', |
| | | method: 'post', |
| | | params: { |
| | | statDate: statDate |
| | | } |
| | | }) |
| | | } |
| New file |
| | |
| | | # 任务GPS里程统计功能集成指南 |
| | | |
| | | ## 功能概述 |
| | | |
| | | 该功能实现了任务与GPS里程的自动关联,可以在任务详情页面直接展示该任务执行期间的GPS行驶里程统计。 |
| | | |
| | | ## 核心特性 |
| | | |
| | | - ✅ 自动关联:GPS里程计算时自动关联正在执行的任务 |
| | | - ✅ 实时查询:支持按任务ID快速查询关联的GPS里程数据 |
| | | - ✅ 分段展示:显示任务期间每5分钟的GPS里程分段明细 |
| | | - ✅ 统计汇总:自动计算任务总里程、分段数、GPS点数 |
| | | - ✅ 可视化组件:提供开箱即用的Vue组件 |
| | | |
| | | ## 快速开始 |
| | | |
| | | ### 1. 执行数据库迁移 |
| | | |
| | | ```bash |
| | | mysql -u root -p < sql/updates/add_task_id_to_segment_mileage.sql |
| | | ``` |
| | | |
| | | ### 2. 在任务详情页面集成组件 |
| | | |
| | | ```vue |
| | | <template> |
| | | <div> |
| | | <!-- 任务基本信息 --> |
| | | <el-card> |
| | | <!-- ... 任务详情内容 ... --> |
| | | </el-card> |
| | | |
| | | <!-- GPS里程统计组件 --> |
| | | <task-mileage-detail :task-id="taskId" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import TaskMileageDetail from '@/components/TaskMileageDetail' |
| | | |
| | | export default { |
| | | components: { |
| | | TaskMileageDetail |
| | | }, |
| | | data() { |
| | | return { |
| | | taskId: 123 // 任务ID |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | ``` |
| | | |
| | | ### 3. API调用示例 |
| | | |
| | | ```javascript |
| | | import { getTaskTotalMileage, getSegmentsByTaskId } from '@/api/system/gpsSegment' |
| | | |
| | | // 查询任务总里程 |
| | | getTaskTotalMileage(taskId).then(res => { |
| | | console.log('任务总里程:', res.data, 'km') |
| | | }) |
| | | |
| | | // 查询任务GPS分段明细 |
| | | getSegmentsByTaskId(taskId).then(res => { |
| | | console.log('分段明细:', res.data) |
| | | }) |
| | | ``` |
| | | |
| | | ## 组件属性 |
| | | |
| | | ### TaskMileageDetail |
| | | |
| | | | 属性 | 类型 | 必填 | 说明 | |
| | | |-----|------|------|------| |
| | | | taskId | Number/String | 是 | 任务ID | |
| | | |
| | | ## API接口说明 |
| | | |
| | | ### 1. 按任务ID查询GPS分段里程 |
| | | |
| | | **接口地址**: `GET /system/gpsSegment/task/{taskId}` |
| | | |
| | | **权限**: `system:gpsSegment:query` |
| | | |
| | | **返回示例**: |
| | | ```json |
| | | { |
| | | "code": 200, |
| | | "data": [ |
| | | { |
| | | "segmentId": 1, |
| | | "vehicleId": 10, |
| | | "vehicleNo": "粤A12345", |
| | | "taskId": 100, |
| | | "taskCode": "T20250115001", |
| | | "segmentStartTime": "2025-01-15 10:00:00", |
| | | "segmentEndTime": "2025-01-15 10:05:00", |
| | | "segmentDistance": 2.5, |
| | | "gpsPointCount": 5, |
| | | "startLongitude": 113.264385, |
| | | "startLatitude": 23.129112, |
| | | "endLongitude": 113.280637, |
| | | "endLatitude": 23.125178 |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | ### 2. 查询任务总里程 |
| | | |
| | | **接口地址**: `GET /system/gpsSegment/task/{taskId}/total` |
| | | |
| | | **权限**: `system:gpsSegment:query` |
| | | |
| | | **返回示例**: |
| | | ```json |
| | | { |
| | | "code": 200, |
| | | "data": 15.8 |
| | | } |
| | | ``` |
| | | |
| | | ### 3. 按日期范围查询GPS分段里程 |
| | | |
| | | **接口地址**: `GET /system/gpsSegment/range` |
| | | |
| | | **参数**: |
| | | - `vehicleId`: 车辆ID |
| | | - `startDate`: 开始日期 (格式: yyyy-MM-dd) |
| | | - `endDate`: 结束日期 (格式: yyyy-MM-dd) |
| | | |
| | | ## 数据流程 |
| | | |
| | | ```mermaid |
| | | graph LR |
| | | A[定时任务触发] --> B[计算GPS里程] |
| | | B --> C[查询车辆正在执行的任务] |
| | | C --> D{时间段是否重叠?} |
| | | D -->|是| E[关联task_id和task_code] |
| | | D -->|否| F[task_id为NULL] |
| | | E --> G[保存分段里程记录] |
| | | F --> G |
| | | G --> H[前端查询展示] |
| | | ``` |
| | | |
| | | ## 统计逻辑 |
| | | |
| | | 1. **时间重叠判断**: |
| | | - GPS分段时间段:`[segmentStartTime, segmentEndTime]` |
| | | - 任务执行时间段:`[actualStartTime, actualEndTime]` |
| | | - 重叠条件:`segmentStartTime < taskEndTime && segmentEndTime > taskStartTime` |
| | | |
| | | 2. **优先级**: |
| | | - 优先使用任务的实际开始/结束时间 |
| | | - 如果实际时间为空,则使用计划时间 |
| | | |
| | | 3. **跨段里程**: |
| | | - 自动计算相邻5分钟段之间的间隙距离 |
| | | - 避免里程统计遗漏 |
| | | |
| | | ## 注意事项 |
| | | |
| | | 1. **权限配置**:确保用户拥有 `system:gpsSegment:query` 权限 |
| | | 2. **数据准确性**:GPS里程计算后才会有关联数据 |
| | | 3. **实时性**:数据由定时任务更新,非实时数据 |
| | | 4. **历史数据**:已计算的历史GPS数据不会自动关联任务,需重新计算 |
| | | |
| | | ## 扩展建议 |
| | | |
| | | 1. **任务列表展示**:在任务列表中直接显示任务里程 |
| | | 2. **报表统计**:基于task_id进行任务里程分析 |
| | | 3. **异常检测**:对比预估里程与实际GPS里程,发现异常 |
| | | 4. **成本核算**:基于任务里程进行费用计算 |
| | | |
| | | ## 性能优化 |
| | | |
| | | - ✅ 使用索引:`idx_task_id`, `idx_vehicle_task` |
| | | - ✅ 聚合查询:直接SUM计算总里程 |
| | | - ✅ 分页加载:分段明细支持分页 |
| | | - ✅ 缓存策略:可对任务总里程进行Redis缓存 |
| | | |
| | | ## 技术栈 |
| | | |
| | | - **后端**: Spring Boot + MyBatis |
| | | - **前端**: Vue 2 + Element UI |
| | | - **数据库**: MySQL 5.7+ |
| New file |
| | |
| | | <template> |
| | | <div class="task-mileage-detail"> |
| | | <el-card class="box-card" shadow="hover"> |
| | | <div slot="header" class="clearfix"> |
| | | <span class="card-title"> |
| | | <i class="el-icon-location-information"></i> |
| | | 任务GPS里程统计 |
| | | </span> |
| | | <el-button |
| | | style="float: right; padding: 3px 10px" |
| | | type="text" |
| | | size="small" |
| | | @click="refreshData" |
| | | :loading="loading" |
| | | > |
| | | <i class="el-icon-refresh"></i> 刷新 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 统计概览 --> |
| | | <div class="mileage-summary"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <div class="stat-item total-mileage"> |
| | | <div class="stat-label">总里程</div> |
| | | <div class="stat-value">{{ totalMileage }} <span class="unit">km</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item segment-count"> |
| | | <div class="stat-label">分段数</div> |
| | | <div class="stat-value">{{ segmentCount }} <span class="unit">段</span></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item gps-count"> |
| | | <div class="stat-label">GPS点数</div> |
| | | <div class="stat-value">{{ totalGpsPoints }} <span class="unit">个</span></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 分段明细 --> |
| | | <div class="segment-detail" v-if="segmentList.length > 0"> |
| | | <el-divider content-position="left"> |
| | | <i class="el-icon-tickets"></i> 里程分段明细 |
| | | </el-divider> |
| | | |
| | | <el-table |
| | | :data="segmentList" |
| | | size="small" |
| | | :max-height="400" |
| | | stripe |
| | | border |
| | | > |
| | | <el-table-column type="index" label="序号" width="50" align="center" /> |
| | | <el-table-column label="时间段开始" prop="segmentStartTime" width="160" align="center"> |
| | | <template slot-scope="scope"> |
| | | {{ parseTime(scope.row.segmentStartTime, '{h}:{i}:{s}') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="时间段结束" prop="segmentEndTime" width="160" align="center"> |
| | | <template slot-scope="scope"> |
| | | {{ parseTime(scope.row.segmentEndTime, '{h}:{i}:{s}') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="里程(km)" prop="segmentDistance" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag type="success" size="mini">{{ scope.row.segmentDistance }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="GPS点数" prop="gpsPointCount" width="90" align="center" /> |
| | | <el-table-column label="起点坐标" align="center" width="180"> |
| | | <template slot-scope="scope"> |
| | | <span class="coordinate"> |
| | | {{ scope.row.startLongitude }}, {{ scope.row.startLatitude }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="终点坐标" align="center" width="180"> |
| | | <template slot-scope="scope"> |
| | | <span class="coordinate"> |
| | | {{ scope.row.endLongitude }}, {{ scope.row.endLatitude }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计算方式" prop="calculateMethod" width="100" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.calculateMethod === 'tianditu' ? 'warning' : 'info'" size="mini"> |
| | | {{ scope.row.calculateMethod === 'tianditu' ? '天地图' : 'Haversine' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <!-- 无数据提示 --> |
| | | <el-empty |
| | | v-else-if="!loading" |
| | | description="暂无GPS里程数据" |
| | | :image-size="80" |
| | | ></el-empty> |
| | | |
| | | <!-- 加载中 --> |
| | | <div v-if="loading" class="loading-container"> |
| | | <el-skeleton :rows="5" animated /> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { getSegmentsByTaskId, getTaskTotalMileage } from '@/api/system/gpsSegment' |
| | | |
| | | export default { |
| | | name: 'TaskMileageDetail', |
| | | props: { |
| | | taskId: { |
| | | type: [Number, String], |
| | | required: true |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | totalMileage: '0.00', |
| | | segmentList: [], |
| | | segmentCount: 0, |
| | | totalGpsPoints: 0 |
| | | } |
| | | }, |
| | | watch: { |
| | | taskId: { |
| | | handler(newVal) { |
| | | if (newVal) { |
| | | this.loadData() |
| | | } |
| | | }, |
| | | immediate: true |
| | | } |
| | | }, |
| | | methods: { |
| | | /** 加载数据 */ |
| | | loadData() { |
| | | if (!this.taskId) { |
| | | return |
| | | } |
| | | |
| | | this.loading = true |
| | | |
| | | // 并行请求总里程和分段明细 |
| | | Promise.all([ |
| | | getTaskTotalMileage(this.taskId), |
| | | getSegmentsByTaskId(this.taskId) |
| | | ]).then(([totalRes, segmentRes]) => { |
| | | // 处理总里程 |
| | | if (totalRes.code === 200) { |
| | | this.totalMileage = (totalRes.data || 0).toFixed(2) |
| | | } |
| | | |
| | | // 处理分段明细 |
| | | if (segmentRes.code === 200 && segmentRes.data) { |
| | | this.segmentList = segmentRes.data |
| | | this.segmentCount = this.segmentList.length |
| | | |
| | | // 计算总GPS点数 |
| | | this.totalGpsPoints = this.segmentList.reduce((sum, item) => { |
| | | return sum + (item.gpsPointCount || 0) |
| | | }, 0) |
| | | } |
| | | }).catch(error => { |
| | | console.error('加载任务GPS里程数据失败', error) |
| | | this.$message.error('加载GPS里程数据失败') |
| | | }).finally(() => { |
| | | this.loading = false |
| | | }) |
| | | }, |
| | | |
| | | /** 刷新数据 */ |
| | | refreshData() { |
| | | this.loadData() |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .task-mileage-detail { |
| | | margin-top: 15px; |
| | | |
| | | .card-title { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | |
| | | i { |
| | | margin-right: 5px; |
| | | color: #409EFF; |
| | | } |
| | | } |
| | | |
| | | .mileage-summary { |
| | | margin-bottom: 20px; |
| | | |
| | | .stat-item { |
| | | padding: 20px; |
| | | text-align: center; |
| | | border-radius: 4px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: white; |
| | | |
| | | &.total-mileage { |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | } |
| | | |
| | | &.segment-count { |
| | | background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
| | | } |
| | | |
| | | &.gps-count { |
| | | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | opacity: 0.9; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | |
| | | .unit { |
| | | font-size: 14px; |
| | | font-weight: normal; |
| | | margin-left: 4px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .segment-detail { |
| | | margin-top: 20px; |
| | | |
| | | .coordinate { |
| | | font-family: 'Courier New', monospace; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | .loading-container { |
| | | padding: 20px; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | # 车辆GPS行驶统计管理界面 |
| | | |
| | | ## 功能概述 |
| | | |
| | | 在 ruoyi-ui 后台管理系统中新增了车辆GPS行驶统计功能,用于查看和管理车辆的GPS里程统计数据。 |
| | | |
| | | ## 新增文件 |
| | | |
| | | ### 1. 前端页面 |
| | | - **文件路径**: `ruoyi-ui/src/views/system/mileageStats/index.vue` |
| | | - **功能**: 车辆里程统计管理界面 |
| | | |
| | | ### 2. API接口文件 |
| | | - **文件路径**: `ruoyi-ui/src/api/system/mileageStats.js` |
| | | - **功能**: 车辆里程统计相关的API接口调用 |
| | | |
| | | ## 主要功能 |
| | | |
| | | ### 1. 数据查询与展示 |
| | | - ✅ 支持按车牌号、车辆ID、统计日期查询 |
| | | - ✅ 支持日期范围查询 |
| | | - ✅ 分页显示统计数据 |
| | | - ✅ 实时展示以下数据: |
| | | - 车辆ID、车牌号 |
| | | - 统计日期 |
| | | - 总里程(km) |
| | | - 任务里程(km) |
| | | - 非任务里程(km) |
| | | - 任务占比(带颜色标签) |
| | | - GPS点数量 |
| | | - 任务数量 |
| | | - 统计时间 |
| | | |
| | | ### 2. 手动统计 |
| | | - ✅ 可指定车辆ID和统计日期 |
| | | - ✅ 手动触发单个车辆的里程计算 |
| | | - ✅ 实时显示统计进度 |
| | | |
| | | ### 3. 批量统计 |
| | | - ✅ 可指定统计日期 |
| | | - ✅ 自动对所有活跃车辆进行里程计算 |
| | | - ✅ 提示用户等待时间可能较长 |
| | | |
| | | ### 4. 数据详情 |
| | | - ✅ 查看详细的里程统计信息 |
| | | - ✅ 使用描述列表美观展示 |
| | | |
| | | ### 5. 数据导出 |
| | | - ✅ 支持将统计数据导出为Excel文件 |
| | | - ✅ 可按查询条件导出 |
| | | |
| | | ### 6. 数据删除 |
| | | - ✅ 支持单条或批量删除统计记录 |
| | | - ✅ 删除前二次确认 |
| | | |
| | | ## 界面特色 |
| | | |
| | | ### 1. 任务占比颜色标识 |
| | | - 🟢 **绿色** (success): 占比 ≥ 80% |
| | | - 🔵 **蓝色** (primary): 占比 60% - 79% |
| | | - 🟡 **橙色** (warning): 占比 40% - 59% |
| | | - 🔴 **红色** (danger): 占比 < 40% |
| | | - ⚪ **灰色** (info): 无数据 |
| | | |
| | | ### 2. 数据高亮显示 |
| | | - 总里程:蓝色加粗 |
| | | - 任务里程:绿色加粗 |
| | | - 详情数据:大字号加粗 |
| | | |
| | | ## 权限配置 |
| | | |
| | | 需要在系统菜单中配置以下权限标识: |
| | | |
| | | ``` |
| | | system:mileageStats:list # 查询列表 |
| | | system:mileageStats:query # 查看详情 |
| | | system:mileageStats:export # 导出数据 |
| | | system:mileageStats:remove # 删除数据 |
| | | system:mileageStats:calculate # 手动统计 |
| | | system:mileageStats:batch # 批量统计 |
| | | ``` |
| | | |
| | | ## 菜单配置示例 |
| | | |
| | | 在系统管理 → 菜单管理中添加: |
| | | |
| | | ``` |
| | | 菜单名称: 车辆里程统计 |
| | | 父菜单: 系统管理 |
| | | 菜单类型: 菜单 |
| | | 路由地址: mileageStats |
| | | 组件路径: system/mileageStats/index |
| | | 权限标识: system:mileageStats:list |
| | | 菜单图标: chart |
| | | ``` |
| | | |
| | | ## 后端接口 |
| | | |
| | | 后端接口已完成,路径为: |
| | | - Controller: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleMileageStatsController.java` |
| | | - Service: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java` |
| | | |
| | | ### 接口列表 |
| | | |
| | | | 接口路径 | 方法 | 说明 | |
| | | |---------|------|------| |
| | | | /system/mileageStats/list | GET | 查询统计列表 | |
| | | | /system/mileageStats/{statsId} | GET | 获取统计详情 | |
| | | | /system/mileageStats/{statsIds} | DELETE | 删除统计记录 | |
| | | | /system/mileageStats/export | POST | 导出统计数据 | |
| | | | /system/mileageStats/calculate | POST | 手动计算统计 | |
| | | | /system/mileageStats/batchCalculate | POST | 批量计算统计 | |
| | | |
| | | ## 使用说明 |
| | | |
| | | ### 1. 查看统计数据 |
| | | 1. 在后台管理系统中访问"车辆里程统计"菜单 |
| | | 2. 可以通过车牌号、车辆ID、日期等条件筛选数据 |
| | | 3. 点击"详情"按钮查看完整的统计信息 |
| | | |
| | | ### 2. 手动统计单个车辆 |
| | | 1. 点击"手动统计"按钮 |
| | | 2. 输入车辆ID和统计日期 |
| | | 3. 点击"开始统计",系统将计算该车辆在指定日期的里程数据 |
| | | |
| | | ### 3. 批量统计所有车辆 |
| | | 1. 点击"批量统计"按钮 |
| | | 2. 选择统计日期 |
| | | 3. 点击"开始统计",系统将计算所有活跃车辆在该日期的里程数据 |
| | | 4. 注意:此操作可能需要较长时间,请耐心等待 |
| | | |
| | | ### 4. 导出数据 |
| | | 1. 设置查询条件(可选) |
| | | 2. 点击"导出"按钮 |
| | | 3. 系统将生成Excel文件供下载 |
| | | |
| | | ## 数据说明 |
| | | |
| | | - **总里程**: 车辆在统计日期内的总行驶里程 |
| | | - **任务里程**: 车辆在执行任务期间的行驶里程 |
| | | - **非任务里程**: 车辆在非任务时段的行驶里程 |
| | | - **任务占比**: 任务里程 / 总里程的比例 |
| | | - **GPS点数**: 统计日期内记录的GPS定位点数量 |
| | | - **任务数**: 统计日期内车辆执行的任务数量 |
| | | |
| | | ## 注意事项 |
| | | |
| | | 1. 统计数据基于GPS定位点计算,需要车辆安装GPS设备并正常上传数据 |
| | | 2. 批量统计可能消耗较多系统资源,建议在业务低峰期执行 |
| | | 3. 建议定期清理历史统计数据,保留必要的时间范围即可 |
| | | 4. 如果统计结果为0,可能是该日期内无GPS数据或无任务数据 |
| | | |
| | | ## 技术实现 |
| | | |
| | | ### 里程计算方法 |
| | | - 使用 **Haversine公式** 计算GPS坐标点之间的距离 |
| | | - 根据任务时间段自动分配里程到任务/非任务类别 |
| | | - 精确到小数点后2位 |
| | | |
| | | ### 数据来源 |
| | | - GPS数据表: `tb_vehicle_gps` |
| | | - 任务数据表: `sys_task` |
| | | - 统计结果表: `tb_vehicle_mileage_stats` |
| | | |
| | | ## 未来扩展 |
| | | |
| | | 可以考虑增加以下功能: |
| | | - 📊 统计图表展示(折线图、饼图等) |
| | | - 📅 月度、年度汇总统计 |
| | | - 📈 车辆里程趋势分析 |
| | | - 🚗 车辆利用率分析 |
| | | - 💰 里程成本核算 |
| New file |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px"> |
| | | <el-form-item label="归属分公司" prop="deptId"> |
| | | <el-select |
| | | v-model="queryParams.deptId" |
| | | placeholder="请选择分公司" |
| | | clearable |
| | | filterable |
| | | size="small" |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="dept in deptOptions" |
| | | :key="dept.deptId" |
| | | :label="dept.deptName" |
| | | :value="dept.deptId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="车牌号" prop="vehicleNo"> |
| | | <el-input |
| | | v-model="queryParams.vehicleNo" |
| | | placeholder="请输入车牌号" |
| | | clearable |
| | | size="small" |
| | | @keyup.enter.native="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="统计日期" prop="statDate"> |
| | | <el-date-picker |
| | | v-model="queryParams.statDate" |
| | | type="date" |
| | | value-format="yyyy-MM-dd" |
| | | placeholder="选择统计日期" |
| | | clearable |
| | | size="small" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="日期范围"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | size="small" |
| | | style="width: 240px" |
| | | value-format="yyyy-MM-dd" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | ></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> |
| | | <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="success" |
| | | plain |
| | | icon="el-icon-s-operation" |
| | | size="mini" |
| | | @click="handleCalculate" |
| | | v-hasPermi="['system:mileageStats:calculate']" |
| | | >手动统计</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="info" |
| | | plain |
| | | icon="el-icon-s-grid" |
| | | size="mini" |
| | | @click="handleBatchCalculate" |
| | | v-hasPermi="['system:mileageStats:batch']" |
| | | >批量统计</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="warning" |
| | | plain |
| | | icon="el-icon-download" |
| | | size="mini" |
| | | @click="handleExport" |
| | | v-hasPermi="['system:mileageStats:export']" |
| | | >导出</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | icon="el-icon-delete" |
| | | size="mini" |
| | | :disabled="multiple" |
| | | @click="handleDelete" |
| | | v-hasPermi="['system:mileageStats:remove']" |
| | | >删除</el-button> |
| | | </el-col> |
| | | <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> |
| | | </el-row> |
| | | |
| | | <el-table v-loading="loading" :data="statsList" @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="55" align="center" /> |
| | | <el-table-column label="车牌号" align="center" prop="vehicleNo" width="120" fixed /> |
| | | <el-table-column label="归属分公司" align="center" prop="deptName" width="150" show-overflow-tooltip /> |
| | | <el-table-column label="统计日期" align="center" prop="statDate" width="120"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ parseTime(scope.row.statDate, '{y}-{m}-{d}') }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="总里程(km)" align="center" prop="totalMileage" width="110"> |
| | | <template slot-scope="scope"> |
| | | <span class="mileage-value">{{ scope.row.totalMileage || '0.00' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="任务里程(km)" align="center" prop="taskMileage" width="120"> |
| | | <template slot-scope="scope"> |
| | | <span class="mileage-value task-mileage">{{ scope.row.taskMileage || '0.00' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="非任务里程(km)" align="center" prop="nonTaskMileage" width="130"> |
| | | <template slot-scope="scope"> |
| | | <span class="mileage-value">{{ scope.row.nonTaskMileage || '0.00' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="任务占比" align="center" prop="taskRatio" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="getRatioType(scope.row.taskRatio)"> |
| | | {{ formatRatio(scope.row.taskRatio) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="GPS点数" align="center" prop="gpsPointCount" width="90" /> |
| | | <el-table-column label="任务数" align="center" prop="taskCount" width="80" /> |
| | | <el-table-column label="分段数" align="center" prop="segmentCount" width="80" /> |
| | | <el-table-column label="统计时间" align="center" prop="createTime" width="160"> |
| | | <template slot-scope="scope"> |
| | | <span>{{ parseTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-view" |
| | | @click="handleView(scope.row)" |
| | | v-hasPermi="['system:mileageStats:query']" |
| | | >详情</el-button> |
| | | <el-button |
| | | size="mini" |
| | | type="text" |
| | | icon="el-icon-delete" |
| | | @click="handleDelete(scope.row)" |
| | | v-hasPermi="['system:mileageStats:remove']" |
| | | >删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="total>0" |
| | | :total="total" |
| | | :page.sync="queryParams.pageNum" |
| | | :limit.sync="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | |
| | | <!-- 手动统计对话框 --> |
| | | <el-dialog title="手动里程统计" :visible.sync="calculateOpen" width="500px" append-to-body> |
| | | <el-form ref="calculateForm" :model="calculateForm" :rules="calculateRules" label-width="100px"> |
| | | <el-form-item label="车辆ID" prop="vehicleId"> |
| | | <el-input v-model="calculateForm.vehicleId" placeholder="请输入车辆ID" type="number" /> |
| | | </el-form-item> |
| | | <el-form-item label="统计日期" prop="statDate"> |
| | | <el-date-picker |
| | | v-model="calculateForm.statDate" |
| | | type="date" |
| | | value-format="yyyy-MM-dd" |
| | | placeholder="选择统计日期" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button type="primary" @click="submitCalculate" :loading="calculateLoading">开始统计</el-button> |
| | | <el-button @click="calculateOpen = false">取 消</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 批量统计对话框 --> |
| | | <el-dialog title="批量里程统计" :visible.sync="batchCalculateOpen" width="500px" append-to-body> |
| | | <el-form ref="batchCalculateForm" :model="batchCalculateForm" :rules="batchCalculateRules" label-width="100px"> |
| | | <el-form-item label="统计日期" prop="statDate"> |
| | | <el-date-picker |
| | | v-model="batchCalculateForm.statDate" |
| | | type="date" |
| | | value-format="yyyy-MM-dd" |
| | | placeholder="选择统计日期" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-alert |
| | | title="提示" |
| | | type="warning" |
| | | description="批量统计将对所有活跃车辆进行指定日期的里程计算,可能需要较长时间,请耐心等待。" |
| | | :closable="false" |
| | | show-icon |
| | | /> |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button type="primary" @click="submitBatchCalculate" :loading="batchCalculateLoading">开始统计</el-button> |
| | | <el-button @click="batchCalculateOpen = false">取 消</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- 详情对话框 --> |
| | | <el-dialog title="里程统计详情" :visible.sync="detailOpen" width="600px" append-to-body> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="车牌号">{{ detailData.vehicleNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="归属分公司">{{ detailData.deptName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="统计日期"> |
| | | {{ parseTime(detailData.statDate, '{y}-{m}-{d}') }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="总里程"> |
| | | <span class="detail-value">{{ detailData.totalMileage }} km</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="任务里程"> |
| | | <span class="detail-value task-mileage">{{ detailData.taskMileage }} km</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="非任务里程"> |
| | | <span class="detail-value">{{ detailData.nonTaskMileage }} km</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="任务占比"> |
| | | <el-tag :type="getRatioType(detailData.taskRatio)"> |
| | | {{ formatRatio(detailData.taskRatio) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="GPS点数">{{ detailData.gpsPointCount }}</el-descriptions-item> |
| | | <el-descriptions-item label="任务数">{{ detailData.taskCount }}</el-descriptions-item> |
| | | <el-descriptions-item label="分段数">{{ detailData.segmentCount }}</el-descriptions-item> |
| | | <el-descriptions-item label="数据来源"> |
| | | <el-tag :type="detailData.dataSource === 'segment' ? 'success' : 'info'" size="small"> |
| | | {{ detailData.dataSource === 'segment' ? '从分段汇总' : '直接计算' }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="统计时间" :span="2"> |
| | | {{ parseTime(detailData.createTime) }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button @click="detailOpen = false">关 闭</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { listMileageStats, getMileageStats, delMileageStats, calculateMileageStats, batchCalculateMileageStats } from "@/api/system/mileageStats"; |
| | | import { listDept } from "@/api/system/dept"; |
| | | |
| | | export default { |
| | | name: "MileageStats", |
| | | data() { |
| | | return { |
| | | // 遮罩层 |
| | | loading: true, |
| | | // 选中数组 |
| | | ids: [], |
| | | // 非单个禁用 |
| | | single: true, |
| | | // 非多个禁用 |
| | | multiple: true, |
| | | // 显示搜索条件 |
| | | showSearch: true, |
| | | // 总条数 |
| | | total: 0, |
| | | // 车辆里程统计表格数据 |
| | | statsList: [], |
| | | // 分公司选项 |
| | | deptOptions: [], |
| | | // 日期范围 |
| | | dateRange: [], |
| | | // 弹出层标题 |
| | | title: "", |
| | | // 是否显示手动统计弹出层 |
| | | calculateOpen: false, |
| | | // 是否显示批量统计弹出层 |
| | | batchCalculateOpen: false, |
| | | // 是否显示详情弹出层 |
| | | detailOpen: false, |
| | | // 详情数据 |
| | | detailData: {}, |
| | | // 手动统计加载状态 |
| | | calculateLoading: false, |
| | | // 批量统计加载状态 |
| | | batchCalculateLoading: false, |
| | | // 查询参数 |
| | | queryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | vehicleId: null, |
| | | vehicleNo: null, |
| | | deptId: null, |
| | | statDate: null |
| | | }, |
| | | // 手动统计表单 |
| | | calculateForm: { |
| | | vehicleId: null, |
| | | statDate: null |
| | | }, |
| | | // 批量统计表单 |
| | | batchCalculateForm: { |
| | | statDate: null |
| | | }, |
| | | // 手动统计表单校验 |
| | | calculateRules: { |
| | | vehicleId: [ |
| | | { required: true, message: "车辆ID不能为空", trigger: "blur" } |
| | | ], |
| | | statDate: [ |
| | | { required: true, message: "统计日期不能为空", trigger: "change" } |
| | | ] |
| | | }, |
| | | // 批量统计表单校验 |
| | | batchCalculateRules: { |
| | | statDate: [ |
| | | { required: true, message: "统计日期不能为空", trigger: "change" } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | created() { |
| | | this.getList(); |
| | | this.getDeptList(); |
| | | }, |
| | | methods: { |
| | | /** 查询分公司列表 */ |
| | | getDeptList() { |
| | | listDept().then(response => { |
| | | this.deptOptions = response.data || []; |
| | | }); |
| | | }, |
| | | /** 查询车辆里程统计列表 */ |
| | | getList() { |
| | | this.loading = true; |
| | | listMileageStats(this.addDateRange(this.queryParams, this.dateRange)).then(response => { |
| | | this.statsList = response.rows; |
| | | this.total = response.total; |
| | | this.loading = false; |
| | | }); |
| | | }, |
| | | /** 搜索按钮操作 */ |
| | | handleQuery() { |
| | | this.queryParams.pageNum = 1; |
| | | this.getList(); |
| | | }, |
| | | /** 重置按钮操作 */ |
| | | resetQuery() { |
| | | this.dateRange = []; |
| | | this.resetForm("queryForm"); |
| | | this.handleQuery(); |
| | | }, |
| | | // 多选框选中数据 |
| | | handleSelectionChange(selection) { |
| | | this.ids = selection.map(item => item.statsId) |
| | | this.single = selection.length !== 1 |
| | | this.multiple = !selection.length |
| | | }, |
| | | /** 手动统计按钮操作 */ |
| | | handleCalculate() { |
| | | this.reset(); |
| | | this.calculateOpen = true; |
| | | }, |
| | | /** 批量统计按钮操作 */ |
| | | handleBatchCalculate() { |
| | | this.batchCalculateForm = { |
| | | statDate: null |
| | | }; |
| | | this.batchCalculateOpen = true; |
| | | }, |
| | | /** 提交手动统计 */ |
| | | submitCalculate() { |
| | | this.$refs["calculateForm"].validate(valid => { |
| | | if (valid) { |
| | | this.calculateLoading = true; |
| | | calculateMileageStats(this.calculateForm.vehicleId, this.calculateForm.statDate).then(response => { |
| | | this.$modal.msgSuccess("里程统计完成"); |
| | | this.calculateOpen = false; |
| | | this.calculateLoading = false; |
| | | this.getList(); |
| | | }).catch(() => { |
| | | this.calculateLoading = false; |
| | | }); |
| | | } |
| | | }); |
| | | }, |
| | | /** 提交批量统计 */ |
| | | submitBatchCalculate() { |
| | | this.$refs["batchCalculateForm"].validate(valid => { |
| | | if (valid) { |
| | | this.$modal.confirm('确认要对所有活跃车辆进行里程统计吗?').then(() => { |
| | | this.batchCalculateLoading = true; |
| | | batchCalculateMileageStats(this.batchCalculateForm.statDate).then(response => { |
| | | this.$modal.msgSuccess(response.msg || "批量里程统计完成"); |
| | | this.batchCalculateOpen = false; |
| | | this.batchCalculateLoading = false; |
| | | this.getList(); |
| | | }).catch(() => { |
| | | this.batchCalculateLoading = false; |
| | | }); |
| | | }); |
| | | } |
| | | }); |
| | | }, |
| | | /** 查看详情按钮操作 */ |
| | | handleView(row) { |
| | | const statsId = row.statsId; |
| | | getMileageStats(statsId).then(response => { |
| | | this.detailData = response.data; |
| | | this.detailOpen = true; |
| | | }); |
| | | }, |
| | | /** 删除按钮操作 */ |
| | | handleDelete(row) { |
| | | const statsIds = row.statsId || this.ids; |
| | | this.$modal.confirm('是否确认删除选中的车辆里程统计数据?').then(function() { |
| | | return delMileageStats(statsIds); |
| | | }).then(() => { |
| | | this.getList(); |
| | | this.$modal.msgSuccess("删除成功"); |
| | | }).catch(() => {}); |
| | | }, |
| | | /** 导出按钮操作 */ |
| | | handleExport() { |
| | | this.download('system/mileageStats/export', { |
| | | ...this.queryParams |
| | | }, `车辆里程统计_${new Date().getTime()}.xlsx`) |
| | | }, |
| | | /** 格式化任务占比显示 */ |
| | | formatRatio(ratio) { |
| | | if (ratio === null || ratio === undefined) { |
| | | return '0%'; |
| | | } |
| | | return (ratio * 100).toFixed(2) + '%'; |
| | | }, |
| | | /** 根据占比获取标签类型 */ |
| | | getRatioType(ratio) { |
| | | if (ratio === null || ratio === undefined) { |
| | | return 'info'; |
| | | } |
| | | const percent = ratio * 100; |
| | | if (percent >= 80) { |
| | | return 'success'; |
| | | } else if (percent >= 60) { |
| | | return ''; |
| | | } else if (percent >= 40) { |
| | | return 'warning'; |
| | | } else { |
| | | return 'danger'; |
| | | } |
| | | }, |
| | | // 表单重置 |
| | | reset() { |
| | | this.calculateForm = { |
| | | vehicleId: null, |
| | | statDate: null |
| | | }; |
| | | this.resetForm("calculateForm"); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mileage-value { |
| | | font-weight: bold; |
| | | color: #409EFF; |
| | | } |
| | | |
| | | .task-mileage { |
| | | color: #67C23A; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | } |
| | | </style> |
| | |
| | | <el-select v-model="queryParams.vehicleType" placeholder="请选择车辆类型" clearable size="small"> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_vehicle_type" |
| | | :key="dict.value" |
| | | :key="'vtype-' + dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | |
| | | <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small"> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_normal_disable" |
| | | :key="dict.value" |
| | | :key="'status-' + dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | |
| | | <el-select v-model="queryParams.platformCode" placeholder="请选择平台" clearable size="small"> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_platform" |
| | | :key="dict.value" |
| | | :key="'platform-' + dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | |
| | | <el-select v-model="form.vehicleType" placeholder="请选择车辆类型" clearable style="width: 100%"> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_vehicle_type" |
| | | :key="dict.value" |
| | | :key="'form-vtype-' + dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | |
| | | <el-select v-model="form.platformCode" placeholder="请选择平台" clearable> |
| | | <el-option |
| | | v-for="dict in dict.type.sys_platform" |
| | | :key="dict.value" |
| | | :key="'form-platform-' + dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio |
| | | v-for="dict in dict.type.sys_normal_disable" |
| | | :key="dict.value" |
| | | :key="'form-status-' + dict.value" |
| | | :label="dict.value" |
| | | >{{dict.label}}</el-radio> |
| | | </el-radio-group> |
| | |
| | | /** 获取部门列表(只显示分公司:parent_id=100) */ |
| | | getDeptList() { |
| | | listDept({ parentId: 100 }).then(response => { |
| | | |
| | | // 过滤出分公司(parent_id=100的部门) |
| | | if (response.data) { |
| | | this.deptList = response.data.filter(dept => dept.parentId === 100); |
| | | |
| | | this.deptList = response.data.filter(dept => dept.parentId === "100"); |
| | | // console.log("deptList: ",this.deptList,response.data.filter(dept => dept.parentId === "100")); |
| | | } else { |
| | | this.deptList = []; |
| | | } |
| | |
| | | const vehicleId = row.vehicleId || this.ids |
| | | getVehicle(vehicleId).then(response => { |
| | | this.form = response.data; |
| | | // 如果没有deptIds,则从 deptId 和 deptName 中预填 |
| | | if (!this.form.deptIds || this.form.deptIds.length === 0) { |
| | | if (this.form.deptId) { |
| | | this.form.deptIds = [this.form.deptId]; |
| | | } else { |
| | | // 确保deptIds是一个数组 |
| | | if (!this.form.deptIds) { |
| | | this.form.deptIds = []; |
| | | } |
| | | // 如果deptIds为空数组,但deptId有值,则添加deptId到deptIds中 |
| | | if (this.form.deptIds.length === 0 && this.form.deptId) { |
| | | this.form.deptIds = [this.form.deptId]; |
| | | } |
| | | this.open = true; |
| | | this.title = "修改车辆信息"; |
| New file |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-page-header @back="goBack" content="任务详情"> |
| | | </el-page-header> |
| | | |
| | | <el-card class="box-card" style="margin-top: 20px;" shadow="hover"> |
| | | <div slot="header" class="clearfix"> |
| | | <span><i class="el-icon-document"></i> 任务基本信息</span> |
| | | </div> |
| | | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="任务编号">{{ taskInfo.taskCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="任务状态"> |
| | | <el-tag :type="getStatusType(taskInfo.taskStatus)"> |
| | | {{ getStatusText(taskInfo.taskStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="车牌号">{{ taskInfo.vehicleNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="执行人">{{ taskInfo.assigneeName }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划开始时间">{{ taskInfo.plannedStartTime }}</el-descriptions-item> |
| | | <el-descriptions-item label="计划结束时间">{{ taskInfo.plannedEndTime }}</el-descriptions-item> |
| | | <el-descriptions-item label="实际开始时间">{{ taskInfo.actualStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="实际结束时间">{{ taskInfo.actualEndTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="出发地址" :span="2">{{ taskInfo.departureAddress }}</el-descriptions-item> |
| | | <el-descriptions-item label="目的地址" :span="2">{{ taskInfo.destinationAddress }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- GPS里程统计组件 --> |
| | | <task-mileage-detail v-if="taskInfo.taskId" :task-id="taskInfo.taskId" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import TaskMileageDetail from '@/components/TaskMileageDetail' |
| | | import { getTask } from '@/api/task' |
| | | |
| | | export default { |
| | | name: 'TaskDetail', |
| | | components: { |
| | | TaskMileageDetail |
| | | }, |
| | | data() { |
| | | return { |
| | | taskInfo: { |
| | | taskId: null, |
| | | taskCode: '', |
| | | taskStatus: '', |
| | | vehicleNo: '', |
| | | assigneeName: '', |
| | | plannedStartTime: '', |
| | | plannedEndTime: '', |
| | | actualStartTime: '', |
| | | actualEndTime: '', |
| | | departureAddress: '', |
| | | destinationAddress: '' |
| | | } |
| | | } |
| | | }, |
| | | created() { |
| | | const taskId = this.$route.params && this.$route.params.taskId |
| | | if (taskId) { |
| | | this.loadTaskInfo(taskId) |
| | | } |
| | | }, |
| | | methods: { |
| | | /** 加载任务信息 */ |
| | | loadTaskInfo(taskId) { |
| | | getTask(taskId).then(response => { |
| | | this.taskInfo = response.data |
| | | }) |
| | | }, |
| | | |
| | | /** 返回 */ |
| | | goBack() { |
| | | this.$router.go(-1) |
| | | }, |
| | | |
| | | /** 获取状态类型 */ |
| | | getStatusType(status) { |
| | | const statusMap = { |
| | | 'PENDING': 'info', |
| | | 'DEPARTING': 'warning', |
| | | 'ARRIVED': 'primary', |
| | | 'RETURNING': 'warning', |
| | | 'COMPLETED': 'success', |
| | | 'CANCELLED': 'danger' |
| | | } |
| | | return statusMap[status] || 'info' |
| | | }, |
| | | |
| | | /** 获取状态文本 */ |
| | | getStatusText(status) { |
| | | const statusMap = { |
| | | 'PENDING': '待出发', |
| | | 'DEPARTING': '出发中', |
| | | 'ARRIVED': '已到达', |
| | | 'RETURNING': '返程中', |
| | | 'COMPLETED': '已完成', |
| | | 'CANCELLED': '已取消' |
| | | } |
| | | return statusMap[status] || status |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | -- 为车辆里程统计表添加分段数量字段 |
| | | -- 用途:记录当日关联的GPS分段数量,用于数据完整性校验和业务监控 |
| | | |
| | | USE `ry-vue`; |
| | | |
| | | -- 检查字段是否存在,如果不存在则添加 |
| | | SET @dbname = DATABASE(); |
| | | SET @tablename = 'tb_vehicle_mileage_stats'; |
| | | SET @columnname = 'segment_count'; |
| | | SET @preparedStatement = (SELECT IF( |
| | | ( |
| | | SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS |
| | | WHERE |
| | | (TABLE_SCHEMA = @dbname) |
| | | AND (TABLE_NAME = @tablename) |
| | | AND (COLUMN_NAME = @columnname) |
| | | ) > 0, |
| | | 'SELECT 1', |
| | | CONCAT('ALTER TABLE ', @tablename, ' ADD COLUMN `segment_count` int(11) DEFAULT 0 COMMENT ''关联的分段数量'' AFTER `task_count`;') |
| | | )); |
| | | PREPARE alterIfNotExists FROM @preparedStatement; |
| | | EXECUTE alterIfNotExists; |
| | | DEALLOCATE PREPARE alterIfNotExists; |
| | | |
| | | -- 验证字段是否添加成功 |
| | | DESC tb_vehicle_mileage_stats; |
| New file |
| | |
| | | -- 为车辆GPS分段里程表添加任务关联字段 |
| | | -- 用途:在计算GPS里程时,自动关联车辆正在执行的任务,方便统计任务里程 |
| | | |
| | | USE ry-vue; |
| | | |
| | | -- 添加任务ID字段 |
| | | ALTER TABLE tb_vehicle_gps_segment_mileage |
| | | ADD COLUMN task_id BIGINT(20) NULL COMMENT '关联的任务ID' AFTER gps_ids; |
| | | |
| | | -- 添加任务编号字段 |
| | | ALTER TABLE tb_vehicle_gps_segment_mileage |
| | | ADD COLUMN task_code VARCHAR(50) NULL COMMENT '关联的任务编号' AFTER task_id; |
| | | |
| | | -- 创建索引,提升按任务ID查询的性能 |
| | | CREATE INDEX idx_task_id ON tb_vehicle_gps_segment_mileage(task_id); |
| | | |
| | | -- 创建复合索引,提升按车辆ID和任务ID组合查询的性能 |
| | | CREATE INDEX idx_vehicle_task ON tb_vehicle_gps_segment_mileage(vehicle_id, task_id); |
| | | |
| | | -- 查看表结构确认 |
| | | DESC tb_vehicle_gps_segment_mileage; |
| | | |
| | | -- 使用说明: |
| | | -- 1. 运行此SQL脚本添加字段 |
| | | -- 2. 重新计算GPS里程时,系统会自动关联任务ID |
| | | -- 3. 统计任务里程时,可直接通过task_id查询: |
| | | -- SELECT SUM(segment_distance) FROM tb_vehicle_gps_segment_mileage WHERE task_id = ? |
| New file |
| | |
| | | -- 移除tb_vehicle_info表的dept_id字段 |
| | | -- 说明:车辆与部门的关联关系现已完全迁移到tb_vehicle_dept多对多关系表 |
| | | |
| | | -- 1. 数据迁移检查(确保所有数据已迁移到tb_vehicle_dept表) |
| | | SELECT '数据迁移检查' AS 检查项, |
| | | (SELECT COUNT(*) FROM tb_vehicle_info WHERE dept_id IS NOT NULL) AS 原表有dept_id的记录数, |
| | | (SELECT COUNT(DISTINCT vehicle_id) FROM tb_vehicle_dept) AS 关联表中的车辆数; |
| | | |
| | | -- 2. 查看未迁移的数据(如果有) |
| | | SELECT vehicle_id, vehicle_no, dept_id, '未迁移到tb_vehicle_dept' AS 状态 |
| | | FROM tb_vehicle_info |
| | | WHERE dept_id IS NOT NULL |
| | | AND vehicle_id NOT IN (SELECT DISTINCT vehicle_id FROM tb_vehicle_dept); |
| | | |
| | | -- 3. 如果上面有未迁移的数据,先执行迁移(可选) |
| | | -- INSERT INTO tb_vehicle_dept (vehicle_id, dept_id, create_time) |
| | | -- SELECT vehicle_id, dept_id, NOW() |
| | | -- FROM tb_vehicle_info |
| | | -- WHERE dept_id IS NOT NULL |
| | | -- AND vehicle_id NOT IN (SELECT DISTINCT vehicle_id FROM tb_vehicle_dept); |
| | | |
| | | -- 4. 删除dept_id字段 |
| | | ALTER TABLE tb_vehicle_info DROP COLUMN dept_id; |
| | | |
| | | -- 5. 验证字段已删除 |
| | | SHOW COLUMNS FROM tb_vehicle_info; |
| New file |
| | |
| | | -- 更新现有的空vehicle_no数据 |
| | | -- 从tb_vehicle_info表同步车牌号到tb_vehicle_gps_segment_mileage和tb_vehicle_mileage_stats |
| | | |
| | | -- 1. 更新GPS分段里程表的vehicle_no |
| | | UPDATE tb_vehicle_gps_segment_mileage seg |
| | | INNER JOIN tb_vehicle_info v ON seg.vehicle_id = v.vehicle_id |
| | | SET seg.vehicle_no = v.vehicle_no |
| | | WHERE seg.vehicle_no IS NULL OR seg.vehicle_no = ''; |
| | | |
| | | -- 2. 更新里程统计表的vehicle_no |
| | | UPDATE tb_vehicle_mileage_stats stats |
| | | INNER JOIN tb_vehicle_info v ON stats.vehicle_id = v.vehicle_id |
| | | SET stats.vehicle_no = v.vehicle_no |
| | | WHERE stats.vehicle_no IS NULL OR stats.vehicle_no = ''; |
| | | |
| | | -- 查询更新结果 |
| | | SELECT '更新GPS分段里程表' AS 表名, COUNT(*) AS 记录数, |
| | | SUM(CASE WHEN vehicle_no IS NOT NULL AND vehicle_no != '' THEN 1 ELSE 0 END) AS 已有车牌号, |
| | | SUM(CASE WHEN vehicle_no IS NULL OR vehicle_no = '' THEN 1 ELSE 0 END) AS 无车牌号 |
| | | FROM tb_vehicle_gps_segment_mileage |
| | | UNION ALL |
| | | SELECT '更新里程统计表' AS 表名, COUNT(*) AS 记录数, |
| | | SUM(CASE WHEN vehicle_no IS NOT NULL AND vehicle_no != '' THEN 1 ELSE 0 END) AS 已有车牌号, |
| | | SUM(CASE WHEN vehicle_no IS NULL OR vehicle_no = '' THEN 1 ELSE 0 END) AS 无车牌号 |
| | | FROM tb_vehicle_mileage_stats; |
| New file |
| | |
| | | -- 车辆GPS分段里程表(按5分钟时间段统计) |
| | | CREATE TABLE `tb_vehicle_gps_segment_mileage` ( |
| | | `segment_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分段ID', |
| | | `vehicle_id` bigint(20) NOT NULL COMMENT '车辆ID', |
| | | `vehicle_no` varchar(20) DEFAULT NULL COMMENT '车牌号', |
| | | `segment_start_time` datetime NOT NULL COMMENT '时间段开始时间', |
| | | `segment_end_time` datetime NOT NULL COMMENT '时间段结束时间', |
| | | `start_longitude` decimal(10,7) DEFAULT NULL COMMENT '起点经度', |
| | | `start_latitude` decimal(10,7) DEFAULT NULL COMMENT '起点纬度', |
| | | `end_longitude` decimal(10,7) DEFAULT NULL COMMENT '终点经度', |
| | | `end_latitude` decimal(10,7) DEFAULT NULL COMMENT '终点纬度', |
| | | `segment_distance` decimal(10,3) DEFAULT 0.000 COMMENT '段距离(公里)', |
| | | `gps_point_count` int(11) DEFAULT 0 COMMENT 'GPS点数量', |
| | | `gps_ids` text COMMENT '关联的GPS记录ID列表(逗号分隔)', |
| | | `calculate_method` varchar(20) DEFAULT 'tianditu' COMMENT '计算方式(tianditu-天地图/haversine-球面距离)', |
| | | `create_time` datetime COMMENT '创建时间', |
| | | `update_time` datetime COMMENT '更新时间', |
| | | PRIMARY KEY (`segment_id`), |
| | | UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `segment_start_time`), |
| | | KEY `idx_vehicle_id` (`vehicle_id`), |
| | | KEY `idx_start_time` (`segment_start_time`), |
| | | KEY `idx_vehicle_date` (`vehicle_id`, `segment_start_time`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆GPS分段里程表'; |
| | | |
| | | -- GPS记录计算状态关联表(用于快速查询GPS点是否已被计算) |
| | | CREATE TABLE `tb_vehicle_gps_calculated` ( |
| | | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', |
| | | `gps_id` bigint(20) NOT NULL COMMENT 'GPS记录ID', |
| | | `segment_id` bigint(20) NOT NULL COMMENT '分段里程ID', |
| | | `vehicle_id` bigint(20) NOT NULL COMMENT '车辆ID', |
| | | `create_time` datetime COMMENT '计算时间', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_gps_id` (`gps_id`), |
| | | KEY `idx_segment_id` (`segment_id`), |
| | | KEY `idx_vehicle_id` (`vehicle_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='GPS记录计算状态表'; |
| | | |
| | | -- 添加配置参数到系统配置表 |
| | | INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time) |
| | | VALUES ('GPS里程计算时间间隔', 'gps.mileage.segment.minutes', '5', 'Y', 'GPS里程计算的时间间隔(分钟),用于将GPS数据分段计算里程', 'admin', NOW()) |
| | | ON DUPLICATE KEY UPDATE config_value = config_value; |
| | | |
| | | INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time) |
| | | VALUES ('GPS里程计算方式', 'gps.mileage.calculate.method', 'tianditu', 'Y', 'GPS里程计算方式:tianditu-天地图API,haversine-球面距离公式', 'admin', NOW()) |
| | | ON DUPLICATE KEY UPDATE config_value = config_value; |
| | | |
| | | INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time) |
| | | VALUES ('GPS里程重复计算控制', 'gps.mileage.skip.calculated', 'true', 'Y', '是否跳过已计算的GPS点:true-跳过,false-允许重复计算', 'admin', NOW()) |
| | | ON DUPLICATE KEY UPDATE config_value = config_value; |
| New file |
| | |
| | | -- 车辆GPS分段里程计算定时任务配置 |
| | | |
| | | -- 1. 实时计算任务(每5分钟执行一次,计算最近10分钟的数据) |
| | | INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) |
| | | VALUES ( |
| | | 'GPS分段里程实时计算', |
| | | 'DEFAULT', |
| | | 'vehicleGpsSegmentMileageTask.calculateRecentSegmentMileage(''10'')', |
| | | '0 0/5 * * * ?', |
| | | '2', |
| | | '0', |
| | | '1', |
| | | 'admin', |
| | | NOW(), |
| | | '每5分钟执行一次,计算最近10分钟内所有车辆的GPS分段里程。参数10表示计算最近10分钟的数据,可根据需要调整' |
| | | ); |
| | | |
| | | -- 2. 每小时汇总任务(每小时执行一次,计算最近1小时的数据) |
| | | INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) |
| | | VALUES ( |
| | | 'GPS分段里程小时汇总', |
| | | 'DEFAULT', |
| | | 'vehicleGpsSegmentMileageTask.calculateRecentSegmentMileage(''60'')', |
| | | '0 5 * * * ?', |
| | | '2', |
| | | '0', |
| | | '1', |
| | | 'admin', |
| | | NOW(), |
| | | '每小时第5分钟执行,计算最近60分钟内所有车辆的GPS分段里程,用于补充遗漏的数据' |
| | | ); |
| | | |
| | | -- 3. 每日全天计算任务(每天凌晨执行,计算前一天的数据) |
| | | INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) |
| | | VALUES ( |
| | | 'GPS分段里程每日汇总', |
| | | 'DEFAULT', |
| | | 'vehicleGpsSegmentMileageTask.calculateDateSegmentMileage(DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 DAY), ''%Y-%m-%d''))', |
| | | '0 10 1 * * ?', |
| | | '2', |
| | | '0', |
| | | '1', |
| | | 'admin', |
| | | NOW(), |
| | | '每天凌晨1点10分执行,计算前一天所有车辆的GPS分段里程。使用DATE_FORMAT动态计算前一天的日期' |
| | | ); |
| | | |
| | | -- 注意事项说明 |
| | | -- 1. 默认所有任务状态为'1'(暂停),需要在后台管理系统中手动启动 |
| | | -- 2. 时间间隔配置在 sys_config 表中:gps.mileage.segment.minutes(默认5分钟) |
| | | -- 3. 计算方式配置在 sys_config 表中:gps.mileage.calculate.method(默认tianditu) |
| | | -- 4. 建议启动顺序: |
| | | -- - 先启动"GPS分段里程实时计算",观察运行情况 |
| | | -- - 确认无误后,再启动"GPS分段里程小时汇总"作为补充 |
| | | -- - 最后启动"GPS分段里程每日汇总"作为数据完整性保障 |
| | | -- 5. cron表达式说明: |
| | | -- - '0 0/5 * * * ?' : 每5分钟执行一次 |
| | | -- - '0 5 * * * ?' : 每小时第5分钟执行 |
| | | -- - '0 10 1 * * ?' : 每天凌晨1点10分执行 |
| | |
| | | `task_ratio` decimal(5,4) DEFAULT 0.0000 COMMENT '任务里程占比(0-1)', |
| | | `gps_point_count` int(11) DEFAULT 0 COMMENT 'GPS点数量', |
| | | `task_count` int(11) DEFAULT 0 COMMENT '任务数量', |
| | | `segment_count` int(11) DEFAULT 0 COMMENT '关联的分段数量', |
| | | `data_source` varchar(20) DEFAULT 'gps' COMMENT '数据来源(segment-从分段汇总,gps-直接计算)', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | | `update_time` datetime DEFAULT NULL COMMENT '更新时间', |
| | | PRIMARY KEY (`stats_id`), |
| | |
| | | KEY `idx_vehicle_id` (`vehicle_id`), |
| | | KEY `idx_stat_date` (`stat_date`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆里程统计表'; |
| | | |
| | | -- 车辆里程统计明细表(可选,用于调试和追溯) |
| | | CREATE TABLE `tb_vehicle_mileage_detail` ( |
| | | `detail_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '明细ID', |
| | | `stats_id` bigint(20) NOT NULL COMMENT '统计ID', |
| | | `vehicle_id` bigint(20) NOT NULL COMMENT '车辆ID', |
| | | `segment_start_time` datetime NOT NULL COMMENT '段起始时间', |
| | | `segment_end_time` datetime NOT NULL COMMENT '段结束时间', |
| | | `start_longitude` decimal(10,7) DEFAULT NULL COMMENT '起点经度', |
| | | `start_latitude` decimal(10,7) DEFAULT NULL COMMENT '起点纬度', |
| | | `end_longitude` decimal(10,7) DEFAULT NULL COMMENT '终点经度', |
| | | `end_latitude` decimal(10,7) DEFAULT NULL COMMENT '终点纬度', |
| | | `segment_distance` decimal(10,3) DEFAULT 0.000 COMMENT '段距离(公里)', |
| | | `task_distance` decimal(10,3) DEFAULT 0.000 COMMENT '任务内距离(公里)', |
| | | `non_task_distance` decimal(10,3) DEFAULT 0.000 COMMENT '任务外距离(公里)', |
| | | `is_in_task` tinyint(1) DEFAULT 0 COMMENT '是否完全在任务时段内(0-否,1-是,2-部分)', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | | PRIMARY KEY (`detail_id`), |
| | | KEY `idx_stats_id` (`stats_id`), |
| | | KEY `idx_vehicle_id` (`vehicle_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆里程统计明细表'; |
| | |
| | | -- 添加车辆里程统计定时任务 |
| | | -- 添加车辆里程统计定时任务(从GPS分段汇总) |
| | | INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, update_by, update_time, remark) |
| | | VALUES ( |
| | | (SELECT IFNULL(MAX(job_id), 0) + 1 FROM sys_job t), |
| | | '车辆里程统计任务', |
| | | '车辆里程统计任务(从分段汇总)', |
| | | 'DEFAULT', |
| | | 'vehicleMileageStatsTask.calculateYesterdayMileage', |
| | | 'vehicleMileageStatsTask.aggregateYesterdayFromSegments', |
| | | '0 30 1 * * ?', |
| | | '3', |
| | | '1', |
| | | '0', |
| | | '1', |
| | | 'admin', |
| | | NOW(), |
| | | '', |
| | | NULL, |
| | | '每天凌晨1:30执行,统计昨日所有车辆的行驶里程' |
| | | '每天凌晨1:30执行,从GPS分段里程汇总生成昨日统计数据(推荐方式,性能更好)' |
| | | ); |