wlzboy
2025-11-15 caf56217dc2bf898b63b0e1f31a7098202c32825
sql/³µÁ¾Àï³Ìͳ¼ÆÊµÏÖ×ܽá.md
@@ -0,0 +1,339 @@
# è½¦è¾†GPS里程统计功能实现总结
## ä¸€ã€åŠŸèƒ½å®žçŽ°æ¸…å•
### âœ… 1. æ•°æ®åº“层
- [x] `vehicle_mileage_stats.sql` - åˆ›å»ºé‡Œç¨‹ç»Ÿè®¡è¡¨å’Œæ˜Žç»†è¡¨
- [x] `vehicle_mileage_stats_job.sql` - åˆ›å»ºå®šæ—¶ä»»åŠ¡é…ç½®
- [x] `vehicle_mileage_stats_menu.sql` - åˆ›å»ºèœå•权限配置
### âœ… 2. å®žä½“类(Domain)
- [x] `VehicleMileageStats.java` - é‡Œç¨‹ç»Ÿè®¡å®žä½“
- [x] `TaskTimeInterval.java` - ä»»åŠ¡æ—¶é—´åŒºé—´è¾…åŠ©ç±»
### âœ… 3. æ•°æ®è®¿é—®å±‚(Mapper)
- [x] `VehicleMileageStatsMapper.java` - é‡Œç¨‹ç»Ÿè®¡Mapper接口
- [x] `VehicleMileageStatsMapper.xml` - MyBatis映射配置
- [x] `VehicleGpsMapper.java` - æ‰©å±•GPS查询方法(新增2个方法)
- [x] `VehicleGpsMapper.xml` - æ‰©å±•GPS查询SQL
### âœ… 4. ä¸šåŠ¡é€»è¾‘å±‚ï¼ˆService)
- [x] `IVehicleMileageStatsService.java` - Service接口
- [x] `VehicleMileageStatsServiceImpl.java` - Service实现(核心算法)
### âœ… 5. æŽ§åˆ¶å±‚(Controller)
- [x] `VehicleMileageStatsController.java` - REST API接口
### âœ… 6. å®šæ—¶ä»»åŠ¡ï¼ˆTask)
- [x] `VehicleMileageStatsTask.java` - è‡ªåŠ¨ç»Ÿè®¡å®šæ—¶ä»»åŠ¡
### âœ… 7. å‰ç«¯API
- [x] `mileageStats.js` - å‰ç«¯æŽ¥å£å°è£…
### âœ… 8. æ–‡æ¡£
- [x] `车辆里程统计使用说明.md` - è¯¦ç»†ä½¿ç”¨æ–‡æ¡£
## äºŒã€æ ¸å¿ƒæŠ€æœ¯å®žçް
### 1. é‡Œç¨‹è®¡ç®—算法
#### Haversine公式(计算GPS点间距离)
```java
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
    double dLat = Math.toRadians(lat2 - lat1);
    double dLon = Math.toRadians(lon2 - lon1);
    double rLat1 = Math.toRadians(lat1);
    double rLat2 = Math.toRadians(lat2);
    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;
}
```
#### æ—¶é—´é‡å æ¯”例计算(任务里程分摊)
```java
private double calculateTaskOverlapRatio(Date segmentStart, Date segmentEnd,
                                          List<TaskTimeInterval> taskIntervals) {
    long segmentDuration = segmentEnd.getTime() - segmentStart.getTime();
    long totalOverlap = 0;
    for (TaskTimeInterval task : taskIntervals) {
        long overlapStart = Math.max(segmentStart.getTime(), task.getStartTime().getTime());
        long overlapEnd = Math.min(segmentEnd.getTime(), task.getEndTime().getTime());
        if (overlapEnd > overlapStart) {
            totalOverlap += (overlapEnd - overlapStart);
        }
    }
    return (double) totalOverlap / segmentDuration;
}
```
### 2. ä»»åŠ¡æ—¶æ®µå®šä¹‰
任务时段 = ä»Žä»»åŠ¡åˆ›å»ºæ—¶é—´ï¼ˆ`sys_task.create_time`)到任务完成时间(`sys_task.actual_end_time`)
SQL查询:
```sql
select tv.task_id, t.create_time as start_time,
       IFNULL(t.actual_end_time, NOW()) as end_time
from sys_task_vehicle tv
inner join sys_task t on tv.task_id = t.task_id
where tv.vehicle_id = #{vehicleId}
  and t.del_flag = '0'
  and t.actual_end_time is not null
  and t.create_time < #{endTime}
  and t.actual_end_time > #{startTime}
```
### 3. ç»Ÿè®¡æ•°æ®ç¼“å­˜
- æ¯æ—¥å®šæ—¶ä»»åŠ¡è‡ªåŠ¨ç»Ÿè®¡å‰ä¸€å¤©çš„æ•°æ®
- ç»Ÿè®¡ç»“果存储在 `tb_vehicle_mileage_stats` è¡¨ä¸­
- æ”¯æŒé‡å¤è®¡ç®—(更新已有记录)
- å”¯ä¸€ç´¢å¼•:`uk_vehicle_date (vehicle_id, stat_date)`
## ä¸‰ã€API接口说明
### 1. æŸ¥è¯¢ç»Ÿè®¡åˆ—表
```
GET /system/mileageStats/list
参数:
  - vehicleId: è½¦è¾†ID(可选)
  - vehicleNo: è½¦ç‰Œå·ï¼ˆå¯é€‰ï¼‰
  - statDate: ç»Ÿè®¡æ—¥æœŸï¼ˆå¯é€‰ï¼‰
  - beginStatDate: å¼€å§‹æ—¥æœŸï¼ˆå¯é€‰ï¼‰
  - endStatDate: ç»“束日期(可选)
  - pageNum: é¡µç 
  - pageSize: æ¯é¡µæ•°é‡
```
### 2. æ‰‹åŠ¨è®¡ç®—å•è½¦è¾†é‡Œç¨‹
```
POST /system/mileageStats/calculate
参数:
  - vehicleId: è½¦è¾†ID(必填)
  - statDate: ç»Ÿè®¡æ—¥æœŸï¼Œæ ¼å¼ yyyy-MM-dd(必填)
返回:VehicleMileageStats对象
```
### 3. æ‰¹é‡è®¡ç®—所有车辆里程
```
POST /system/mileageStats/batchCalculate
参数:
  - statDate: ç»Ÿè®¡æ—¥æœŸï¼Œæ ¼å¼ yyyy-MM-dd(必填)
返回:成功统计的车辆数量
```
### 4. å¯¼å‡ºç»Ÿè®¡æ•°æ®
```
POST /system/mileageStats/export
参数:同查询列表接口
返回:Excel文件
```
## å››ã€å®šæ—¶ä»»åŠ¡é…ç½®
### é»˜è®¤é…ç½®
- **任务名称**:车辆里程统计任务
- **Bean名称**:vehicleMileageStatsTask
- **方法调用**:calculateYesterdayMileage
- **Cron表达式**:`0 30 1 * * ?`(每天凌晨1:30执行)
- **执行策略**:立即执行
- **并发执行**:禁止
- **状态**:启用
### æ‰‹åŠ¨è§¦å‘æ–¹å¼
在系统管理 -> å®šæ—¶ä»»åŠ¡ä¸­ï¼Œå¯ä»¥æ‰‹åŠ¨æ‰§è¡Œï¼š
1. **统计昨日数据**:
   ```
   vehicleMileageStatsTask.calculateYesterdayMileage
   ```
2. **统计指定日期**:
   ```
   vehicleMileageStatsTask.calculateMileageByDate('2025-11-09')
   ```
## äº”、数据表结构
### tb_vehicle_mileage_stats
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž |
|--------|------|------|
| stats_id | bigint(20) | ç»Ÿè®¡ID,主键 |
| vehicle_id | bigint(20) | è½¦è¾†ID |
| vehicle_no | varchar(20) | è½¦ç‰Œå· |
| stat_date | date | ç»Ÿè®¡æ—¥æœŸ |
| total_mileage | decimal(10,2) | æ€»é‡Œç¨‹ï¼ˆå…¬é‡Œï¼‰ |
| task_mileage | decimal(10,2) | ä»»åŠ¡æ—¶æ®µé‡Œç¨‹ï¼ˆå…¬é‡Œï¼‰ |
| non_task_mileage | decimal(10,2) | éžä»»åŠ¡æ—¶æ®µé‡Œç¨‹ï¼ˆå…¬é‡Œï¼‰ |
| task_ratio | decimal(5,4) | ä»»åŠ¡é‡Œç¨‹å æ¯”ï¼ˆ0-1) |
| gps_point_count | int(11) | GPS点数量 |
| task_count | int(11) | ä»»åŠ¡æ•°é‡ |
| create_time | datetime | åˆ›å»ºæ—¶é—´ |
| update_time | datetime | æ›´æ–°æ—¶é—´ |
**索引:**
- PRIMARY KEY: stats_id
- UNIQUE KEY: uk_vehicle_date (vehicle_id, stat_date)
- KEY: idx_vehicle_id (vehicle_id)
- KEY: idx_stat_date (stat_date)
### tb_vehicle_mileage_detail(可选)
用于存储里程计算明细,方便调试和追溯。
## å…­ã€éƒ¨ç½²æ­¥éª¤
### 1. æ‰§è¡Œæ•°æ®åº“脚本(按顺序)
```bash
1. sql/vehicle_mileage_stats.sql
2. sql/vehicle_mileage_stats_job.sql
3. sql/vehicle_mileage_stats_menu.sql
```
### 2. é‡å¯åº”用
代码文件已自动创建,重启应用即可生效。
### 3. éªŒè¯éƒ¨ç½²
1. ç™»å½•系统,检查菜单是否显示"车辆里程统计"
2. è¿›å…¥ç³»ç»Ÿç®¡ç† -> å®šæ—¶ä»»åŠ¡ï¼Œæ£€æŸ¥æ˜¯å¦æœ‰"车辆里程统计任务"
3. æ‰‹åŠ¨æ‰§è¡Œå®šæ—¶ä»»åŠ¡æˆ–è°ƒç”¨API接口测试功能
## ä¸ƒã€ä½¿ç”¨ç¤ºä¾‹
### ç¤ºä¾‹1:手动计算某车辆昨日里程
```javascript
import { calculateMileage } from '@/api/mileageStats'
calculateMileage(1001, '2025-11-09').then(response => {
  console.log('统计结果:', response.data)
  // è¾“出示例:
  // {
  //   vehicleNo: '粤A12345',
  //   statDate: '2025-11-09',
  //   totalMileage: 285.67,
  //   taskMileage: 198.43,
  //   nonTaskMileage: 87.24,
  //   taskRatio: 0.6948,
  //   gpsPointCount: 1205,
  //   taskCount: 8
  // }
})
```
### ç¤ºä¾‹2:批量计算所有车辆指定日期里程
```javascript
import { batchCalculateMileage } from '@/api/mileageStats'
batchCalculateMileage('2025-11-09').then(response => {
  console.log(response.msg) // è¾“出:批量里程统计完成,成功统计 45 è¾†è½¦
})
```
### ç¤ºä¾‹3:查询车辆里程统计报表
```javascript
import { listMileageStats } from '@/api/mileageStats'
const query = {
  vehicleNo: '粤A12345',
  beginStatDate: '2025-11-01',
  endStatDate: '2025-11-09',
  pageNum: 1,
  pageSize: 10
}
listMileageStats(query).then(response => {
  console.log('统计列表:', response.rows)
})
```
## å…«ã€æ³¨æ„äº‹é¡¹
### 1. GPS数据要求
- GPS采集间隔:建议30-60秒
- æ•°æ®å­—段必填:vehicle_id, longitude, latitude, collect_time
- åæ ‡ç³»ç»Ÿï¼šæ”¯æŒWGS84、GCJ02等常用坐标系
### 2. ä»»åŠ¡æ•°æ®è¦æ±‚
- ä»»åŠ¡è¡¨ï¼šsys_task
- è½¦è¾†ä»»åŠ¡å…³è”è¡¨ï¼šsys_task_vehicle
- å¿…填字段:task_id, vehicle_id, create_time, actual_end_time
### 3. æ€§èƒ½ä¼˜åŒ–建议
- å®šæ—¶ä»»åŠ¡é¿å¼€ä¸šåŠ¡é«˜å³°æœŸï¼ˆå»ºè®®å‡Œæ™¨æ‰§è¡Œï¼‰
- GPS原始数据定期清理(建议保留7-30天)
- ç»Ÿè®¡æ•°æ®å®šæœŸå½’档(建议保留3-6个月)
### 4. æ•°æ®å‡†ç¡®æ€§
- é‡Œç¨‹è®¡ç®—基于GPS轨迹,精度受GPS信号质量影响
- Haversine公式计算的是直线距离,实际道路距离可能更长
- å¯ç»“合天地图路径规划API获取更准确的道路距离
## ä¹ã€æ‰©å±•功能建议
### 1. é›†æˆå¤©åœ°å›¾è·¯å¾„距离(更准确)
当前使用Haversine公式计算直线距离,可升级为:
- å°†GPS轨迹点发送到天地图路径规划API
- èŽ·å–å®žé™…é“è·¯è·ç¦»
- æé«˜é‡Œç¨‹ç»Ÿè®¡ç²¾åº¦
### 2. å®žæ—¶é‡Œç¨‹ç»Ÿè®¡
- åœ¨GPS数据入库时实时计算
- ä½¿ç”¨Redis缓存当日累计里程
- å‡Œæ™¨å®šæ—¶ä»»åŠ¡ä»…åšæ•°æ®å›ºåŒ–
### 3. é‡Œç¨‹å¼‚常告警
- å•日里程超过阈值告警
- é•¿æ—¶é—´æ— GPS数据告警
- é‡Œç¨‹çªå˜å¼‚常告警
### 4. æ•°æ®å¯è§†åŒ–
- æ¯æ—¥é‡Œç¨‹è¶‹åŠ¿å›¾
- ä»»åŠ¡é‡Œç¨‹å æ¯”é¥¼å›¾
- è½¦è¾†é‡Œç¨‹æŽ’名榜
## åã€æ•…障排查
### é—®é¢˜1:定时任务未执行
- æ£€æŸ¥å®šæ—¶ä»»åŠ¡çŠ¶æ€æ˜¯å¦ä¸º"启用"
- æ£€æŸ¥Cron表达式是否正确
- æŸ¥çœ‹å®šæ—¶ä»»åŠ¡æ—¥å¿—
### é—®é¢˜2:统计结果为0
- æ£€æŸ¥GPS数据是否存在
- æ£€æŸ¥GPS数据的collect_time字段是否正确
- æ£€æŸ¥ä»»åŠ¡æ•°æ®æ˜¯å¦å­˜åœ¨
### é—®é¢˜3:里程数据异常
- æ£€æŸ¥GPS坐标是否合法(经纬度范围)
- æ£€æŸ¥æ˜¯å¦å­˜åœ¨GPS漂移点
- å¯ç”¨æ˜Žç»†è¡¨åˆ†æžæ¯æ®µè·ç¦»
### æŸ¥çœ‹æ—¥å¿—
```bash
# Service层日志
grep "VehicleMileageStatsServiceImpl" logs/ruoyi-*.log
# å®šæ—¶ä»»åŠ¡æ—¥å¿—
grep "VehicleMileageStatsTask" logs/ruoyi-*.log
```
## åä¸€ã€æ€»ç»“
✅ æœ¬åŠŸèƒ½å·²å®Œæ•´å®žçŽ°è½¦è¾†GPS里程统计的所有需求:
- âœ… æ¯æ—¥è‡ªåŠ¨ç»Ÿè®¡è½¦è¾†è¡Œé©¶é‡Œç¨‹
- âœ… åŒºåˆ†ä»»åŠ¡æ—¶æ®µå’Œéžä»»åŠ¡æ—¶æ®µé‡Œç¨‹
- âœ… è®¡ç®—任务里程占比
- âœ… ç»Ÿè®¡æ•°æ®ç¼“存到数据库表
- âœ… æ”¯æŒæ‰‹åŠ¨è§¦å‘å’Œæ‰¹é‡è®¡ç®—
- âœ… æä¾›å®Œæ•´çš„æŸ¥è¯¢å’Œå¯¼å‡ºåŠŸèƒ½
- âœ… é›†æˆå®šæ—¶ä»»åŠ¡è‡ªåŠ¨åŒ–æ‰§è¡Œ
核心算法采用Haversine公式计算GPS点间距离,按时间重叠比例分摊里程到任务和非任务时段,确保统计准确性。所有数据缓存在专用统计表中,支持高效查询和分析。