25个文件已修改
17 文件已重命名
4个文件已添加
| | |
| | | "navigationBarTitleText": "选择任务类型" |
| | | } |
| | | }, { |
| | | "path": "pages/task/create-normal", |
| | | "style": { |
| | | "navigationBarTitleText": "创建普通任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/create-emergency", |
| | | "style": { |
| | | "navigationBarTitleText": "创建转运任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/create-welfare", |
| | | "style": { |
| | | "navigationBarTitleText": "创建福祉车任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "任务详情" |
| | | } |
| | | }, { |
| | | "path": "pages/task/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/edit-emergency", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑转运任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/edit-welfare", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑福祗车任务" |
| | | } |
| | | }, { |
| | | "path": "pages/task/settlement", |
| | | "style": { |
| | | "navigationBarTitleText": "任务结算" |
| | | } |
| | | }, { |
| | | "path": "pages/message/index", |
| | | "style": { |
| | | "navigationBarTitleText": "消息中心" |
| | | } |
| | | }], |
| | | "subPackages": [{ |
| | | "root": "pagesTask", |
| | | "name": "task", |
| | | "pages": [{ |
| | | "path": "create-normal", |
| | | "style": { |
| | | "navigationBarTitleText": "创建普通任务" |
| | | } |
| | | }, { |
| | | "path": "create-emergency", |
| | | "style": { |
| | | "navigationBarTitleText": "创建转运任务" |
| | | } |
| | | }, { |
| | | "path": "create-welfare", |
| | | "style": { |
| | | "navigationBarTitleText": "创建福祉车任务" |
| | | } |
| | | }, { |
| | | "path": "detail", |
| | | "style": { |
| | | "navigationBarTitleText": "任务详情" |
| | | } |
| | | }, { |
| | | "path": "edit", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑任务" |
| | | } |
| | | }, { |
| | | "path": "edit-emergency", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑转运任务" |
| | | } |
| | | }, { |
| | | "path": "edit-welfare", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑福祗车任务" |
| | | } |
| | | }, { |
| | | "path": "settlement", |
| | | "style": { |
| | | "navigationBarTitleText": "任务结算" |
| | | } |
| | | }] |
| | | }], |
| | | "tabBar": { |
| | | "color": "#000000", |
| | | "selectedColor": "#000000", |
| | |
| | | // 查看任务详情 |
| | | viewTaskDetail(task) { |
| | | // 跳转到任务详情页面 - 使用taskId |
| | | this.$tab.navigateTo(`/pages/task/detail?id=${task.taskId || task.id}`); |
| | | this.$tab.navigateTo(`/pagesTask/detail?id=${task.taskId || task.id}`); |
| | | }, |
| | | |
| | | // 处理任务操作 |
| | |
| | | |
| | | // 跳转到任务详情页面 |
| | | if (message.taskId) { |
| | | this.$tab.navigateTo(`/pages/task/detail?id=${message.taskId}`) |
| | | this.$tab.navigateTo(`/pagesTask/detail?id=${message.taskId}`) |
| | | } else { |
| | | this.$modal.showToast('无法找到关联任务') |
| | | } |
| | |
| | | console.error('标记消息已读失败:', error) |
| | | // 即使标记失败,也允许跳转 |
| | | if (message && message.taskId) { |
| | | this.$tab.navigateTo(`/pages/task/detail?id=${message.taskId}`) |
| | | this.$tab.navigateTo(`/pagesTask/detail?id=${message.taskId}`) |
| | | } |
| | | } |
| | | }, |
| | |
| | | color: '#E54D42', |
| | | description: '紧急医疗转运任务', |
| | | taskType: 'EMERGENCY_TRANSFER', |
| | | page: '/pages/task/create-emergency' |
| | | page: '/pagesTask/create-emergency' |
| | | }, |
| | | { |
| | | type: 'normal', |
| | |
| | | color: '#007AFF', |
| | | description: '设备维修、保养等日常任务', |
| | | taskType: 'MAINTENANCE', |
| | | page: '/pages/task/create-normal' |
| | | page: '/pagesTask/create-normal' |
| | | }, |
| | | { |
| | | type: 'normal', |
| | |
| | | color: '#1AAD19', |
| | | description: '车辆加油等任务', |
| | | taskType: 'FUEL', |
| | | page: '/pages/task/create-normal' |
| | | page: '/pagesTask/create-normal' |
| | | }, |
| | | |
| | | { |
| | |
| | | color: '#F37B1D', |
| | | description: '老年人、残疾人等特殊群体用车服务', |
| | | taskType: 'WELFARE', |
| | | page: '/pages/task/create-welfare' |
| | | page: '/pagesTask/create-welfare' |
| | | } |
| | | ] |
| | | } |
| | |
| | | |
| | | // 跳转到任务详情页面 - 使用uni.navigateTo |
| | | uni.navigateTo({ |
| | | url: `/pages/task/detail?id=${task.taskId}` |
| | | url: `/pagesTask/detail?id=${task.taskId}` |
| | | }); |
| | | }, |
| | | |
| File was renamed from app/components/DepartureSelector.vue |
| | |
| | | |
| | | <view class="current-location-btn" @click="getCurrentLocation"> |
| | | <uni-icons type="location" size="20" color="#007AFF"></uni-icons> |
| | | <text>当前位置</text> |
| | | |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | this.showAddressSuggestions = false |
| | | this.addressSuggestions = [] |
| | | }, |
| | | |
| | | // 获取当前位置 |
| | | getCurrentLocation() { |
| | | uni.showLoading({ |
| | | title: '获取位置中...' |
| | | }) |
| | | |
| | | // 使用uni-app的GPS定位功能 |
| | | uni.getLocation({ |
| | | type: 'gcj02', // 返回国测局坐标,适用于国内地图 |
| | | success: (res) => { |
| | | console.log('获取到GPS坐标:', res) |
| | | const latitude = res.latitude |
| | | const longitude = res.longitude |
| | | |
| | | // 更新GPS坐标 |
| | | this.$emit('update:longitude', longitude) |
| | | this.$emit('update:latitude', latitude) |
| | | |
| | | geocoder(longitude, latitude){ |
| | | // 调用逆地理编码接口,将坐标转换为地址 |
| | | reverseGeocoder(latitude, longitude) |
| | | .then(response => { |
| | | uni.hideLoading() |
| | | |
| | | if (response.code === 200 && response.data) { |
| | | // 获取详细地址 |
| | | const address = response.data.address || response.data.formattedAddress || '' |
| | | console.log('逆地理编码API完整响应:', JSON.stringify(response)) |
| | | |
| | | if (response.code === 200 && response.data) { |
| | | // 解析后端返回的数据(可能是字符串,需要parse) |
| | | let responseData = response.data |
| | | if (typeof responseData === 'string') { |
| | | try { |
| | | responseData = JSON.parse(responseData) |
| | | } catch (e) { |
| | | console.error('解析响应数据失败:', e) |
| | | responseData = {} |
| | | } |
| | | } |
| | | |
| | | console.log('解析后的responseData:', responseData) |
| | | |
| | | // 腾讯地图API返回格式: {status: 0, result: {address: "..."}} |
| | | let address = '' |
| | | if (responseData.status === 0 && responseData.result) { |
| | | address = responseData.result.address || responseData.result.formatted_addresses?.recommend || '' |
| | | } else if (responseData.address) { |
| | | // 兼容其他可能的返回格式 |
| | | address = responseData.address |
| | | } else if (responseData.formattedAddress) { |
| | | address = responseData.formattedAddress |
| | | } |
| | | |
| | | console.log('解析出的地址:', address) |
| | | |
| | | if (address) { |
| | | // 更新地址 |
| | | this.$emit('update:address', address) |
| | | |
| | |
| | | latitude: latitude |
| | | }) |
| | | |
| | | console.log('逆地理编码成功:', address) |
| | | console.log('逆地理编码成功,地址已更新:', address) |
| | | this.$modal.showToast('已获取当前位置') |
| | | } else { |
| | | console.error('逆地理编码失败:', response.msg) |
| | | console.error('未能从响应中提取地址,responseData:', responseData) |
| | | |
| | | // 即使地址解析失败,坐标已保存,触发事件 |
| | | this.$emit('location-success', { |
| | | address: '', |
| | | longitude: longitude, |
| | | latitude: latitude |
| | | }) |
| | | |
| | | this.$modal.showToast('位置解析失败,请手动输入地址') |
| | | } |
| | | } else { |
| | | console.error('逆地理编码失败,response.code:', response.code, 'msg:', response.msg) |
| | | |
| | | // 即使地址解析失败,坐标已保存,触发事件 |
| | | this.$emit('location-success', { |
| | |
| | | this.$modal.showToast('位置解析失败,但GPS坐标已保存') |
| | | }) |
| | | }, |
| | | // 获取当前位置 |
| | | getCurrentLocation() { |
| | | uni.showLoading({ |
| | | title: '获取位置中...' |
| | | }) |
| | | |
| | | // 使用uni-app的GPS定位功能 |
| | | uni.getLocation({ |
| | | type: 'gcj02', // 返回国测局坐标,适用于国内地图 |
| | | success: (res) => { |
| | | console.log('获取到GPS坐标:', res) |
| | | const latitude = res.latitude |
| | | const longitude = res.longitude |
| | | |
| | | // 更新GPS坐标 |
| | | this.$emit('update:longitude', longitude) |
| | | this.$emit('update:latitude', latitude) |
| | | this.geocoder(longitude, latitude) |
| | | |
| | | }, |
| | | fail: (err) => { |
| | | uni.hideLoading() |
| | | console.error('获取位置失败:', err) |
| | | //我们使用默认的坐标来处理 |
| | | //23.20593,113.228998 |
| | | const longitude = 113.228998 |
| | | const latitude = 23.20593 |
| | | this.$emit('location-success', { |
| | | address: '', |
| | | longitude: longitude, |
| | | latitude: latitude |
| | | }) |
| | | this.geocoder(longitude, latitude) |
| | | |
| | | // 提示用户可能的原因 |
| | | let errorMsg = '获取位置失败' |
| | |
| | | background-color: #f0f7ff; |
| | | border-radius: 10rpx; |
| | | white-space: nowrap; |
| | | min-height: 70rpx; |
| | | min-height: 40rpx; |
| | | |
| | | &:active { |
| | | background-color: #e0f0ff; |
| File was renamed from app/components/OrganizationSelector.vue |
| | |
| | | if (organization) { |
| | | // 提取地域关键词(去除"分公司"、"总公司"、"总部"后缀) |
| | | const region = organization.deptName.replace(/(分公司|总公司|总部)$/g, '').trim() |
| | | |
| | | // console.log('region', organization) |
| | | this.$emit('input', organization.deptId) |
| | | this.$emit('change', { |
| | | deptId: organization.deptId, |
| File was renamed from app/pages/task/create-emergency.vue |
| | |
| | | v-model="taskForm.transferTime" |
| | | type="datetime" |
| | | :placeholder="'请选择转运时间'" |
| | | class="form-input" |
| | | /> |
| | | </view> |
| | | |
| | |
| | | import { addTask } from "@/api/task" |
| | | import { listAvailableVehicles, getUserBoundVehicle } from "@/api/vehicle" |
| | | import { searchHospitals, searchHospitalsByDeptRegion } from "@/api/hospital" |
| | | import DepartureSelector from '@/components/DepartureSelector.vue' |
| | | import DepartureSelector from './components/DepartureSelector.vue' |
| | | import { calculateDistance, baiduDistanceByAddress, baiduPlaceSuggestion } from "@/api/map" |
| | | import { listBranchUsers } from "@/api/system/user" |
| | | import { searchIcd10 } from "@/api/icd10" |
| | |
| | | import { getDicts } from "@/api/dict" |
| | | import { getServiceOrdAreaTypes, getServiceOrderTypes, getHospitalDepartments } from "@/api/dictionary" |
| | | import { listBranchCompany, getDept } from "@/api/system/dept" |
| | | import MapSelector from '@/components/map-selector.vue' |
| | | import OrganizationSelector from '@/components/OrganizationSelector.vue' |
| | | import HospitalSelector from '@/components/HospitalSelector.vue' |
| | | import DiseaseSelector from '@/components/DiseaseSelector.vue' |
| | | import MapSelector from './components/map-selector.vue' |
| | | import OrganizationSelector from './components/OrganizationSelector.vue' |
| | | import HospitalSelector from './components/HospitalSelector.vue' |
| | | import DiseaseSelector from './components/DiseaseSelector.vue' |
| | | |
| | | export default { |
| | | components: { |
| File was renamed from app/pages/task/create-welfare.vue |
| | |
| | | import { addTask } from "@/api/task" |
| | | import { listAvailableVehicles } from "@/api/vehicle" |
| | | import { calculateDistance } from "@/api/map" |
| | | import MapSelector from '@/components/map-selector.vue' |
| | | import MapSelector from './components/map-selector.vue' |
| | | |
| | | export default { |
| | | components: { |
| File was renamed from app/pages/task/detail.vue |
| | |
| | | import { checkVehicleActiveTasks } from '@/api/task' |
| | | import { getPaymentInfo } from '@/api/payment' |
| | | import { formatDateTime } from '@/utils/common' |
| | | import AttachmentUpload from '@/components/AttachmentUpload.vue' |
| | | import AttachmentUpload from './components/AttachmentUpload.vue' |
| | | |
| | | export default { |
| | | components: { |
| | |
| | | if (taskType === 'EMERGENCY_TRANSFER') { |
| | | // 转运任务:跳转到转运任务编辑页面 |
| | | uni.navigateTo({ |
| | | url: `/pages/task/edit-emergency?id=${taskId}` |
| | | url: `/pagesTask/edit-emergency?id=${taskId}` |
| | | }) |
| | | } else if (taskType === 'WELFARE') { |
| | | // 福祗车任务:跳转到福祗车编辑页面 |
| | | uni.navigateTo({ |
| | | url: `/pages/task/edit-welfare?id=${taskId}` |
| | | url: `/pagesTask/edit-welfare?id=${taskId}` |
| | | }) |
| | | } else { |
| | | // 其他任务:跳转到通用任务编辑页面 |
| | | uni.navigateTo({ |
| | | url: `/pages/task/edit?id=${taskId}` |
| | | url: `/pagesTask/edit?id=${taskId}` |
| | | }) |
| | | } |
| | | }, |
| | |
| | | // 处理结算 |
| | | handleSettlement() { |
| | | uni.navigateTo({ |
| | | url: '/pages/task/settlement?taskId=' + this.taskId |
| | | url: '/pagesTask/settlement?taskId=' + this.taskId |
| | | }) |
| | | }, |
| | | |
| File was renamed from app/pages/task/edit-emergency.vue |
| | |
| | | import { listBranchUsers } from "@/api/system/user" |
| | | import { baiduDistanceByAddress } from "@/api/map" |
| | | import { calculateTransferPrice } from "@/api/price" |
| | | import MapSelector from '@/components/map-selector.vue' |
| | | import VehicleSelector from '@/components/VehicleSelector.vue' |
| | | import OrganizationSelector from '@/components/OrganizationSelector.vue' |
| | | import HospitalSelector from '@/components/HospitalSelector.vue' |
| | | import DiseaseSelector from '@/components/DiseaseSelector.vue' |
| | | import MapSelector from './components/map-selector.vue' |
| | | import VehicleSelector from './components/VehicleSelector.vue' |
| | | import OrganizationSelector from './components/OrganizationSelector.vue' |
| | | import HospitalSelector from './components/HospitalSelector.vue' |
| | | import DiseaseSelector from './components/DiseaseSelector.vue' |
| | | import distanceCalculator from '@/mixins/distanceCalculator.js' |
| | | |
| | | export default { |
| File was renamed from app/pages/task/edit-welfare.vue |
| | |
| | | import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue' |
| | | import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue' |
| | | import { getTask, updateTask } from "@/api/task" |
| | | import MapSelector from '@/components/map-selector.vue' |
| | | import VehicleSelector from '@/components/VehicleSelector.vue' |
| | | import MapSelector from './components/map-selector.vue' |
| | | import VehicleSelector from './components/VehicleSelector.vue' |
| | | import distanceCalculator from '@/mixins/distanceCalculator.js' |
| | | |
| | | export default { |
| File was renamed from app/pages/task/edit.vue |
| | |
| | | import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue' |
| | | import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue' |
| | | import { getTask, updateTask } from "@/api/task" |
| | | import MapSelector from '@/components/map-selector.vue' |
| | | import VehicleSelector from '@/components/VehicleSelector.vue' |
| | | import TaskTypeSelector from '@/components/TaskTypeSelector.vue' |
| | | import MapSelector from './components/map-selector.vue' |
| | | import VehicleSelector from './components/VehicleSelector.vue' |
| | | import TaskTypeSelector from './components/TaskTypeSelector.vue' |
| | | import distanceCalculator from '@/mixins/distanceCalculator.js' |
| | | |
| | | export default { |
| | |
| | | |
| | | # GPS服务配置 |
| | | gps: |
| | | mileage: |
| | | compensation: |
| | | days: 7 |
| | | segment: |
| | | # 分段计算时间间隔(单位:分钟) |
| | | minutes: 5 |
| | | calculate: |
| | | # 计算方法,可选值:haversine(haversine公式)、tianditu(天地图) |
| | | method: haversine |
| | | # 是否跳过已经计算的点 |
| | | skip: |
| | | calculated: true |
| | | service: |
| | | domain: https://gps51.com |
| | | username: 王某人 |
| | |
| | | List<String> onlyCms=cmsPlateNos.stream().filter(e->!notCmsVehicles.contains(e)).collect((Collectors.toList())); |
| | | |
| | | Integer syncCarCount=0; |
| | | response.getDevices().stream().filter(e->onlyCms.contains(this.getPlateNo(e.getVid()))).forEach(vehicle->{ |
| | | for (CmsVehicleDeviceListResponse.CmsVehicleDevice vehicle : response.getDevices()) { |
| | | try { |
| | | if (!onlyCms.contains(this.getPlateNo(vehicle.getVid()))) { |
| | | continue; |
| | | } |
| | | |
| | | String plateNo =this.getPlateNo(vehicle.getVid()); |
| | | |
| | | // 使用重试机制处理死锁 |
| | | int maxRetries = 3; |
| | | int retryCount = 0; |
| | | boolean success = false; |
| | | |
| | | while (!success && retryCount < maxRetries) { |
| | | try { |
| | | // 查询车辆信息(使用精确匹配) |
| | | VehicleInfo vehicleInfo= vehicleInfoService.selectVehicleInfoByPlateNumber(plateNo); |
| | | if (vehicleInfo==null) { |
| | | // 新增车辆 |
| | | vehicleInfo = new VehicleInfo(); |
| | | vehicleInfo.setVehicleNo(plateNo); |
| | | vehicleInfo.setDeviceId(vehicle.getDid()); |
| | | vehicleInfo.setPlatformCode("CMS"); |
| | | vehicleInfo.setStatus("0"); |
| | | vehicleInfoService.insertVehicleInfo(vehicleInfo); |
| | | } |
| | | else{ |
| | | vehicleInfo.setVehicleNo(plateNo); |
| | | syncCarCount++; |
| | | log.info("新增CMS车辆: {}", plateNo); |
| | | } else { |
| | | // 更新车辆 - 仅更新必要字段,避免触发关联表操作 |
| | | vehicleInfo.setDeviceId(vehicle.getDid()); |
| | | vehicleInfo.setPlatformCode("CMS"); |
| | | vehicleInfo.setStatus("0"); |
| | | vehicleInfo.setDeptIds(null); // 不更新部门关联,避免死锁 |
| | | vehicleInfoService.updateVehicleInfo(vehicleInfo); |
| | | syncCarCount++; |
| | | log.debug("更新CMS车辆: {}", plateNo); |
| | | } |
| | | |
| | | }); |
| | | success = true; |
| | | } catch (org.springframework.dao.DeadlockLoserDataAccessException e) { |
| | | retryCount++; |
| | | if (retryCount < maxRetries) { |
| | | log.warn("同步车辆 {} 遇到死锁,第{}次重试", plateNo, retryCount); |
| | | // 随机等待50-200ms后重试,避免多个线程同时重试 |
| | | Thread.sleep(50 + (long)(Math.random() * 150)); |
| | | } else { |
| | | log.error("同步车辆 {} 失败: 死锁重试{}次后仍失败", plateNo, maxRetries); |
| | | throw e; |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("同步车辆 {} 失败: {}", vehicle.getVid(), e.getMessage()); |
| | | // 继续处理下一个车辆 |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | |
| | | |
| | | import java.util.Calendar; |
| | | import java.util.Date; |
| | | import javax.annotation.PostConstruct; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | |
| | | @Autowired |
| | | private ISysConfigService configService; |
| | | |
| | | /** |
| | | * 服务启动时执行补偿计算 |
| | | * 检查最近7天内未被处理的GPS坐标并进行补偿计算 |
| | | */ |
| | | @PostConstruct |
| | | public void init() { |
| | | // 启动后延迟执行,避免影响服务启动速度 |
| | | new Thread(() -> { |
| | | try { |
| | | // 延迟30秒启动,确保所有服务已就绪 |
| | | Thread.sleep(30000); |
| | | |
| | | logger.info("========== 开始执行GPS分段里程补偿计算 =========="); |
| | | |
| | | // 获取配置的回溯天数,默认7天 |
| | | int lookbackDays = 7; |
| | | String lookbackConfig = configService.selectConfigByKey("gps.mileage.compensation.days"); |
| | | if (lookbackConfig != null && !lookbackConfig.isEmpty()) { |
| | | try { |
| | | lookbackDays = Integer.parseInt(lookbackConfig); |
| | | } catch (NumberFormatException e) { |
| | | logger.warn("补偿回溯天数配置错误,使用默认值7天"); |
| | | } |
| | | } |
| | | |
| | | // 执行补偿计算 |
| | | int successCount = segmentMileageService.compensateCalculation(lookbackDays); |
| | | |
| | | logger.info("========== GPS分段里程补偿计算完成 - 成功处理 {} 辆车 ==========", successCount); |
| | | |
| | | } catch (InterruptedException e) { |
| | | logger.error("补偿计算线程被中断", e); |
| | | Thread.currentThread().interrupt(); |
| | | } catch (Exception e) { |
| | | logger.error("GPS分段里程补偿计算失败", e); |
| | | } |
| | | }, "GPS-Compensation-Thread").start(); |
| | | } |
| | | |
| | | /** |
| | | * 计算最近一段时间的GPS分段里程 |
| | |
| | | } |
| | | } |
| | | |
| | | // 计算时间范围 |
| | | // 计算时间范围(向前回溯指定分钟数) |
| | | Calendar cal = Calendar.getInstance(); |
| | | Date endTime = cal.getTime(); |
| | | cal.add(Calendar.MINUTE, -minutes); |
| | | Date startTime = cal.getTime(); |
| | | |
| | | // 注意:此方法只计算最近时间段的数据,历史遗漏数据由补偿机制处理 |
| | | |
| | | logger.info("开始计算GPS分段里程 - 时间范围: {} 到 {}, 时间段间隔: {}分钟", |
| | | startTime, endTime, segmentMinutes); |
| | | |
| | |
| | | package com.ruoyi.system.mapper; |
| | | |
| | | import java.util.List; |
| | | |
| | | import com.ruoyi.common.annotation.DataSource; |
| | | import com.ruoyi.common.enums.DataSourceType; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import com.ruoyi.system.domain.SysTaskEmergency; |
| | | |
| | |
| | | * @author ruoyi |
| | | * @date 2024-01-16 |
| | | */ |
| | | @DataSource(DataSourceType.MASTER) |
| | | public interface SysTaskEmergencyMapper { |
| | | |
| | | /** |
| | |
| | | package com.ruoyi.system.mapper; |
| | | |
| | | import java.util.List; |
| | | |
| | | import com.ruoyi.common.annotation.DataSource; |
| | | import com.ruoyi.common.enums.DataSourceType; |
| | | import com.ruoyi.system.domain.SysTask; |
| | | import com.ruoyi.system.domain.vo.TaskQueryVO; |
| | | import com.ruoyi.system.domain.vo.TaskStatisticsVO; |
| | |
| | | * @author ruoyi |
| | | * @date 2024-01-15 |
| | | */ |
| | | @DataSource(DataSourceType.MASTER) |
| | | public interface SysTaskMapper { |
| | | |
| | | /** |
| | |
| | | /** |
| | | * 查询任务管理列表 |
| | | * |
| | | * @param sysTask 任务管理 |
| | | * @param queryVO 任务管理 |
| | | * @return 任务管理集合 |
| | | */ |
| | | public List<SysTask> selectSysTaskList(TaskQueryVO queryVO); |
| | |
| | | package com.ruoyi.system.mapper; |
| | | |
| | | import java.util.List; |
| | | |
| | | import com.ruoyi.common.annotation.DataSource; |
| | | import com.ruoyi.common.enums.DataSourceType; |
| | | import com.ruoyi.system.domain.SysTaskPayment; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| | |
| | | * @author ruoyi |
| | | * @date 2025-01-15 |
| | | */ |
| | | @DataSource(DataSourceType.MASTER) |
| | | public interface SysTaskPaymentMapper { |
| | | |
| | | /** |
| | |
| | | package com.ruoyi.system.mapper; |
| | | |
| | | import java.util.List; |
| | | |
| | | import com.ruoyi.common.annotation.DataSource; |
| | | import com.ruoyi.common.enums.DataSourceType; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | |
| | |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @DataSource(DataSourceType.MASTER) |
| | | public interface SysUserMapper |
| | | { |
| | | /** |
| | |
| | | * 检查GPS点是否已被计算 |
| | | */ |
| | | public Long selectGpsCalculatedSegmentId(@Param("gpsId") Long gpsId); |
| | | |
| | | /** |
| | | * 查询车辆在指定时间之前最后一个已处理的GPS坐标ID |
| | | * @param vehicleId 车辆ID |
| | | * @param beforeTime 截止时间(查询此时间之前的最后一个已处理GPS点) |
| | | * @return GPS坐标ID,如果没有则返回null |
| | | */ |
| | | public Long selectLastCalculatedGpsId(@Param("vehicleId") Long vehicleId, |
| | | @Param("beforeTime") Date beforeTime); |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | @DataSource(DataSourceType.SQLSERVER) |
| | | public boolean syncAdditionalFeeToLegacy(Long feeId) { |
| | | try { |
| | | // 1. 查询新系统附加费用记录 |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | public boolean syncAdditionalFeeFromLegacy(Long paidMoneyAddId) { |
| | | try { |
| | | // 1. 查询旧系统PaidMoney_Add记录 |
| | |
| | | } |
| | | |
| | | @Override |
| | | @DataSource(DataSourceType.SQLSERVER) |
| | | public int batchSyncAdditionalFeeFromLegacy(Integer hours) { |
| | | int successCount = 0; |
| | | try { |
| | |
| | | @Override |
| | | public int compensateCalculation(int lookbackDays) { |
| | | try { |
| | | // 计算时间范围 |
| | | // 计算时间范围(回溯指定天数) |
| | | Calendar cal = Calendar.getInstance(); |
| | | Date endTime = cal.getTime(); |
| | | cal.add(Calendar.DAY_OF_MONTH, -lookbackDays); |
| | |
| | | logger.info("车辆 {} 发现 {} 个未计算的GPS点,开始补偿计算...", |
| | | vehicleId, uncalculatedGps.size()); |
| | | |
| | | // 获取未计算GPS数据的时间范围 |
| | | Date uncalculatedStartTime = parseDateTime(uncalculatedGps.get(0).getCollectTime()); |
| | | Date uncalculatedEndTime = parseDateTime(uncalculatedGps.get(uncalculatedGps.size() - 1).getCollectTime()); |
| | | |
| | | // 查找该时间段之前最后一个已处理的GPS坐标ID |
| | | Long lastCalculatedGpsId = segmentMileageMapper.selectLastCalculatedGpsId(vehicleId, uncalculatedStartTime); |
| | | |
| | | if (lastCalculatedGpsId != null) { |
| | | logger.info("车辆 {} 找到最后一个已处理的GPS点ID: {},将与未处理数据一起计算", vehicleId, lastCalculatedGpsId); |
| | | |
| | | // 将最后一个已处理的GPS点加入列表前面,作为前置点 |
| | | VehicleGps lastCalculatedGps = vehicleGpsMapper.selectVehicleGpsById(lastCalculatedGpsId); |
| | | if (lastCalculatedGps != null) { |
| | | uncalculatedGps.add(0, lastCalculatedGps); // 插入到列表最前面 |
| | | logger.info("已将GPS点 {} 作为前置点加入计算列表", lastCalculatedGpsId); |
| | | } |
| | | } else { |
| | | logger.info("车辆 {} 没有找到已处理的前置 GPS点,从第一个未处理点开始计算", vehicleId); |
| | | } |
| | | |
| | | // 重新计算该车辆在该时间范围的分段里程 |
| | | // 注意:这里会重新计算整个时间范围,确保边缘节点被正确处理 |
| | | int segmentCount = calculateVehicleSegmentMileage(vehicleId, startTime, endTime); |
| | | int segmentCount = calculateVehicleSegmentMileageWithGpsList( |
| | | vehicleId, uncalculatedGps, uncalculatedStartTime, uncalculatedEndTime); |
| | | |
| | | if (segmentCount > 0) { |
| | | successCount++; |
| | |
| | | @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); |
| | | |
| | |
| | | return 0; |
| | | } |
| | | |
| | | logger.info("车辆ID: {} 查询到 {} 条GPS数据", vehicleId, gpsList.size()); |
| | | logger.info("车辆ID: {} 查询到 {} 条GPS数据 startTime:{},endTime:{}", vehicleId, gpsList.size(),startTime,endTime); |
| | | |
| | | return calculateVehicleSegmentMileageWithGpsList(vehicleId, gpsList, startTime, endTime); |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("计算车辆 {} 分段里程失败", vehicleId, e); |
| | | throw new RuntimeException("计算分段里程失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据提供的GPS列表计算车辆分段里程 |
| | | * @param vehicleId 车辆ID |
| | | * @param gpsList GPS列表(已按时间排序) |
| | | * @param startTime 起始时间 |
| | | * @param endTime 结束时间 |
| | | * @return 生成的分段数量 |
| | | */ |
| | | private int calculateVehicleSegmentMileageWithGpsList(Long vehicleId, List<VehicleGps> gpsList, |
| | | Date startTime, Date endTime) { |
| | | try { |
| | | // 验证输入数据 |
| | | if (gpsList == null || gpsList.isEmpty()) { |
| | | logger.debug("车辆ID: {} 在时间范围 {} 到 {} 内无GPS数据", vehicleId, startTime, endTime); |
| | | return 0; |
| | | } |
| | | |
| | | // 加载配置参数 |
| | | MileageCalculationConfig config = loadMileageCalculationConfig(); |
| | | |
| | | // 按时间段分组GPS数据 |
| | | Map<Date, List<VehicleGps>> segmentedData = segmentGpsDataByTime(gpsList, segmentMinutes); |
| | | Map<Date, List<VehicleGps>> segmentedData = segmentGpsDataByTime(gpsList, config.segmentMinutes); |
| | | |
| | | // 处理每个时间段并计算里程 |
| | | int savedCount = processSegmentedGpsData(vehicleId, segmentedData, config); |
| | | |
| | | logger.info("车辆 {} 计算完成,保存了 {} 个时间段的里程数据", vehicleId, savedCount); |
| | | |
| | | // 自动触发每日统计汇总 |
| | | if (savedCount > 0) { |
| | | triggerDailyMileageAggregation(vehicleId, segmentedData); |
| | | } |
| | | |
| | | return savedCount; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("计算车辆 {} 分段里程失败", vehicleId, e); |
| | | throw new RuntimeException("计算分段里程失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 加载里程计算配置参数 |
| | | */ |
| | | private MileageCalculationConfig loadMileageCalculationConfig() { |
| | | MileageCalculationConfig config = new MileageCalculationConfig(); |
| | | |
| | | // 获取时间间隔配置(分钟) |
| | | config.segmentMinutes = configService.selectConfigByKey("gps.mileage.segment.minutes") != null |
| | | ? Integer.parseInt(configService.selectConfigByKey("gps.mileage.segment.minutes")) |
| | | : 5; |
| | | |
| | | // 获取计算方式配置 |
| | | config.calculateMethod = configService.selectConfigByKey("gps.mileage.calculate.method"); |
| | | if (config.calculateMethod == null || config.calculateMethod.isEmpty()) { |
| | | config.calculateMethod = "tianditu"; |
| | | } |
| | | |
| | | // 获取是否跳过已计算GPS点的配置 |
| | | String skipCalculatedConfig = configService.selectConfigByKey("gps.mileage.skip.calculated"); |
| | | config.skipCalculated = skipCalculatedConfig == null || "true".equalsIgnoreCase(skipCalculatedConfig); |
| | | logger.info("控制跳过重复计算标识: {}", config.skipCalculated); |
| | | |
| | | return config; |
| | | } |
| | | |
| | | /** |
| | | * 处理分段后的GPS数据并计算里程 |
| | | * @param vehicleId 车辆ID |
| | | * @param segmentedData 分段后的GPS数据 |
| | | * @param config 计算配置 |
| | | * @return 成功保存的分段数量 |
| | | */ |
| | | private int processSegmentedGpsData(Long vehicleId, Map<Date, List<VehicleGps>> segmentedData, |
| | | MileageCalculationConfig config) { |
| | | int savedCount = 0; |
| | | VehicleGps previousSegmentLastPoint = null; // 记录上一个时间段的最后一个点 |
| | | |
| | |
| | | Date segmentStartTime = entry.getKey(); |
| | | List<VehicleGps> segmentGpsList = entry.getValue(); |
| | | |
| | | if (segmentGpsList.size() < 2) { |
| | | // 如果本段只有1个点,但有上一段的最后一个点,仍可计算跨段距离 |
| | | if (segmentGpsList.size() == 1 && previousSegmentLastPoint != null) { |
| | | // 保留当前点作为下一段的前置点,但不创建记录 |
| | | // 校验当前时间段数据 |
| | | if (!isSegmentValidForCalculation(segmentGpsList, previousSegmentLastPoint, vehicleId, segmentStartTime)) { |
| | | // 保留当前点作为下一段的前置点(如果有的话) |
| | | if (!segmentGpsList.isEmpty()) { |
| | | previousSegmentLastPoint = segmentGpsList.get(0); |
| | | } |
| | | continue; // 至少需要2个点才能计算距离 |
| | | continue; |
| | | } |
| | | |
| | | // 检查是否已存在该时间段的记录 |
| | | VehicleGpsSegmentMileage existing = segmentMileageMapper.selectByVehicleIdAndTime(vehicleId, segmentStartTime); |
| | | if (existing != null) { |
| | | logger.debug("车辆 {} 时间段 {} 的分段里程已存在,跳过", vehicleId, segmentStartTime); |
| | | // 更新上一段最后一个点 |
| | | if (isSegmentAlreadyCalculated(vehicleId, segmentStartTime, segmentGpsList)) { |
| | | previousSegmentLastPoint = segmentGpsList.get(segmentGpsList.size() - 1); |
| | | continue; |
| | | } |
| | | |
| | | // 计算并保存分段里程 |
| | | boolean success = calculateAndSaveSegment(vehicleId, segmentStartTime, segmentGpsList, |
| | | previousSegmentLastPoint, config); |
| | | if (success) { |
| | | savedCount++; |
| | | } |
| | | |
| | | // 更新上一段最后一个点,供下一段使用 |
| | | previousSegmentLastPoint = segmentGpsList.get(segmentGpsList.size() - 1); |
| | | } |
| | | |
| | | return savedCount; |
| | | } |
| | | |
| | | /** |
| | | * 校验时间段数据是否有效 |
| | | */ |
| | | private boolean isSegmentValidForCalculation(List<VehicleGps> segmentGpsList, |
| | | VehicleGps previousSegmentLastPoint, |
| | | Long vehicleId, Date segmentStartTime) { |
| | | // 如果当前段没有GPS点,跳过 |
| | | if (segmentGpsList == null || segmentGpsList.isEmpty()) { |
| | | return false; |
| | | } |
| | | |
| | | // 如果本段只有1个点,且没有上一段的最后一个点,无法计算距离 |
| | | if (segmentGpsList.size() == 1 && previousSegmentLastPoint == null) { |
| | | logger.debug("车辆 {} 时间段 {} 只有1个GPS点且无前置点,暂存待下一段计算", vehicleId, segmentStartTime); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 检查时间段是否已被计算 |
| | | */ |
| | | private boolean isSegmentAlreadyCalculated(Long vehicleId, Date segmentStartTime, List<VehicleGps> segmentGpsList) { |
| | | VehicleGpsSegmentMileage existing = segmentMileageMapper.selectByVehicleIdAndTime(vehicleId, segmentStartTime); |
| | | if (existing != null) { |
| | | logger.debug("车辆 {} 时间段 {} 的分段里程已存在,跳过", vehicleId, segmentStartTime); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 计算并保存单个时间段的里程 |
| | | */ |
| | | private boolean calculateAndSaveSegment(Long vehicleId, Date segmentStartTime, |
| | | List<VehicleGps> segmentGpsList, |
| | | VehicleGps previousSegmentLastPoint, |
| | | MileageCalculationConfig config) { |
| | | try { |
| | | // 计算时间段的结束时间 |
| | | Date segmentEndTime = calculateSegmentEndTime(segmentStartTime, config.segmentMinutes); |
| | | |
| | | // 计算该时间段的里程(包括跨段距离) |
| | | BigDecimal distance = calculateSegmentDistanceWithGap(segmentGpsList, config.calculateMethod, previousSegmentLastPoint); |
| | | |
| | | // 收集GPS ID列表 |
| | | List<Long> gpsIdList = collectGpsIds(segmentGpsList, previousSegmentLastPoint); |
| | | String gpsIds = gpsIdList.stream() |
| | | .map(String::valueOf) |
| | | .collect(java.util.stream.Collectors.joining(",")); |
| | | |
| | | // 创建分段里程记录 |
| | | VehicleGpsSegmentMileage segment = buildSegmentMileageRecord( |
| | | vehicleId, segmentStartTime, segmentEndTime, segmentGpsList, |
| | | distance, gpsIdList, gpsIds, config.calculateMethod); |
| | | |
| | | // 保存到数据库 |
| | | logger.info("保存车辆分时段里程到数据库中,车辆ID: {}, 时间段: {} 到 {}", vehicleId, segmentStartTime, segmentEndTime); |
| | | segmentMileageMapper.insertVehicleGpsSegmentMileage(segment); |
| | | |
| | | // 记录已计算的GPS点(如果开启了重复计算控制) |
| | | if (config.skipCalculated && segment.getSegmentId() != null) { |
| | | recordCalculatedGpsPoints(gpsIdList, segment.getSegmentId(), vehicleId); |
| | | } |
| | | |
| | | logger.debug("车辆 {} 时间段 {} 到 {} 里程: {}km, GPS点数: {}, GPS IDs: {}", |
| | | vehicleId, segmentStartTime, segmentEndTime, distance, segmentGpsList.size(), |
| | | gpsIds.length() > 50 ? gpsIds.substring(0, 50) + "..." : gpsIds); |
| | | |
| | | return true; |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("保存车辆 {} 时间段 {} 的里程记录失败", vehicleId, segmentStartTime, e); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算时间段结束时间 |
| | | */ |
| | | private Date calculateSegmentEndTime(Date segmentStartTime, int segmentMinutes) { |
| | | Calendar cal = Calendar.getInstance(); |
| | | cal.setTime(segmentStartTime); |
| | | cal.add(Calendar.MINUTE, segmentMinutes); |
| | | Date segmentEndTime = cal.getTime(); |
| | | return cal.getTime(); |
| | | } |
| | | |
| | | // 计算该时间段的里程(包括跨段距离) |
| | | BigDecimal distance = calculateSegmentDistanceWithGap(segmentGpsList, calculateMethod, previousSegmentLastPoint); |
| | | |
| | | // 收集GPS ID列表(包括上一段的最后一个点,因为跨段间隙距离也用到了它) |
| | | /** |
| | | * 收集GPS ID列表(包括前置点) |
| | | */ |
| | | private List<Long> collectGpsIds(List<VehicleGps> segmentGpsList, VehicleGps previousSegmentLastPoint) { |
| | | List<Long> gpsIdList = new ArrayList<>(); |
| | | // 如果有上一段的最后一个点,先添加它的ID |
| | | |
| | | // 如果有上一段的最后一个点,先添加它的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(",")); |
| | | |
| | | // 创建分段里程记录 |
| | | return gpsIdList; |
| | | } |
| | | |
| | | /** |
| | | * 构建分段里程记录对象 |
| | | */ |
| | | private VehicleGpsSegmentMileage buildSegmentMileageRecord(Long vehicleId, Date segmentStartTime, |
| | | Date segmentEndTime, List<VehicleGps> segmentGpsList, |
| | | BigDecimal distance, List<Long> gpsIdList, |
| | | String gpsIds, String calculateMethod) { |
| | | VehicleGpsSegmentMileage segment = new VehicleGpsSegmentMileage(); |
| | | segment.setVehicleId(vehicleId); |
| | | |
| | | // 从GPS数据或车辆表获取车牌号 |
| | | String vehicleNo = segmentGpsList.get(0).getVehicleNo(); |
| | | // 获取车牌号 |
| | | String vehicleNo = getVehicleNo(vehicleId, segmentGpsList.get(0)); |
| | | 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()); |
| | | segment.setGpsIds(gpsIds); |
| | | segment.setCalculateMethod(calculateMethod); |
| | | |
| | | // 查询并关联正在执行的任务 |
| | | associateActiveTask(segment, vehicleId, segmentStartTime, segmentEndTime); |
| | | |
| | | return segment; |
| | | } |
| | | |
| | | /** |
| | | * 获取车牌号 |
| | | */ |
| | | private String getVehicleNo(Long vehicleId, VehicleGps firstGps) { |
| | | String vehicleNo = firstGps.getVehicleNo(); |
| | | if (vehicleNo == null || vehicleNo.trim().isEmpty()) { |
| | | // GPS数据中没有车牌号,从车辆表查询 |
| | | VehicleInfo vehicleInfo = vehicleInfoMapper.selectVehicleInfoById(vehicleId); |
| | |
| | | vehicleNo = vehicleInfo.getVehicleNo(); |
| | | } |
| | | } |
| | | segment.setVehicleNo(vehicleNo); |
| | | return 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) { |
| | | /** |
| | | * 记录已计算的GPS点到状态表 |
| | | */ |
| | | private void recordCalculatedGpsPoints(List<Long> gpsIdList, Long segmentId, Long vehicleId) { |
| | | for (Long gpsId : gpsIdList) { |
| | | try { |
| | | segmentMileageMapper.insertGpsCalculated(gpsId, segment.getSegmentId(), vehicleId); |
| | | segmentMileageMapper.insertGpsCalculated(gpsId, segmentId, 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) { |
| | | /** |
| | | * 触发每日里程统计汇总 |
| | | */ |
| | | private void triggerDailyMileageAggregation(Long vehicleId, Map<Date, List<VehicleGps>> segmentedData) { |
| | | 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()); |
| | | } |
| | | // 获取涉及的日期范围 |
| | | Set<Date> affectedDates = extractAffectedDates(segmentedData); |
| | | |
| | | // 对每个涉及的日期,触发汇总 |
| | | for (Date statDate : affectedDates) { |
| | |
| | | } |
| | | } |
| | | |
| | | return savedCount; |
| | | /** |
| | | * 提取受影响的日期列表(用于汇总统计) |
| | | */ |
| | | private Set<Date> extractAffectedDates(Map<Date, List<VehicleGps>> segmentedData) { |
| | | Set<Date> affectedDates = new HashSet<>(); |
| | | Calendar cal = Calendar.getInstance(); |
| | | |
| | | } catch (Exception e) { |
| | | logger.error("计算车辆 {} 分段里程失败", vehicleId, e); |
| | | throw new RuntimeException("计算分段里程失败: " + e.getMessage()); |
| | | 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()); |
| | | } |
| | | |
| | | return affectedDates; |
| | | } |
| | | |
| | | /** |
| | | * 里程计算配置类 |
| | | */ |
| | | private static class MileageCalculationConfig { |
| | | int segmentMinutes; // 时间段间隔(分钟) |
| | | String calculateMethod; // 计算方式 |
| | | boolean skipCalculated; // 是否跳过已计算的GPS点 |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | /** |
| | | * 计算一个时间段内的总里程(包括与上一段的间隙距离) |
| | | * @param gpsList 当前时间段的GPS点列表 |
| | | * @param gpsList 当前时间段的GPS点列表(至少1个点) |
| | | * @param calculateMethod 计算方式 |
| | | * @param previousLastPoint 上一个时间段的最后一个点(可为null) |
| | | * @return 总里程(公里),保留3位小数 |
| | | */ |
| | | private BigDecimal calculateSegmentDistanceWithGap(List<VehicleGps> gpsList, String calculateMethod, VehicleGps previousLastPoint) { |
| | | if (gpsList == null || gpsList.size() < 2) { |
| | | if (gpsList == null || gpsList.isEmpty()) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | |
| | |
| | | String.format("%.3f", gapDistance)); |
| | | } |
| | | |
| | | // 2. 再计算当前段内部的距离 |
| | | // 2. 再计算当前段内部的距离(如果有2个或以上GPS点) |
| | | if (gpsList.size() >= 2) { |
| | | BigDecimal segmentInternalDistance; |
| | | if ("tianditu".equalsIgnoreCase(calculateMethod)) { |
| | | segmentInternalDistance = calculateDistanceByTianditu(gpsList); |
| | |
| | | segmentInternalDistance = calculateDistanceByHaversine(gpsList); |
| | | } |
| | | totalDistance = totalDistance.add(segmentInternalDistance); |
| | | } |
| | | // 如果只有1个点,段内距离为0,只计算跨段距离 |
| | | |
| | | return totalDistance.setScale(3, RoundingMode.HALF_UP); |
| | | } |
| | |
| | | * @param vehicleInfo 车辆信息 |
| | | * @return 结果 |
| | | */ |
| | | /** |
| | | * 修改车辆信息 |
| | | * 注意: |
| | | * - 如果需要更新部门关联,请设置 vehicleInfo.deptIds |
| | | * - 如果不需要更新部门关联(仅更新车辆基本信息),请保持 vehicleInfo.deptIds = null |
| | | * - 这样可以避免并发同步时的死锁问题 |
| | | * |
| | | * @param vehicleInfo 车辆信息 |
| | | * @return 结果 |
| | | */ |
| | | @Override |
| | | @Transactional |
| | | public int updateVehicleInfo(VehicleInfo vehicleInfo) { |
| | | // 只有当 deptIds 不为 null 时才更新部门关联(避免不必要的锁竞争) |
| | | if (vehicleInfo.getDeptIds() != null) { |
| | | // 先删除旧的关联关系 |
| | | vehicleInfoMapper.deleteVehicleDeptByVehicleId(vehicleInfo.getVehicleId()); |
| | | |
| | | // 如果选择了多个分公司,保存到关联表 |
| | | if (vehicleInfo.getDeptIds() != null && !vehicleInfo.getDeptIds().isEmpty()) { |
| | | // 如果选择了多个分公司,保存到关联表 |
| | | if (!vehicleInfo.getDeptIds().isEmpty()) { |
| | | insertVehicleDept(vehicleInfo); |
| | | } |
| | | } |
| | | |
| | | return vehicleInfoMapper.updateVehicleInfo(vehicleInfo); |
| | | } |
| | |
| | | // 计算任务里程占比 |
| | | if (result.totalMileage.compareTo(BigDecimal.ZERO) > 0) { |
| | | result.taskRatio = result.taskMileage.divide(result.totalMileage, 4, RoundingMode.HALF_UP); |
| | | |
| | | // 数据校验:占比应在0-1之间 |
| | | if (result.taskRatio.compareTo(BigDecimal.ONE) > 0) { |
| | | logger.warn("任务里程占比异常: {} (任务里程:{}, 总里程:{}), 强制设为1.0", |
| | | result.taskRatio, result.taskMileage, result.totalMileage); |
| | | result.taskRatio = BigDecimal.ONE; |
| | | } else if (result.taskRatio.compareTo(BigDecimal.ZERO) < 0) { |
| | | logger.warn("任务里程占比为负: {}, 强制设为0", result.taskRatio); |
| | | result.taskRatio = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | |
| | | // 保留两位小数 |
| | |
| | | BigDecimal taskRatio = BigDecimal.ZERO; |
| | | if (totalMileage.compareTo(BigDecimal.ZERO) > 0) { |
| | | taskRatio = taskMileage.divide(totalMileage, 4, RoundingMode.HALF_UP); |
| | | |
| | | // 数据校验:占比应在0-1之间,如果超出说明数据异常 |
| | | if (taskRatio.compareTo(BigDecimal.ONE) > 0) { |
| | | logger.warn("车辆ID: {} 日期: {} 任务里程占比异常: {} (任务里程:{}, 总里程:{}), 强制设为1.0", |
| | | vehicleId, statDate, taskRatio, taskMileage, totalMileage); |
| | | taskRatio = BigDecimal.ONE; |
| | | } else if (taskRatio.compareTo(BigDecimal.ZERO) < 0) { |
| | | logger.warn("车辆ID: {} 日期: {} 任务里程占比为负: {}, 强制设为0", |
| | | vehicleId, statDate, taskRatio); |
| | | taskRatio = BigDecimal.ZERO; |
| | | } |
| | | } |
| | | |
| | | // 5. 查询或创建统计记录 |
| | |
| | | |
| | | <!-- 根据编码列表查询分公司 --> |
| | | <select id="selectBranchCompaniesByOrderCodes" resultMap="SysDeptResult"> |
| | | select dept_id, dept_name, parent_id, ancestors, service_order_class, dispatch_order_class |
| | | select dept_id, dept_name, parent_id, ancestors, service_order_class, dispatch_order_class,departure_address, departure_longitude, departure_latitude |
| | | from sys_dept |
| | | where parent_id = 100 |
| | | and del_flag = '0' |
| | |
| | | <select id="selectGpsCalculatedSegmentId" resultType="Long"> |
| | | SELECT segment_id FROM tb_vehicle_gps_calculated WHERE gps_id = #{gpsId} LIMIT 1 |
| | | </select> |
| | | |
| | | <!-- 查询车辆在指定时间之前最后一个已处理的GPS坐标ID --> |
| | | <select id="selectLastCalculatedGpsId" resultType="Long"> |
| | | SELECT c.gps_id |
| | | FROM tb_vehicle_gps_calculated c |
| | | INNER JOIN tb_vehicle_gps g ON c.gps_id = g.gps_id |
| | | WHERE c.vehicle_id = #{vehicleId} |
| | | AND STR_TO_DATE(g.collect_time, '%Y-%m-%d %H:%i:%s') < #{beforeTime} |
| | | ORDER BY STR_TO_DATE(g.collect_time, '%Y-%m-%d %H:%i:%s') DESC |
| | | LIMIT 1 |
| | | </select> |
| | | </mapper> |
| | |
| | | <select id="selectVehicleInfoByPlateNumber" parameterType="String" resultMap="VehicleInfoResult"> |
| | | <include refid="selectVehicleInfoVo"/> |
| | | where v.vehicle_no LIKE concat('%', #{plateNumber}, '%') |
| | | limit 1 |
| | | </select> |
| | | |
| | | <select id="selectVehicleInfoByVehicleNo" parameterType="String" resultMap="VehicleInfoResult"> |
| | |
| | | s.data_source, s.create_time, s.update_time, vd.dept_id |
| | | </select> |
| | | |
| | | <!-- 根据车辆ID和统计日期查询唯一统计记录 --> |
| | | <select id="selectByVehicleIdAndDate" resultMap="VehicleMileageStatsResult"> |
| | | <include refid="selectVehicleMileageStatsVo"/> |
| | | 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, |
| | | 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, vd.dept_id |
| | | s.data_source, s.create_time, s.update_time |
| | | from tb_vehicle_mileage_stats s |
| | | where s.vehicle_id = #{vehicleId} and s.stat_date = #{statDate} |
| | | limit 1 |
| | | </select> |
| | | |
| | | <select id="selectTaskTimeIntervals" resultMap="TaskTimeIntervalResult"> |
| | |
| | | -- dryad-payment 支付模块数据表 |
| | | -- 在主数据库 966120 中创建支付模块所需的表 |
| | | |
| | | USE `966120`; |
| | | |
| | | -- 支付订单表 |
| | | CREATE TABLE IF NOT EXISTS `pay_order` ( |
| New file |
| | |
| | | -- ============================================ |
| | | -- 修复重复车牌号问题 |
| | | -- ============================================ |
| | | |
| | | -- 1. 查询重复的车牌号 |
| | | SELECT |
| | | vehicle_no, |
| | | COUNT(*) as count, |
| | | GROUP_CONCAT(vehicle_id ORDER BY vehicle_id) as vehicle_ids, |
| | | GROUP_CONCAT(platform_code ORDER BY vehicle_id) as platform_codes, |
| | | GROUP_CONCAT(create_time ORDER BY vehicle_id) as create_times |
| | | FROM tb_vehicle_info |
| | | GROUP BY vehicle_no |
| | | HAVING COUNT(*) > 1 |
| | | ORDER BY count DESC; |
| | | |
| | | -- 2. 查看具体重复记录的详情 |
| | | SELECT |
| | | vehicle_id, |
| | | vehicle_no, |
| | | platform_code, |
| | | device_id, |
| | | car_id, |
| | | status, |
| | | create_time, |
| | | update_time |
| | | FROM tb_vehicle_info |
| | | WHERE vehicle_no IN ( |
| | | SELECT vehicle_no |
| | | FROM tb_vehicle_info |
| | | GROUP BY vehicle_no |
| | | HAVING COUNT(*) > 1 |
| | | ) |
| | | ORDER BY vehicle_no, vehicle_id; |
| | | |
| | | -- 3. 备份重复数据(在删除前) |
| | | CREATE TABLE IF NOT EXISTS tb_vehicle_info_duplicate_backup AS |
| | | SELECT * FROM tb_vehicle_info |
| | | WHERE vehicle_no IN ( |
| | | SELECT vehicle_no |
| | | FROM tb_vehicle_info |
| | | GROUP BY vehicle_no |
| | | HAVING COUNT(*) > 1 |
| | | ); |
| | | |
| | | -- 4. 删除重复记录(保留最早创建的记录) |
| | | -- 注意:执行前请先确认备份完成! |
| | | -- 取消下面的注释来执行删除操作: |
| | | |
| | | /* |
| | | DELETE v1 FROM tb_vehicle_info v1 |
| | | INNER JOIN tb_vehicle_info v2 |
| | | WHERE v1.vehicle_no = v2.vehicle_no |
| | | AND v1.vehicle_id > v2.vehicle_id; |
| | | */ |
| | | |
| | | -- 5. 验证删除后的结果 |
| | | SELECT |
| | | vehicle_no, |
| | | COUNT(*) as count |
| | | FROM tb_vehicle_info |
| | | GROUP BY vehicle_no |
| | | HAVING COUNT(*) > 1; |
| | | |
| | | -- 6. 添加唯一索引(防止未来出现重复) |
| | | -- 注意:只有在确认没有重复数据后才能执行! |
| | | -- 取消下面的注释来添加唯一索引: |
| | | |
| | | /* |
| | | ALTER TABLE tb_vehicle_info |
| | | ADD UNIQUE INDEX uk_vehicle_no (vehicle_no); |
| | | */ |
| | | |
| | | -- 7. 查询统计信息 |
| | | SELECT |
| | | '总车辆数' as item, |
| | | COUNT(*) as count |
| | | FROM tb_vehicle_info |
| | | UNION ALL |
| | | SELECT |
| | | '唯一车牌数' as item, |
| | | COUNT(DISTINCT vehicle_no) as count |
| | | FROM tb_vehicle_info |
| | | UNION ALL |
| | | SELECT |
| | | 'CMS平台车辆' as item, |
| | | COUNT(*) as count |
| | | FROM tb_vehicle_info |
| | | WHERE platform_code = 'CMS' |
| | | UNION ALL |
| | | SELECT |
| | | 'GPS51平台车辆' as item, |
| | | COUNT(*) as count |
| | | FROM tb_vehicle_info |
| | | WHERE platform_code = 'GPS51' |
| | | UNION ALL |
| | | SELECT |
| | | 'LEGACY平台车辆' as item, |
| | | COUNT(*) as count |
| | | FROM tb_vehicle_info |
| | | WHERE platform_code = 'LEGACY'; |
| New file |
| | |
| | | -- ============================================ |
| | | -- 修复车辆里程统计表中的重复数据 |
| | | -- 问题: 同一车辆同一天存在多条统计记录 |
| | | -- 原因: 历史数据或并发插入导致 |
| | | -- 解决: 保留stats_id最大的记录,删除其他重复记录 |
| | | -- ============================================ |
| | | |
| | | -- 1. 查看重复数据 |
| | | SELECT vehicle_id, stat_date, COUNT(*) as count |
| | | FROM tb_vehicle_mileage_stats |
| | | GROUP BY vehicle_id, stat_date |
| | | HAVING COUNT(*) > 1 |
| | | ORDER BY count DESC; |
| | | |
| | | -- 2. 删除重复数据(保留stats_id最大的记录) |
| | | DELETE FROM tb_vehicle_mileage_stats |
| | | WHERE stats_id NOT IN ( |
| | | SELECT max_id FROM ( |
| | | SELECT MAX(stats_id) as max_id |
| | | FROM tb_vehicle_mileage_stats |
| | | GROUP BY vehicle_id, stat_date |
| | | ) as temp |
| | | ); |
| | | |
| | | -- 3. 验证清理结果 |
| | | SELECT vehicle_id, stat_date, COUNT(*) as count |
| | | FROM tb_vehicle_mileage_stats |
| | | GROUP BY vehicle_id, stat_date |
| | | HAVING COUNT(*) > 1; |
| | | |
| | | -- 4. 确认唯一索引存在 |
| | | SHOW INDEX FROM tb_vehicle_mileage_stats WHERE Key_name = 'uk_vehicle_date'; |
| | | |
| | | -- 5. 如果唯一索引不存在,则创建 |
| | | -- ALTER TABLE tb_vehicle_mileage_stats ADD UNIQUE KEY `uk_vehicle_date` (`vehicle_id`, `stat_date`); |
| New file |
| | |
| | | -- ============================================ |
| | | -- 修复 task_ratio 字段范围限制问题 |
| | | -- 问题: decimal(5,4) 最大值为 9.9999,可能不足以存储异常数据 |
| | | -- 解决: 扩大为 decimal(6,4),最大值 99.9999 |
| | | -- ============================================ |
| | | |
| | | -- 1. 查看当前字段定义 |
| | | DESC tb_vehicle_mileage_stats; |
| | | |
| | | -- 2. 修改字段类型 |
| | | ALTER TABLE tb_vehicle_mileage_stats |
| | | MODIFY COLUMN task_ratio DECIMAL(6,4) DEFAULT 0.0000 COMMENT '任务里程占比(0-1,异常时可能超出)'; |
| | | |
| | | -- 3. 验证修改结果 |
| | | DESC tb_vehicle_mileage_stats; |
| | | |
| | | -- 4. 查询超出正常范围的数据(占比应在0-1之间) |
| | | SELECT stats_id, vehicle_id, vehicle_no, stat_date, |
| | | total_mileage, task_mileage, task_ratio |
| | | FROM tb_vehicle_mileage_stats |
| | | WHERE task_ratio > 1.0 OR task_ratio < 0.0 |
| | | ORDER BY task_ratio DESC; |
| | | |
| | | -- 5. 修复异常数据(可选) |
| | | -- UPDATE tb_vehicle_mileage_stats SET task_ratio = 1.0 WHERE task_ratio > 1.0; |
| | | -- UPDATE tb_vehicle_mileage_stats SET task_ratio = 0.0 WHERE task_ratio < 0.0; |
| New file |
| | |
| | | -- GPS分段里程补偿计算配置 |
| | | -- 用于配置服务启动时的补偿计算回溯天数 |
| | | |
| | | -- 插入补偿回溯天数配置(默认7天) |
| | | INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark) |
| | | VALUES |
| | | ('GPS里程补偿回溯天数', 'gps.mileage.compensation.days', '7', 'N', 'admin', NOW(), 'admin', NOW(), |
| | | '服务启动时补偿计算的回溯天数,检查并处理该时间范围内未被计算的GPS坐标。默认7天。'); |
| | | |
| | | -- 说明: |
| | | -- 1. gps.mileage.compensation.days 控制服务启动时自动补偿计算的回溯天数 |
| | | -- 2. 补偿机制会在服务启动30秒后自动执行 |
| | | -- 3. 补偿计算会查找每辆车未被处理的GPS坐标,并与最后一个已处理的坐标一起计算距离 |
| | | -- 4. 已处理过的GPS坐标会被记录到 tb_vehicle_gps_calculated 表中,避免重复计算 |
| | | -- 5. 如果不希望启动时执行补偿计算,可将此配置值设为0 |
| | |
| | | -- 重新同步车辆和人员变更的任务到旧系统 |
| | | INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) |
| | | VALUES |
| | | ('重新同步车辆人员变更', 'DEFAULT', 'legacySystemSyncTask.resyncVehicleAndPersonnel', '0 0/5 * * * ?', '3', '1', '0', 'admin', sysdate(), |
| | | ('同步服务调度到旧系统变更', 'DEFAULT', 'legacySystemSyncTask.resyncVehicleAndPersonnel', '0 0/5 * * * ?', '3', '1', '0', 'admin', sysdate(), |
| | | '每5分钟自动重新同步车辆、人员、地址、成交价发生变更的任务到旧系统。当任务的车辆、人员、地址或费用被修改后,会标记need_resync=1,定时任务会将这些变更同步到旧系统调度单表。'); |
| | | |
| | | -- 说明: |
| | |
| | | -- 为车辆里程统计表添加分段数量字段 |
| | | -- 用途:记录当日关联的GPS分段数量,用于数据完整性校验和业务监控 |
| | | |
| | | USE `ry-vue`; |
| | | -- USE `ry-vue`; |
| | | |
| | | -- 检查字段是否存在,如果不存在则添加 |
| | | SET @dbname = DATABASE(); |
| | |
| | | |
| | | -- 验证字段是否添加成功 |
| | | DESC tb_vehicle_mileage_stats; |
| | | |
| | | |
| | | alter table tb_vehicle_mileage_stats add column segment_count int(11) default 0 comment '关联的分段数量' after task_count; |
| | |
| | | -- 为车辆GPS分段里程表添加任务关联字段 |
| | | -- 用途:在计算GPS里程时,自动关联车辆正在执行的任务,方便统计任务里程 |
| | | |
| | | USE ry-vue; |
| | | -- USE ry-vue; |
| | | |
| | | -- 添加任务ID字段 |
| | | ALTER TABLE tb_vehicle_gps_segment_mileage |