wlzboy
2025-12-02 d294abb765e4ed349907c92ce313689c6299ba7d
feat:地图都改为天地图的接口
52个文件已修改
11个文件已添加
4687 ■■■■ 已修改文件
GPS分段里程计算超时问题修复说明.md 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/map.js 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/wechat.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/index.vue 1664 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/message/index.vue 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/utils/subscribe.js 327 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/pom.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java 530 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleManagementController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/wechat/WechatController.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-dev.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-prod.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-test.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/MapServiceConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/WechatConfig.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/PlateNumberExtractor.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/WechatUtils.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/CmsVehicleSyncTask.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleGpsSegmentMileageTask.java 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleSyncTask.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/config/MapServiceConfiguration.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IMapService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskVehicleService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatTaskNotifyService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/AdditionalFeeSyncServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaiduMapServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/GpsCollectServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 122 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskVehicleServiceImpl.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TiandituMapServiceImpl.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskVehicleMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/task.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/system/gps/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/index.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/vehicle/index.vue 205 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/add_gps_collect_time_index.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/add_legacy_service_ord_no.sql 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_duplicate_mileage_stats.sql 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/gps_compensation_job.sql 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/optimize_gps_query_performance.sql 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
旧系统ServiceOrdNo字段同步功能说明.md 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
GPS·Ö¶ÎÀï³Ì¼ÆË㳬ʱÎÊÌâÐÞ¸´ËµÃ÷.md
New file
@@ -0,0 +1,315 @@
# GPS分段里程计算任务超时问题修复说明
## é—®é¢˜æè¿°
GPS分段里程计算定时任务执行时出现数据库连接超时错误:
```
ERROR c.r.q.t.VehicleGpsSegmentMileageTask - GPS分段里程计算任务执行失败
java.lang.RuntimeException: æ‰¹é‡è®¡ç®—失败:
### Error querying database.  Cause: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet successfully received from the server was 60,022 milliseconds ago.
```
## é—®é¢˜åŽŸå› 
1. **数据库Socket超时配置过短**:`socketTimeout: 60000`(60秒),对于大数据量GPS查询处理不足
2. **查询性能不佳**:缺少必要的数据库索引,导致大表扫描耗时过长
3. **错误处理不完善**:单个车辆计算失败会影响整个批处理流程
4. **缺少进度日志**:无法了解批量处理的实时进度
## è§£å†³æ–¹æ¡ˆ
### 1. å¢žåŠ æ•°æ®åº“è¿žæŽ¥è¶…æ—¶æ—¶é—´
**修改文件**:
- `ruoyi-admin/src/main/resources/application-dev.yml`
- `ruoyi-admin/src/main/resources/application-test.yml`
- `ruoyi-admin/src/main/resources/application-prod.yml`
**修改内容**:
```yaml
# é…ç½®ç½‘络超时时间(从60秒增加到5分钟)
socketTimeout: 300000
```
**原因**:GPS分段里程计算需要处理大量数据,60秒的超时时间不足以完成查询操作。增加到5分钟(300秒)可以避免正常查询被误判为超时。
### 2. ä¼˜åŒ–数据库索引
**执行SQL文件**:`sql/optimize_gps_query_performance.sql`
**关键索引**:
```sql
-- ç»„合索引:vehicle_id + collect_time
ALTER TABLE tb_vehicle_gps
ADD INDEX idx_vehicle_collect_time (vehicle_id, collect_time);
-- å•列索引:collect_time
ALTER TABLE tb_vehicle_gps
ADD INDEX idx_collect_time (collect_time);
-- tb_vehicle_gps_calculated表索引
ALTER TABLE tb_vehicle_gps_calculated
ADD INDEX idx_gps_id (gps_id);
ADD INDEX idx_vehicle_id (vehicle_id);
```
**效果**:
- å¤§å¹…提升按车辆ID和时间范围查询的性能
- ä¼˜åŒ–活跃车辆查询速度
- æ”¹å–„LEFT JOIN查询性能
### 3. å¢žå¼ºæ‰¹å¤„理错误处理
**修改文件**:`VehicleGpsSegmentMileageServiceImpl.java`
**改进点**:
1. **单车失败不影响全局**:单个车辆计算失败不会中断整个批处理
2. **详细的进度日志**:每处理10辆车输出一次进度
3. **成功/失败统计**:记录成功和失败的车辆数量
4. **更详细的错误信息**:包含堆栈跟踪,便于问题定位
**代码改进**:
```java
int successCount = 0;
int failedCount = 0;
for (int i = 0; i < vehicleIds.size(); i++) {
    try {
        // å¤„理单个车辆
        int segmentCount = calculateVehicleSegmentMileage(vehicleId, startTime, endTime);
        if (segmentCount > 0) {
            successCount++;
        }
    } catch (Exception e) {
        failedCount++;
        logger.error("计算车辆失败,但继续处理下一辆", e);
        // ä¸ä¸­æ–­æ‰¹å¤„理
    }
    // è¿›åº¦æ—¥å¿—
    if ((i + 1) % 10 == 0) {
        logger.info("进度: {}/{}, æˆåŠŸ: {}, å¤±è´¥: {}", ...);
    }
}
```
## éƒ¨ç½²æ­¥éª¤
### 1. æ•°æ®åº“优化(必须)
```bash
# è¿žæŽ¥åˆ°MySQL数据库
mysql -u root -p
# æ‰§è¡Œç´¢å¼•优化SQL
source sql/optimize_gps_query_performance.sql
# éªŒè¯ç´¢å¼•创建成功
SHOW INDEX FROM tb_vehicle_gps;
SHOW INDEX FROM tb_vehicle_gps_calculated;
```
### 2. åº”用配置更新(必须)
1. å¤‡ä»½å½“前配置文件
2. æ›´æ–°é…ç½®æ–‡ä»¶ä¸­çš„`socketTimeout`参数
3. é‡å¯åº”用服务
```bash
# åœæ­¢æœåŠ¡
./ry.sh stop
# å¯åŠ¨æœåŠ¡
./ry.sh start
# æŸ¥çœ‹æ—¥å¿—
tail -f logs/sys-info.log
```
### 3. éªŒè¯ä¿®å¤æ•ˆæžœ
1. **手动触发定时任务**:
   - ç™»å½•后台管理系统
   - è¿›å…¥ã€ç³»ç»Ÿç›‘控】->【定时任务】
   - æ‰¾åˆ°"GPS分段里程实时计算"任务
   - ç‚¹å‡»ã€æ‰§è¡Œä¸€æ¬¡ã€‘按钮
2. **查看执行日志**:
```bash
# æŸ¥çœ‹GPS计算日志
grep "VehicleGpsSegmentMileageTask" logs/sys-info.log
# æŸ¥çœ‹æˆåŠŸ/失败统计
grep "批量分段里程计算完成" logs/sys-info.log
```
3. **检查数据库**:
```sql
-- æŸ¥çœ‹æœ€æ–°çš„分段里程记录
SELECT * FROM tb_vehicle_gps_segment_mileage
ORDER BY create_time DESC
LIMIT 10;
-- ç»Ÿè®¡ä»Šæ—¥å¤„理的车辆数
SELECT COUNT(DISTINCT vehicle_id) as vehicle_count
FROM tb_vehicle_gps_segment_mileage
WHERE DATE(create_time) = CURDATE();
```
## æ€§èƒ½ç›‘控建议
### 1. å®šæ—¶ä»»åŠ¡æ‰§è¡Œæ—¶é—´ç›‘æŽ§
在定时任务管理中查看执行时长:
- æ­£å¸¸æƒ…况:每5分钟执行一次,处理10分钟数据,应在1-2分钟内完成
- å¼‚常情况:超过3分钟需要关注,可能存在性能问题
### 2. æ•°æ®åº“慢查询日志
检查MySQL慢查询日志:
```sql
-- æŸ¥çœ‹æ…¢æŸ¥è¯¢é…ç½®
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- å¯ç”¨æ…¢æŸ¥è¯¢æ—¥å¿—(如果未启用)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
```
### 3. Druid监控
访问Druid监控页面:`http://your-domain/druid/`
- ç”¨æˆ·åï¼šruoyi
- å¯†ç ï¼š123456
关注指标:
- SQL执行时间
- è¿žæŽ¥æ± ä½¿ç”¨æƒ…况
- æ…¢SQL列表
## åŽç»­ä¼˜åŒ–建议
### 1. å®šæœŸæ¸…理历史数据
GPS数据量大,建议定期清理历史数据:
```sql
-- ä¿ç•™æœ€è¿‘90天的GPS原始数据
DELETE FROM tb_vehicle_gps
WHERE collect_time < DATE_SUB(NOW(), INTERVAL 90 DAY);
-- ä¼˜åŒ–表空间
OPTIMIZE TABLE tb_vehicle_gps;
```
可以创建定时任务每月执行一次。
### 2. åˆ†æ‰¹å¤„理大量车辆
如果车辆数量超过1000辆,建议修改批处理逻辑,分批处理:
```java
// æ¯æ‰¹å¤„理100辆车辆
int batchSize = 100;
for (int i = 0; i < vehicleIds.size(); i += batchSize) {
    List<Long> batchIds = vehicleIds.subList(i,
        Math.min(i + batchSize, vehicleIds.size()));
    processBatch(batchIds);
}
```
### 3. ä½¿ç”¨å¼‚步处理
对于特别耗时的操作,可以考虑使用异步处理:
```java
@Async
public CompletableFuture<Integer> calculateVehicleAsync(Long vehicleId) {
    // å¼‚步计算
}
```
### 4. å¢žåŠ ç¼“å­˜æœºåˆ¶
对于频繁查询的车辆信息,可以使用Redis缓存:
```java
@Cacheable(value = "vehicleInfo", key = "#vehicleId")
public VehicleInfo getVehicleInfo(Long vehicleId) {
    return vehicleInfoMapper.selectVehicleInfoById(vehicleId);
}
```
## ç›¸å…³æ–‡ä»¶æ¸…单
### ä¿®æ”¹çš„æ–‡ä»¶
1. `ruoyi-admin/src/main/resources/application-dev.yml` - å¼€å‘环境配置
2. `ruoyi-admin/src/main/resources/application-test.yml` - æµ‹è¯•环境配置
3. `ruoyi-admin/src/main/resources/application-prod.yml` - ç”Ÿäº§çŽ¯å¢ƒé…ç½®
4. `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java` - æ‰¹å¤„理逻辑优化
5. `ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml` - æ·»åŠ SQL注释
### æ–°å¢žçš„æ–‡ä»¶
1. `sql/optimize_gps_query_performance.sql` - æ•°æ®åº“性能优化SQL
## æ•…障排查
### é—®é¢˜1:执行SQL时报错"索引已存在"
**解决方案**:
```sql
-- åˆ é™¤å·²å­˜åœ¨çš„索引
DROP INDEX idx_vehicle_collect_time ON tb_vehicle_gps;
DROP INDEX idx_collect_time ON tb_vehicle_gps;
-- é‡æ–°åˆ›å»ºç´¢å¼•
-- ç„¶åŽæ‰§è¡Œoptimize_gps_query_performance.sql
```
### é—®é¢˜2:修改配置后仍然超时
**检查步骤**:
1. ç¡®è®¤é…ç½®æ–‡ä»¶ä¿®æ”¹ç”Ÿæ•ˆï¼šæ£€æŸ¥å½“前激活的profile(dev/test/prod)
2. ç¡®è®¤åº”用已重启:`ps -ef | grep java`
3. æŸ¥çœ‹Druid连接池配置:访问 `/druid/` æ£€æŸ¥socketTimeout值
4. æ£€æŸ¥æ•°æ®åº“服务器配置:`SHOW VARIABLES LIKE 'wait_timeout';`
### é—®é¢˜3:索引创建后性能未改善
**检查步骤**:
```sql
-- 1. éªŒè¯ç´¢å¼•是否被使用
EXPLAIN SELECT DISTINCT vehicle_id
FROM tb_vehicle_gps
WHERE collect_time >= DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 2. æ›´æ–°è¡¨ç»Ÿè®¡ä¿¡æ¯
ANALYZE TABLE tb_vehicle_gps;
-- 3. æ£€æŸ¥è¡¨æ•°æ®é‡
SELECT COUNT(*) FROM tb_vehicle_gps;
```
## æ€»ç»“
本次修复主要解决了GPS分段里程计算任务的超时问题,通过以下措施:
1. âœ… å¢žåŠ æ•°æ®åº“è¿žæŽ¥è¶…æ—¶æ—¶é—´ï¼ˆ60秒 -> 300秒)
2. âœ… æ·»åŠ æ•°æ®åº“ç´¢å¼•ä¼˜åŒ–æŸ¥è¯¢æ€§èƒ½
3. âœ… æ”¹è¿›æ‰¹å¤„理错误处理机制
4. âœ… å¢žå¼ºæ—¥å¿—输出,便于监控和问题定位
**预期效果**:
- ä»»åŠ¡æ‰§è¡Œä¸å†è¶…æ—¶
- æŸ¥è¯¢æ€§èƒ½æå‡50%以上
- å•个车辆失败不影响整体处理
- å®žæ—¶äº†è§£å¤„理进度
**注意事项**:
- å¿…须执行数据库索引优化SQL
- å¿…须重启应用使配置生效
- å»ºè®®å®šæœŸæ¸…理历史GPS数据
- æŒç»­ç›‘控任务执行情况
app/api/map.js
@@ -8,7 +8,8 @@
  }
  
  return request({
    url: '/system/gps/address/search',
//    url: '/system/gps/address/search',
    url: '/system/gps/tianditu/place/suggestion',
    method: 'get',
    params: {
      keyword: keyword,
@@ -28,9 +29,12 @@
  if (isNaN(lat) || isNaN(lng)) {
    return Promise.reject(new Error('参数无效,经纬度坐标格式错误'))
  }
  /**
   * è¿™é‡Œç”¨åˆ°å¤©åœ°å›¾
   */
  return request({
    url: '/system/gps/address/geocoder',
    // url: '/system/gps/address/geocoder',
    url: '/system/gps/tianditu/reverseGeocoding',
    method: 'get',
    params: {
      lat: lat,
@@ -39,6 +43,21 @@
  })
}
export function calculateTianDiTuDistance(fromAddress, toAddress) {
  // å‚数验证
  if (!fromAddress || !toAddress) {
    return Promise.reject(new Error('参数不完整,缺少地址'))
  }
  return request({
    url: '/system/gps/tianditu/distance/byAddress',
    method: 'get',
    params: {
      fromAddress: fromAddress,
      toAddress: toAddress
    }
  })
}
// åœ°å›¾è·¯çº¿è§„划API(计算两点间距离)
export function calculateDistance(fromLat, fromLng, toLat, toLng) {
  // å‚数验证
@@ -56,6 +75,7 @@
  
  return request({
    url: '/system/gps/route/distance',
    // url: '/system/gps/tianditu/byAddress',
    method: 'get',
    params: {
      fromLat: fromLat,
app/api/wechat.js
@@ -33,4 +33,12 @@
    method: 'post',
    data: data
  })
}
// èŽ·å–å¾®ä¿¡é…ç½®ï¼ˆåŒ…æ‹¬è®¢é˜…æ¶ˆæ¯æ¨¡æ¿ID等)
export function getWechatConfig() {
  return request({
    url: '/wechat/config',
    method: 'get'
  })
}
app/pages/index.vue
@@ -5,25 +5,45 @@
      <view class="user-info-content">
        <view class="user-details">
          <view class="user-info-row">
            <text class="user-name">{{ userName || '未登录' }}</text>
            <text class="separator" v-if="currentUser.branchCompanyName">|</text>
            <text class="user-name">{{ userName || "未登录" }}</text>
            <text class="separator" v-if="currentUser.branchCompanyName"
              >|</text
            >
            <view class="branch-company" v-if="currentUser.branchCompanyName">
              <uni-icons type="location" size="16" color="#666" style="margin-right: 4rpx;"></uni-icons>
              <uni-icons
                type="location"
                size="16"
                color="#666"
                style="margin-right: 4rpx"
              ></uni-icons>
              <text>{{ currentUser.branchCompanyName }}</text>
            </view>
            <text class="separator" v-if="boundVehicle">|</text>
            <view class="vehicle-info" @click.stop="goToBindVehicle" v-if="boundVehicle">
            <view
              class="vehicle-info"
              @click.stop="goToBindVehicle"
              v-if="boundVehicle"
            >
              <text>{{ boundVehicle }}</text>
              <uni-icons
                type="loop"
                size="16"
              <uni-icons
                type="loop"
                size="16"
                color="#007AFF"
                style="margin-left: 4rpx;"
                style="margin-left: 4rpx"
              ></uni-icons>
            </view>
          </view>
          <view class="bind-vehicle-btn" v-if="!boundVehicle" @click="goToBindVehicle">
            <uni-icons type="plus-filled" size="16" color="#007AFF" style="margin-right: 4rpx;"></uni-icons>
          <view
            class="bind-vehicle-btn"
            v-if="!boundVehicle"
            @click="goToBindVehicle"
          >
            <uni-icons
              type="plus-filled"
              size="16"
              color="#007AFF"
              style="margin-right: 4rpx"
            ></uni-icons>
            <text>绑定车牌</text>
          </view>
        </view>
@@ -36,9 +56,30 @@
        <uni-icons type="chat" size="24" color="#007AFF"></uni-icons>
      </view>
      <view class="message-text">消息中心</view>
      <view class="unread-dot" v-if="unreadMessageCount > 0">{{ unreadMessageCount }}</view>
      <view class="unread-dot" v-if="unreadMessageCount > 0">{{
        unreadMessageCount
      }}</view>
      <view class="arrow">
        <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
      </view>
    </view>
    <!-- è®¢é˜…通知提示卡片(未订阅时显示) -->
    <view
      class="subscribe-banner"
      v-if="!hasSubscribed"
      @click="clickConfirmsubscribeTaskNotify"
    >
      <view class="banner-icon">
        <uni-icons type="bell" size="28" color="#ff9500"></uni-icons>
      </view>
      <view class="banner-content">
        <view class="banner-title">开启任务通知</view>
        <view class="banner-desc">及时接收任务分配和状态更新提醒</view>
      </view>
      <view class="banner-action">
        <text>立即开启</text>
        <uni-icons type="arrowright" size="16" color="#007AFF"></uni-icons>
      </view>
    </view>
@@ -54,17 +95,38 @@
          <view class="task-main" @click="viewTaskDetail(task)">
            <!-- ä»»åŠ¡å¤´éƒ¨ï¼šæ ‡é¢˜å’ŒçŠ¶æ€æ ‡ç­¾ -->
            <view class="task-header">
              <view class="task-title">{{ getTaskTypeText(task.type) }} - {{ task.vehicle }}</view>
              <view class="task-status" :class="task.taskStatus === 'PENDING' ? 'status-pending' : task.taskStatus === 'DEPARTING' ? 'status-departing' : task.taskStatus === 'ARRIVED' ? 'status-arrived' : task.taskStatus === 'RETURNING' ? 'status-returning' : task.taskStatus === 'COMPLETED' ? 'status-completed' : task.taskStatus === 'CANCELLED' ? 'status-cancelled' : task.taskStatus === 'IN_PROGRESS' ? 'status-in-progress' : 'status-default'">
              <view class="task-title"
                >{{ getTaskTypeText(task.type) }} - {{ task.vehicle }}</view
              >
              <view
                class="task-status"
                :class="
                  task.taskStatus === 'PENDING'
                    ? 'status-pending'
                    : task.taskStatus === 'DEPARTING'
                    ? 'status-departing'
                    : task.taskStatus === 'ARRIVED'
                    ? 'status-arrived'
                    : task.taskStatus === 'RETURNING'
                    ? 'status-returning'
                    : task.taskStatus === 'COMPLETED'
                    ? 'status-completed'
                    : task.taskStatus === 'CANCELLED'
                    ? 'status-cancelled'
                    : task.taskStatus === 'IN_PROGRESS'
                    ? 'status-in-progress'
                    : 'status-default'
                "
              >
                {{ getStatusText(task.status) }}
              </view>
            </view>
            <!-- ä»»åŠ¡ç¼–å·å•ç‹¬ä¸€è¡Œ -->
            <view class="task-code-row">
              <text class="task-code">{{ task.taskNo }}</text>
            </view>
            <!-- ä»»åŠ¡è¯¦ç»†ä¿¡æ¯ -->
            <view class="task-info">
              <view class="info-row">
@@ -89,65 +151,65 @@
              </view>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="task-actions">
            <!-- å¾…处理状态: æ˜¾ç¤ºå‡ºå‘、取消 -->
            <template v-if="task.taskStatus === 'PENDING'">
              <button
                class="action-btn primary"
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'depart')"
              >
                å‡ºå‘
              </button>
              <button
                class="action-btn cancel"
              <button
                class="action-btn cancel"
                @click="handleTaskAction(task, 'cancel')"
              >
                å–消
              </button>
            </template>
            <!-- å‡ºå‘中状态: æ˜¾ç¤ºå·²åˆ°è¾¾ã€å¼ºåˆ¶ç»“束 -->
            <template v-else-if="task.taskStatus === 'DEPARTING'">
              <button
                class="action-btn primary"
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'arrive')"
              >
                å·²åˆ°è¾¾
              </button>
              <button
                class="action-btn cancel"
              <button
                class="action-btn cancel"
                @click="handleTaskAction(task, 'forceCancel')"
              >
                å¼ºåˆ¶ç»“束
              </button>
            </template>
            <!-- å·²åˆ°è¾¾çŠ¶æ€: æ˜¾ç¤ºå·²è¿”程 -->
            <template v-else-if="task.taskStatus === 'ARRIVED'">
              <button
                class="action-btn primary"
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'return')"
              >
                å·²è¿”程
              </button>
            </template>
            <!-- è¿”程中状态: æ˜¾ç¤ºå·²å®Œæˆ -->
            <template v-else-if="task.taskStatus === 'RETURNING'">
              <button
                class="action-btn primary"
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'complete')"
              >
                å·²å®Œæˆ
              </button>
            </template>
            <!-- å·²å®Œæˆ/已取消: ä¸æ˜¾ç¤ºæŒ‰é’® -->
          </view>
        </view>
        <view class="no-data" v-if="runningTasks.length === 0">
          <uni-icons type="info" size="40" color="#ccc"></uni-icons>
          <text>暂无正在运行的任务</text>
@@ -158,192 +220,244 @@
</template>
<script>
  import { mapState } from 'vuex'
  import { getMyTasks, changeTaskStatus } from '@/api/task'
  import { getUserProfile } from '@/api/system/user'
  import { getUserBoundVehicle } from '@/api/vehicle'
  import { getUnreadCount } from '@/api/message'
  import { formatDateTime } from '@/utils/common'
  export default {
    data() {
      return {
        // ç”¨æˆ·ç»‘定的车辆信息
        boundVehicle: '',
        boundVehicleId: null,
        // æ¶ˆæ¯æ•°æ®
        messages: [],
        unreadMessageCount: 0,
        // æ­£åœ¨è¿è¡Œçš„任务列表
        taskList: [],
        loading: false
      }
import { mapState } from "vuex";
import { getMyTasks, changeTaskStatus } from "@/api/task";
import { getUserProfile } from "@/api/system/user";
import { getUserBoundVehicle } from "@/api/vehicle";
import { getUnreadCount } from "@/api/message";
import { formatDateTime } from "@/utils/common";
import subscribeManager from "@/utils/subscribe";
export default {
  data() {
    return {
      // ç”¨æˆ·ç»‘定的车辆信息
      boundVehicle: "",
      boundVehicleId: null,
      // æ¶ˆæ¯æ•°æ®
      messages: [],
      unreadMessageCount: 0,
      // æ­£åœ¨è¿è¡Œçš„任务列表
      taskList: [],
      loading: false,
      // è®¢é˜…状态
      hasSubscribed: true,
    };
  },
  computed: {
    ...mapState({
      userName: (state) => state.user.nickName,
      currentUser: (state) => state.user,
    }),
    // æ­£åœ¨è¿è¡Œçš„任务(待处理和各种处理中的任务)
    runningTasks() {
      return this.taskList.filter((task) => {
        // åŒ…含待处理、出发中、已到达、返程中等所有未完成的状态
        return [
          "PENDING",
          "DEPARTING",
          "ARRIVED",
          "RETURNING",
          "IN_PROGRESS",
        ].includes(task.taskStatus);
      });
    },
    computed: {
      ...mapState({
        userName: state => state.user.nickName,
        currentUser: state => state.user
      }),
      // æ­£åœ¨è¿è¡Œçš„任务(待处理和各种处理中的任务)
      runningTasks() {
        return this.taskList.filter(task => {
          // åŒ…含待处理、出发中、已到达、返程中等所有未完成的状态
          return ['PENDING', 'DEPARTING', 'ARRIVED', 'RETURNING', 'IN_PROGRESS'].includes(task.taskStatus)
  },
  onLoad() {
    // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
    const userId = this.currentUser.userId;
    if (!userId) {
      console.log("用户未登录,跳过加载数据");
      return;
    }
    // æ£€æŸ¥è®¢é˜…状态(先检查本地,后面会检查微信官方状态)
    this.hasSubscribed = subscribeManager.checkLocalSubscribeStatus();
    // è‡ªåŠ¨è®¢é˜…ï¼ˆå¦‚æžœæœªè®¢é˜…åˆ™æ˜¾ç¤ºç¡®è®¤å¼¹çª—ï¼‰
    // this.autoSubscribeOnLaunch();
    // åŠ è½½ç”¨æˆ·ç»‘å®šè½¦è¾†ä¿¡æ¯
    this.loadUserVehicle();
    // åŠ è½½æ­£åœ¨è¿è¡Œçš„ä»»åŠ¡
    this.loadRunningTasks();
    // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
    this.loadUnreadMessageCount();
  },
  onShow() {
    // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
    const userId = this.currentUser.userId;
    if (!userId) {
      console.log("用户未登录,跳过加载数据");
      return;
    }
    // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°ä»»åŠ¡åˆ—è¡¨ã€ç»‘å®šè½¦è¾†å’Œæ¶ˆæ¯æ•°é‡
    this.loadUserVehicle();
    this.loadRunningTasks();
    this.loadUnreadMessageCount();
  },
  onPullDownRefresh() {
    // ä¸‹æ‹‰åˆ·æ–°
    this.loadRunningTasks();
    setTimeout(() => {
      uni.stopPullDownRefresh();
    }, 1000);
  },
  methods: {
    // è‡ªåŠ¨è®¢é˜…ï¼ˆå°ç¨‹åºå¯åŠ¨æ—¶è°ƒç”¨ï¼‰
    autoSubscribeOnLaunch() {
      subscribeManager.autoSubscribe()
        .then((result) => {
          if (result.skipped) {
            console.log('用户已订阅,无需重复订阅');
            this.hasSubscribed = true;
          } else if (result.success) {
            this.hasSubscribed = true;
            console.log('自动订阅成功');
          } else {
            // è®¢é˜…失败或被拒绝,更新状态
            this.hasSubscribed = false;
          }
          // å¦‚果返回了状态信息,输出详细状态
          if (result.status) {
            console.log('详细订阅状态:', result.status);
          }
        })
        .catch((error) => {
          console.log('自动订阅取消或失败:', error);
          this.hasSubscribed = false;
        });
    },
    // åŠ è½½ç”¨æˆ·ç»‘å®šçš„è½¦è¾†ä¿¡æ¯
    loadUserVehicle() {
      const userId = this.currentUser.userId;
      if (!userId) {
        console.error("用户未登录,无法获取绑定车辆信息");
        this.boundVehicle = "";
        this.boundVehicleId = null;
        return;
      }
      getUserBoundVehicle(userId)
        .then((response) => {
          if (response.code === 200 && response.data) {
            const vehicle = response.data;
            this.boundVehicle = vehicle.vehicleNumber || "未知车牌";
            this.boundVehicleId = vehicle.vehicleId;
            console.log("用户绑定车辆:", this.boundVehicle);
          } else {
            this.boundVehicle = "";
            this.boundVehicleId = null;
          }
        })
        .catch((error) => {
          console.error("获取绑定车辆信息失败:", error);
          this.boundVehicle = "";
          this.boundVehicleId = null;
        });
    },
    // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
    loadUnreadMessageCount() {
      // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
      const userId = this.currentUser.userId;
      if (!userId) {
        console.log("用户未登录,跳过获取未读消息数量");
        return;
      }
      getUnreadCount()
        .then((response) => {
          if (response.code === 200) {
            this.unreadMessageCount = response.data || 0;
            // æ›´æ–°TabBar徽标
            this.updateTabBarBadge(this.unreadMessageCount);
          }
        })
        .catch((error) => {
          console.error("获取未读消息数量失败:", error);
        });
    },
    // æ›´æ–°TabBar徽标
    updateTabBarBadge(count) {
      if (count > 0) {
        uni.setTabBarBadge({
          index: 3, // æ¶ˆæ¯é¡µé¢åœ¨tabBar中的索引
          text: count > 99 ? "99+" : count.toString(),
        });
      } else {
        uni.removeTabBarBadge({
          index: 3,
        });
      }
    },
    onLoad() {
      // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
      const userId = this.currentUser.userId
    // åŠ è½½ç”¨æˆ·ä¿¡æ¯ï¼ˆä¿ç•™ä»¥å…¼å®¹ä¹‹å‰çš„ä»£ç ï¼‰
    loadUserProfile() {
      const userId = this.currentUser.userId;
      if (!userId) {
        console.log('用户未登录,跳过加载数据')
        return
        console.error("用户未登录,无法获取用户信息");
        return;
      }
      // åŠ è½½ç”¨æˆ·ç»‘å®šè½¦è¾†ä¿¡æ¯
      this.loadUserVehicle()
      // åŠ è½½æ­£åœ¨è¿è¡Œçš„ä»»åŠ¡
      this.loadRunningTasks()
      // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
      this.loadUnreadMessageCount()
    },
    onShow() {
      // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
      const userId = this.currentUser.userId
      if (!userId) {
        console.log('用户未登录,跳过加载数据')
        return
      }
      // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°ä»»åŠ¡åˆ—è¡¨ã€ç»‘å®šè½¦è¾†å’Œæ¶ˆæ¯æ•°é‡
      this.loadUserVehicle()
      this.loadRunningTasks()
      this.loadUnreadMessageCount()
    },
    onPullDownRefresh() {
      // ä¸‹æ‹‰åˆ·æ–°
      this.loadRunningTasks()
      setTimeout(() => {
        uni.stopPullDownRefresh()
      }, 1000)
    },
    methods: {
      // åŠ è½½ç”¨æˆ·ç»‘å®šçš„è½¦è¾†ä¿¡æ¯
      loadUserVehicle() {
        const userId = this.currentUser.userId
        if (!userId) {
          console.error('用户未登录,无法获取绑定车辆信息')
          this.boundVehicle = ''
          this.boundVehicleId = null
          return
        }
        getUserBoundVehicle(userId).then(response => {
          if (response.code === 200 && response.data) {
            const vehicle = response.data
            this.boundVehicle = vehicle.vehicleNumber || '未知车牌'
            this.boundVehicleId = vehicle.vehicleId
            console.log('用户绑定车辆:', this.boundVehicle)
          } else {
            this.boundVehicle = ''
            this.boundVehicleId = null
          }
        }).catch(error => {
          console.error('获取绑定车辆信息失败:', error)
          this.boundVehicle = ''
          this.boundVehicleId = null
        })
      },
      // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
      loadUnreadMessageCount() {
        // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
        const userId = this.currentUser.userId
        if (!userId) {
          console.log('用户未登录,跳过获取未读消息数量')
          return
        }
        getUnreadCount().then(response => {
          if (response.code === 200) {
            this.unreadMessageCount = response.data || 0
            // æ›´æ–°TabBar徽标
            this.updateTabBarBadge(this.unreadMessageCount)
          }
        }).catch(error => {
          console.error('获取未读消息数量失败:', error)
        })
      },
      // æ›´æ–°TabBar徽标
      updateTabBarBadge(count) {
        if (count > 0) {
          uni.setTabBarBadge({
            index: 3, // æ¶ˆæ¯é¡µé¢åœ¨tabBar中的索引
            text: count > 99 ? '99+' : count.toString()
          })
        } else {
          uni.removeTabBarBadge({
            index: 3
          })
        }
      },
      // åŠ è½½ç”¨æˆ·ä¿¡æ¯ï¼ˆä¿ç•™ä»¥å…¼å®¹ä¹‹å‰çš„ä»£ç ï¼‰
      loadUserProfile() {
        const userId = this.currentUser.userId
        if (!userId) {
          console.error('用户未登录,无法获取用户信息')
          return
        }
        getUserProfile().then(response => {
          const userInfo = response.data || response
      getUserProfile()
        .then((response) => {
          const userInfo = response.data || response;
          // èŽ·å–ç”¨æˆ·ç»‘å®šçš„è½¦è¾†ä¿¡æ¯
          if (userInfo.boundVehicle) {
            this.boundVehicle = userInfo.boundVehicle.vehicleNumber
            this.boundVehicleId = userInfo.boundVehicle.vehicleId
            this.boundVehicle = userInfo.boundVehicle.vehicleNumber;
            this.boundVehicleId = userInfo.boundVehicle.vehicleId;
          }
        }).catch(error => {
          console.error('获取用户信息失败:', error)
        })
      },
      // åŠ è½½æ­£åœ¨è¿è¡Œçš„ä»»åŠ¡
      loadRunningTasks() {
        const userId = this.currentUser.userId
        if (!userId) {
          console.error('用户未登录,无法加载任务列表')
          return
        }
        this.loading = true
        // ä½¿ç”¨ /task/my æŽ¥å£èŽ·å–å½“å‰ç”¨æˆ·ç›¸å…³çš„æ‰€æœ‰ä»»åŠ¡ï¼ˆç”¨æˆ·åˆ›å»ºã€åˆ†é…ç»™ç”¨æˆ·ã€æ‰§è¡Œäººæ˜¯ç”¨æˆ·ï¼‰
        getMyTasks().then(response => {
          this.loading = false
        .catch((error) => {
          console.error("获取用户信息失败:", error);
        });
    },
    // åŠ è½½æ­£åœ¨è¿è¡Œçš„ä»»åŠ¡
    loadRunningTasks() {
      const userId = this.currentUser.userId;
      if (!userId) {
        console.error("用户未登录,无法加载任务列表");
        return;
      }
      this.loading = true;
      // ä½¿ç”¨ /task/my æŽ¥å£èŽ·å–å½“å‰ç”¨æˆ·ç›¸å…³çš„æ‰€æœ‰ä»»åŠ¡ï¼ˆç”¨æˆ·åˆ›å»ºã€åˆ†é…ç»™ç”¨æˆ·ã€æ‰§è¡Œäººæ˜¯ç”¨æˆ·ï¼‰
      getMyTasks()
        .then((response) => {
          this.loading = false;
          // æ ¹æ®åŽç«¯è¿”回的数据结构进行解析
          const data = response.data || response.rows || response || []
          const data = response.data || response.rows || response || [];
          // è¿‡æ»¤å‡ºæœªå®Œæˆçš„任务
          const allTasks = Array.isArray(data) ? data : []
          const allTasks = Array.isArray(data) ? data : [];
          this.taskList = allTasks
            .filter(task => {
            .filter((task) => {
              // åªæ˜¾ç¤ºæœªå®Œæˆå’Œæœªå–消的任务
              return task.taskStatus !== 'COMPLETED' && task.taskStatus !== 'CANCELLED'
              return (
                task.taskStatus !== "COMPLETED" &&
                task.taskStatus !== "CANCELLED"
              );
            })
            .map(task => {
            .map((task) => {
              // ä»ŽassignedVehicles数组中获取车辆信息
              let vehicleInfo = '未分配车辆'
              let vehicleInfo = "未分配车辆";
              if (task.assignedVehicles && task.assignedVehicles.length > 0) {
                const firstVehicle = task.assignedVehicles[0]
                vehicleInfo = firstVehicle.vehicleNo || '未知车牌'
                const firstVehicle = task.assignedVehicles[0];
                vehicleInfo = firstVehicle.vehicleNo || "未知车牌";
                if (task.assignedVehicles.length > 1) {
                  vehicleInfo += ` ç­‰${task.assignedVehicles.length}辆`
                  vehicleInfo += ` ç­‰${task.assignedVehicles.length}辆`;
                }
              }
              return {
                ...task,
                // æ ¼å¼åŒ–显示字段
@@ -351,566 +465,678 @@
                type: task.taskType,
                vehicle: vehicleInfo,
                vehicleList: task.assignedVehicles || [],
                startLocation: this.formatAddress(task.departureAddress || task.startLocation || '未设置'),
                endLocation: this.formatAddress(task.destinationAddress || task.endLocation || '未设置'),
                startTime: task.plannedStartTime ? formatDateTime(task.plannedStartTime, 'YYYY-MM-DD HH:mm') : '未设置',
                assignee: task.assigneeName || '未分配',
                taskNo: task.taskCode || '未知编号',
                status: this.convertStatus(task.taskStatus) // è½¬æ¢çŠ¶æ€æ ¼å¼ä»¥å…¼å®¹æ—§UI
              }
            })
        }).catch(error => {
          this.loading = false
          console.error('加载任务列表失败:', error)
                startLocation: this.formatAddress(
                  task.departureAddress || task.startLocation || "未设置"
                ),
                endLocation: this.formatAddress(
                  task.destinationAddress || task.endLocation || "未设置"
                ),
                startTime: task.plannedStartTime
                  ? formatDateTime(task.plannedStartTime, "YYYY-MM-DD HH:mm")
                  : "未设置",
                assignee: task.assigneeName || "未分配",
                taskNo: task.taskCode || "未知编号",
                status: this.convertStatus(task.taskStatus), // è½¬æ¢çŠ¶æ€æ ¼å¼ä»¥å…¼å®¹æ—§UI
              };
            });
        })
      },
      // æ ¼å¼åŒ–地址 - åªæ˜¾ç¤º-前面的部分
      formatAddress(address) {
        if (!address) return '未设置'
        const dashIndex = address.indexOf('-')
        if (dashIndex > 0) {
          return address.substring(0, dashIndex)
        }
        return address
      },
      // è½¬æ¢çŠ¶æ€æ ¼å¼ï¼ˆå°†æ•°æ®åº“çŠ¶æ€è½¬æ¢ä¸ºUI使用的状态)
      convertStatus(dbStatus) {
        const statusMap = {
          'PENDING': 'pending',
          'DEPARTING': 'processing',
          'ARRIVED': 'processing',
          'RETURNING': 'processing',
          'IN_PROGRESS': 'processing',
          'COMPLETED': 'completed',
          'CANCELLED': 'cancelled'
        }
        return statusMap[dbStatus] || 'pending'
      },
      // è·³è½¬åˆ°ç»‘定车辆页面
      goToBindVehicle() {
        // è·³è½¬åˆ°ç»‘定车辆的页面
        this.$tab.navigateTo('/pages/bind-vehicle');
      },
      // è·³è½¬åˆ°æ¶ˆæ¯é¡µé¢
      goToMessages() {
        this.$tab.switchTab('/pages/message/index');
      },
      // æŸ¥çœ‹ä»»åŠ¡è¯¦æƒ…
      viewTaskDetail(task) {
        // è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…é¡µé¢ - ä½¿ç”¨taskId
        this.$tab.navigateTo(`/pagesTask/detail?id=${task.taskId || task.id}`);
      },
      // å¤„理任务操作
      handleTaskAction(task, action) {
        switch (action) {
          case 'depart':
            // å‡ºå‘ -> çŠ¶æ€å˜ä¸ºå‡ºå‘ä¸­
            this.$modal.confirm('确定要出发吗?').then(() => {
              this.updateTaskStatus(task.taskId, 'DEPARTING', '任务已出发')
            }).catch(() => {});
            break;
          case 'cancel':
            // å–消 -> äºŒæ¬¡ç¡®è®¤åŽçŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
            this.$modal.confirm('确定要取消此任务吗?').then(() => {
              this.updateTaskStatus(task.taskId, 'CANCELLED', '任务已取消')
            }).catch(() => {});
            break;
          case 'arrive':
            // å·²åˆ°è¾¾ -> çŠ¶æ€å˜ä¸ºå·²åˆ°è¾¾
            this.$modal.confirm('确认已到达目的地?').then(() => {
              this.updateTaskStatus(task.taskId, 'ARRIVED', '已到达目的地')
            }).catch(() => {});
            break;
          case 'forceCancel':
            // å¼ºåˆ¶ç»“束 -> çŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
            this.$modal.confirm('确定要强制结束此任务吗?').then(() => {
              this.updateTaskStatus(task.taskId, 'CANCELLED', '任务已强制结束')
            }).catch(() => {});
            break;
          case 'return':
            // å·²è¿”程 -> çŠ¶æ€å˜ä¸ºè¿”ç¨‹ä¸­
            this.$modal.confirm('确认开始返程?').then(() => {
              this.updateTaskStatus(task.taskId, 'RETURNING', '已开始返程')
            }).catch(() => {});
            break;
          case 'complete':
            // å·²å®Œæˆ -> çŠ¶æ€å˜ä¸ºå·²å®Œæˆ
            this.$modal.confirm('确认任务已完成?').then(() => {
              this.updateTaskStatus(task.taskId, 'COMPLETED', '任务已完成')
            }).catch(() => {});
            break;
        }
      },
      // æ›´æ–°ä»»åŠ¡çŠ¶æ€
      updateTaskStatus(taskId, status, remark) {
        // èŽ·å–GPS位置信息
        this.getLocationAndUpdateStatus(taskId, status, remark)
      },
      // èŽ·å–ä½ç½®ä¿¡æ¯å¹¶æ›´æ–°çŠ¶æ€
      getLocationAndUpdateStatus(taskId, status, remark) {
        const that = this
        // ä½¿ç”¨uni.getLocation获取GPS位置
        uni.getLocation({
          type: 'gcj02',
          geocode: true,
          altitude: true,
          success: function(res) {
            console.log('GPS定位成功:', res)
            const statusData = {
              taskStatus: status,
              remark: remark,
              latitude: res.latitude,
              longitude: res.longitude,
              locationAddress: res.address ? res.address.street || res.address.poiName || '' : '',
              locationProvince: res.address ? res.address.province || '' : '',
              locationCity: res.address ? res.address.city || '' : '',
              locationDistrict: res.address ? res.address.district || '' : '',
              gpsAccuracy: res.accuracy,
              altitude: res.altitude,
              speed: res.speed,
              heading: res.direction || res.heading
            }
            changeTaskStatus(taskId, statusData).then(response => {
              that.$modal.showToast('状态更新成功')
              that.loadRunningTasks()
            }).catch(error => {
              console.error('更新任务状态失败:', error)
              that.$modal.showToast('状态更新失败,请重试')
        .catch((error) => {
          this.loading = false;
          console.error("加载任务列表失败:", error);
        });
    },
    // æ ¼å¼åŒ–地址 - åªæ˜¾ç¤º-前面的部分
    formatAddress(address) {
      if (!address) return "未设置";
      const dashIndex = address.indexOf("-");
      if (dashIndex > 0) {
        return address.substring(0, dashIndex);
      }
      return address;
    },
    // è½¬æ¢çŠ¶æ€æ ¼å¼ï¼ˆå°†æ•°æ®åº“çŠ¶æ€è½¬æ¢ä¸ºUI使用的状态)
    convertStatus(dbStatus) {
      const statusMap = {
        PENDING: "pending",
        DEPARTING: "processing",
        ARRIVED: "processing",
        RETURNING: "processing",
        IN_PROGRESS: "processing",
        COMPLETED: "completed",
        CANCELLED: "cancelled",
      };
      return statusMap[dbStatus] || "pending";
    },
    // è·³è½¬åˆ°ç»‘定车辆页面
    goToBindVehicle() {
      // è·³è½¬åˆ°ç»‘定车辆的页面
      this.$tab.navigateTo("/pages/bind-vehicle");
    },
    // è·³è½¬åˆ°æ¶ˆæ¯é¡µé¢
    goToMessages() {
      this.$tab.switchTab("/pages/message/index");
    },
    // æŸ¥çœ‹ä»»åŠ¡è¯¦æƒ…
    viewTaskDetail(task) {
      // è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…é¡µé¢ - ä½¿ç”¨taskId
      this.$tab.navigateTo(`/pagesTask/detail?id=${task.taskId || task.id}`);
    },
    // å¤„理任务操作
    handleTaskAction(task, action) {
      switch (action) {
        case "depart":
          // å‡ºå‘ -> çŠ¶æ€å˜ä¸ºå‡ºå‘ä¸­
          this.$modal
            .confirm("确定要出发吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "DEPARTING", "任务已出发");
            })
          },
          fail: function(err) {
            console.error('GPS定位失败:', err)
            that.$modal.confirm('GPS定位失败,是否继续更新状态?').then(() => {
            .catch(() => {});
          break;
        case "cancel":
          // å–消 -> äºŒæ¬¡ç¡®è®¤åŽçŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
          this.$modal
            .confirm("确定要取消此任务吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "CANCELLED", "任务已取消");
            })
            .catch(() => {});
          break;
        case "arrive":
          // å·²åˆ°è¾¾ -> çŠ¶æ€å˜ä¸ºå·²åˆ°è¾¾
          this.$modal
            .confirm("确认已到达目的地?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "ARRIVED", "已到达目的地");
            })
            .catch(() => {});
          break;
        case "forceCancel":
          // å¼ºåˆ¶ç»“束 -> çŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
          this.$modal
            .confirm("确定要强制结束此任务吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "CANCELLED", "任务已强制结束");
            })
            .catch(() => {});
          break;
        case "return":
          // å·²è¿”程 -> çŠ¶æ€å˜ä¸ºè¿”ç¨‹ä¸­
          this.$modal
            .confirm("确认开始返程?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "RETURNING", "已开始返程");
            })
            .catch(() => {});
          break;
        case "complete":
          // å·²å®Œæˆ -> çŠ¶æ€å˜ä¸ºå·²å®Œæˆ
          this.$modal
            .confirm("确认任务已完成?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "COMPLETED", "任务已完成");
            })
            .catch(() => {});
          break;
      }
    },
    // æ›´æ–°ä»»åŠ¡çŠ¶æ€
    updateTaskStatus(taskId, status, remark) {
      // èŽ·å–GPS位置信息
      this.getLocationAndUpdateStatus(taskId, status, remark);
    },
    // èŽ·å–ä½ç½®ä¿¡æ¯å¹¶æ›´æ–°çŠ¶æ€
    getLocationAndUpdateStatus(taskId, status, remark) {
      const that = this;
      // ä½¿ç”¨uni.getLocation获取GPS位置
      uni.getLocation({
        type: "gcj02",
        geocode: true,
        altitude: true,
        success: function (res) {
          console.log("GPS定位成功:", res);
          const statusData = {
            taskStatus: status,
            remark: remark,
            latitude: res.latitude,
            longitude: res.longitude,
            locationAddress: res.address
              ? res.address.street || res.address.poiName || ""
              : "",
            locationProvince: res.address ? res.address.province || "" : "",
            locationCity: res.address ? res.address.city || "" : "",
            locationDistrict: res.address ? res.address.district || "" : "",
            gpsAccuracy: res.accuracy,
            altitude: res.altitude,
            speed: res.speed,
            heading: res.direction || res.heading,
          };
          changeTaskStatus(taskId, statusData)
            .then((response) => {
              that.$modal.showToast("状态更新成功");
              that.loadRunningTasks();
            })
            .catch((error) => {
              console.error("更新任务状态失败:", error);
              that.$modal.showToast("状态更新失败,请重试");
            });
        },
        fail: function (err) {
          console.error("GPS定位失败:", err);
          that.$modal
            .confirm("GPS定位失败,是否继续更新状态?")
            .then(() => {
              const statusData = {
                taskStatus: status,
                remark: remark
              }
              changeTaskStatus(taskId, statusData).then(response => {
                that.$modal.showToast('状态更新成功')
                that.loadRunningTasks()
              }).catch(error => {
                console.error('更新任务状态失败:', error)
                that.$modal.showToast('状态更新失败,请重试')
              })
            }).catch(() => {})
                remark: remark,
              };
              changeTaskStatus(taskId, statusData)
                .then((response) => {
                  that.$modal.showToast("状态更新成功");
                  that.loadRunningTasks();
                })
                .catch((error) => {
                  console.error("更新任务状态失败:", error);
                  that.$modal.showToast("状态更新失败,请重试");
                });
            })
            .catch(() => {});
        },
      });
    },
    // èŽ·å–çŠ¶æ€æ ·å¼ç±»
    getStatusClass(status) {
      const statusClassMap = {
        PENDING: "status-pending",
        DEPARTING: "status-departing",
        ARRIVED: "status-arrived",
        RETURNING: "status-returning",
        COMPLETED: "status-completed",
        CANCELLED: "status-cancelled",
        IN_PROGRESS: "status-in-progress",
      };
      return statusClassMap[status] || "status-default";
    },
    getStatusText(status) {
      // æ”¯æŒæ–°æ—§ä¸¤ç§çŠ¶æ€æ ¼å¼
      const statusMap = {
        // æ–°æ ¼å¼ï¼ˆæ•°æ®åº“状态)
        PENDING: "待处理",
        DEPARTING: "出发中",
        ARRIVED: "已到达",
        RETURNING: "返程中",
        COMPLETED: "已完成",
        CANCELLED: "已取消",
        IN_PROGRESS: "处理中",
        // æ—§æ ¼å¼ï¼ˆUI状态)
        pending: "待处理",
        processing: "处理中",
        completed: "已完成",
      };
      return statusMap[status] || "未知";
    },
    getTaskTypeText(type) {
      const typeMap = {
        // æ–°æ ¼å¼ï¼ˆæ•°æ®åº“类型)
        MAINTENANCE: "维修保养",
        FUEL: "加油",
        OTHER: "其他",
        EMERGENCY_TRANSFER: "转运任务",
        WELFARE: "福祉车",
        // æ—§æ ¼å¼ï¼ˆUI类型)
        maintenance: "维修保养",
        refuel: "加油",
        inspection: "巡检",
        emergency: "转运任务",
        welfare: "福祉车",
      };
      return typeMap[type] || "未知类型";
    },
    clickConfirmsubscribeTaskNotify() {
      subscribeManager.subscribeWithConfirm()
        .then((result) => {
          if (result.success) {
            this.hasSubscribed = true;
          }
        })
      },
      // èŽ·å–çŠ¶æ€æ ·å¼ç±»
      getStatusClass(status) {
        const statusClassMap = {
          'PENDING': 'status-pending',
          'DEPARTING': 'status-departing',
          'ARRIVED': 'status-arrived',
          'RETURNING': 'status-returning',
          'COMPLETED': 'status-completed',
          'CANCELLED': 'status-cancelled',
          'IN_PROGRESS': 'status-in-progress'
        }
        return statusClassMap[status] || 'status-default'
      },
      getStatusText(status) {
        // æ”¯æŒæ–°æ—§ä¸¤ç§çŠ¶æ€æ ¼å¼
        const statusMap = {
          // æ–°æ ¼å¼ï¼ˆæ•°æ®åº“状态)
          'PENDING': '待处理',
          'DEPARTING': '出发中',
          'ARRIVED': '已到达',
          'RETURNING': '返程中',
          'COMPLETED': '已完成',
          'CANCELLED': '已取消',
          'IN_PROGRESS': '处理中',
          // æ—§æ ¼å¼ï¼ˆUI状态)
          'pending': '待处理',
          'processing': '处理中',
          'completed': '已完成'
        }
        return statusMap[status] || '未知'
      },
      getTaskTypeText(type) {
        const typeMap = {
          // æ–°æ ¼å¼ï¼ˆæ•°æ®åº“类型)
          'MAINTENANCE': '维修保养',
          'FUEL': '加油',
          'OTHER': '其他',
          'EMERGENCY_TRANSFER': '转运任务',
          'WELFARE': '福祉车',
          // æ—§æ ¼å¼ï¼ˆUI类型)
          'maintenance': '维修保养',
          'refuel': '加油',
          'inspection': '巡检',
          'emergency': '转运任务',
          'welfare': '福祉车'
        }
        return typeMap[type] || '未知类型'
      }
    }
  }
        .catch((error) => {
          console.log('订阅取消或失败:', error);
        });
    },
    // è®¢é˜…任务通知(直接调用,不显示确认弹窗)
    subscribeTaskNotify() {
      subscribeManager.subscribeDirect()
        .then((result) => {
          if (result.success) {
            this.hasSubscribed = true;
          }
        })
        .catch((error) => {
          console.log('订阅失败:', error);
        });
    },
  },
};
</script>
<style lang="scss">
  .home-container {
    padding: 20rpx;
    background-color: #f5f5f5;
    height: 100vh;
    display: flex;
    flex-direction: column;
    // éšè—æ»šåŠ¨æ¡ä½†ä¿æŒæ»šåŠ¨åŠŸèƒ½
    ::-webkit-scrollbar {
      display: none;
      width: 0 !important;
      height: 0 !important;
      background: transparent;
    }
    // Firefox滚动条隐藏
    * {
      scrollbar-width: none; /* Firefox */
    }
    // IE/Edge滚动条隐藏
    * {
      -ms-overflow-style: none; /* IE 10+ */
    }
.home-container {
  padding: 20rpx;
  background-color: #f5f5f5;
  height: 100vh;
  display: flex;
  flex-direction: column;
  // éšè—æ»šåŠ¨æ¡ä½†ä¿æŒæ»šåŠ¨åŠŸèƒ½
  ::-webkit-scrollbar {
    display: none;
    width: 0 !important;
    height: 0 !important;
    background: transparent;
  }
  // ç”¨æˆ·ä¿¡æ¯åŒºåŸŸ
  .user-info-section {
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    flex-shrink: 0; // é˜²æ­¢æ”¶ç¼©
    .user-info-content {
      display: flex;
      justify-content: space-between;
      align-items: center;
      .user-details {
        flex: 1;
        .user-info-row {
          display: flex;
          align-items: center;
          flex-wrap: wrap;
          margin-bottom: 12rpx;
          .user-name {
            font-size: 32rpx;
            font-weight: bold;
            color: #333;
          }
          .separator {
            margin: 0 12rpx;
            color: #ddd;
            font-size: 28rpx;
          }
          .branch-company {
            font-size: 26rpx;
            color: #666;
            display: flex;
            align-items: center;
          }
          .vehicle-info {
            font-size: 26rpx;
            color: #007AFF;
            display: flex;
            align-items: center;
          }
  // Firefox滚动条隐藏
  * {
    scrollbar-width: none; /* Firefox */
  }
  // IE/Edge滚动条隐藏
  * {
    -ms-overflow-style: none; /* IE 10+ */
  }
}
// ç”¨æˆ·ä¿¡æ¯åŒºåŸŸ
.user-info-section {
  background-color: white;
  border-radius: 15rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  flex-shrink: 0; // é˜²æ­¢æ”¶ç¼©
  .user-info-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    .user-details {
      flex: 1;
      .user-info-row {
        display: flex;
        align-items: center;
        flex-wrap: wrap;
        margin-bottom: 12rpx;
        .user-name {
          font-size: 32rpx;
          font-weight: bold;
          color: #333;
        }
        .bind-vehicle-btn {
        .separator {
          margin: 0 12rpx;
          color: #ddd;
          font-size: 28rpx;
        }
        .branch-company {
          font-size: 26rpx;
          color: #007AFF;
          color: #666;
          display: flex;
          align-items: center;
          &:active {
            opacity: 0.7;
          }
        }
        .vehicle-info {
          font-size: 26rpx;
          color: #007aff;
          display: flex;
          align-items: center;
        }
      }
      .bind-vehicle-btn {
        font-size: 26rpx;
        color: #007aff;
        display: flex;
        align-items: center;
        &:active {
          opacity: 0.7;
        }
      }
    }
  }
  // æ¶ˆæ¯å…¥å£
  .message-entry {
}
// æ¶ˆæ¯å…¥å£
.message-entry {
  display: flex;
  align-items: center;
  background-color: white;
  border-radius: 15rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  position: relative;
  .message-icon {
    margin-right: 20rpx;
  }
  .message-text {
    flex: 1;
    font-size: 32rpx;
    color: #333;
  }
  .unread-dot {
    position: absolute;
    top: 15rpx;
    right: 60rpx;
    background-color: #ff4d4f;
    color: white;
    border-radius: 50%;
    width: 32rpx;
    height: 32rpx;
    display: flex;
    align-items: center;
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    position: relative;
    .message-icon {
      margin-right: 20rpx;
    }
    .message-text {
      flex: 1;
      font-size: 32rpx;
      color: #333;
    }
    .unread-dot {
      position: absolute;
      top: 15rpx;
      right: 60rpx;
      background-color: #ff4d4f;
      color: white;
      border-radius: 50%;
      width: 32rpx;
      height: 32rpx;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 20rpx;
    }
    .arrow {
      margin-left: 20rpx;
    }
    justify-content: center;
    font-size: 20rpx;
  }
  // æ­£åœ¨è¿è¡Œçš„任务标题
  .running-tasks-header {
    margin-bottom: 20rpx;
    flex-shrink: 0; // é˜²æ­¢æ”¶ç¼©
    .header-title {
      font-size: 36rpx;
  .arrow {
    margin-left: 20rpx;
  }
}
// è®¢é˜…通知横幅
.subscribe-banner {
  display: flex;
  align-items: center;
  background: linear-gradient(135deg, #fff9e6 0%, #fff3e0 100%);
  border-radius: 15rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(255, 149, 0, 0.1);
  border: 1rpx solid #ffe0b2;
  .banner-icon {
    margin-right: 20rpx;
    flex-shrink: 0;
  }
  .banner-content {
    flex: 1;
    .banner-title {
      font-size: 30rpx;
      font-weight: bold;
      color: #333;
      margin-bottom: 8rpx;
    }
    .banner-desc {
      font-size: 24rpx;
      color: #666;
      line-height: 1.4;
    }
  }
  // æ­£åœ¨è¿è¡Œçš„任务列表
  .running-tasks-section {
    flex: 1;
  .banner-action {
    display: flex;
    align-items: center;
    padding: 12rpx 24rpx;
    background-color: white;
    border-radius: 15rpx;
    padding: 30rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
    // éšè—æ»šåŠ¨æ¡ä½†ä¿æŒæ»šåŠ¨åŠŸèƒ½
    ::-webkit-scrollbar {
      display: none;
      width: 0 !important;
      height: 0 !important;
      background: transparent;
    border-radius: 30rpx;
    flex-shrink: 0;
    text {
      font-size: 26rpx;
      color: #007aff;
      margin-right: 4rpx;
    }
    // Firefox滚动条隐藏
    * {
      scrollbar-width: none; /* Firefox */
    }
    // IE/Edge滚动条隐藏
    * {
      -ms-overflow-style: none; /* IE 10+ */
    }
    .task-list {
      .task-item {
        background-color: #fafafa;
        border-radius: 15rpx;
        margin-bottom: 30rpx;
        overflow: hidden;
        .task-main {
          padding: 30rpx;
          border-bottom: 1rpx solid #f0f0f0;
          // ä»»åŠ¡å¤´éƒ¨ï¼šæ ‡é¢˜å’ŒçŠ¶æ€
          .task-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 15rpx;
            .task-title {
              flex: 1;
              font-size: 32rpx;
              font-weight: bold;
              padding-right: 20rpx;
              line-height: 1.4;
            }
            .task-status {
              padding: 8rpx 20rpx;
              border-radius: 30rpx;
              font-size: 24rpx;
              white-space: nowrap;
              flex-shrink: 0;
              // å¾…处理 - æ©™è‰²
              &.status-pending {
                background-color: #fff3e0;
                color: #ff9500;
              }
              // å‡ºå‘中 - è“è‰²
              &.status-departing {
                background-color: #e3f2fd;
                color: #007AFF;
              }
              // å·²åˆ°è¾¾ - ç´«è‰²
              &.status-arrived {
                background-color: #f3e5f5;
                color: #9c27b0;
              }
              // è¿”程中 - é’色
              &.status-returning {
                background-color: #e0f2f1;
                color: #009688;
              }
              // å·²å®Œæˆ - ç»¿è‰²
              &.status-completed {
                background-color: #e8f5e9;
                color: #34C759;
              }
              // å·²å–消 - ç°è‰²
              &.status-cancelled {
                background-color: #f5f5f5;
                color: #999;
              }
              // å¤„理中 (兼容旧数据) - è“è‰²
              &.status-in-progress {
                background-color: #e3f2fd;
                color: #007AFF;
              }
              // é»˜è®¤æ ·å¼
              &.status-default {
                background-color: #f5f5f5;
                color: #666;
              }
            }
  }
  &:active {
    opacity: 0.9;
  }
}
// æ­£åœ¨è¿è¡Œçš„任务标题
.running-tasks-header {
  margin-bottom: 20rpx;
  flex-shrink: 0; // é˜²æ­¢æ”¶ç¼©
  .header-title {
    font-size: 36rpx;
    font-weight: bold;
    color: #333;
  }
}
// æ­£åœ¨è¿è¡Œçš„任务列表
.running-tasks-section {
  flex: 1;
  background-color: white;
  border-radius: 15rpx;
  padding: 30rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  // éšè—æ»šåŠ¨æ¡ä½†ä¿æŒæ»šåŠ¨åŠŸèƒ½
  ::-webkit-scrollbar {
    display: none;
    width: 0 !important;
    height: 0 !important;
    background: transparent;
  }
  // Firefox滚动条隐藏
  * {
    scrollbar-width: none; /* Firefox */
  }
  // IE/Edge滚动条隐藏
  * {
    -ms-overflow-style: none; /* IE 10+ */
  }
  .task-list {
    .task-item {
      background-color: #fafafa;
      border-radius: 15rpx;
      margin-bottom: 30rpx;
      overflow: hidden;
      .task-main {
        padding: 30rpx;
        border-bottom: 1rpx solid #f0f0f0;
        // ä»»åŠ¡å¤´éƒ¨ï¼šæ ‡é¢˜å’ŒçŠ¶æ€
        .task-header {
          display: flex;
          justify-content: space-between;
          align-items: flex-start;
          margin-bottom: 15rpx;
          .task-title {
            flex: 1;
            font-size: 32rpx;
            font-weight: bold;
            padding-right: 20rpx;
            line-height: 1.4;
          }
          // ä»»åŠ¡ç¼–å·å•ç‹¬ä¸€è¡Œ
          .task-code-row {
            margin-bottom: 15rpx;
            padding: 10rpx 0;
            border-bottom: 1rpx dashed #e0e0e0;
            .task-code {
              font-size: 28rpx;
              color: #333;
              font-weight: 500;
              font-family: monospace;
          .task-status {
            padding: 8rpx 20rpx;
            border-radius: 30rpx;
            font-size: 24rpx;
            white-space: nowrap;
            flex-shrink: 0;
            // å¾…处理 - æ©™è‰²
            &.status-pending {
              background-color: #fff3e0;
              color: #ff9500;
            }
          }
          .task-info {
            .info-row {
              display: flex;
              margin-bottom: 15rpx;
              &:last-child {
                margin-bottom: 0;
              }
              .info-item {
                flex: 1;
                display: flex;
                .label {
                  font-size: 26rpx;
                  color: #666;
                  margin-right: 10rpx;
                  white-space: nowrap;
                }
                .value {
                  font-size: 26rpx;
                  flex: 1;
                  word-break: break-all;
                }
              }
            // å‡ºå‘中 - è“è‰²
            &.status-departing {
              background-color: #e3f2fd;
              color: #007aff;
            }
            // å·²åˆ°è¾¾ - ç´«è‰²
            &.status-arrived {
              background-color: #f3e5f5;
              color: #9c27b0;
            }
            // è¿”程中 - é’色
            &.status-returning {
              background-color: #e0f2f1;
              color: #009688;
            }
            // å·²å®Œæˆ - ç»¿è‰²
            &.status-completed {
              background-color: #e8f5e9;
              color: #34c759;
            }
            // å·²å–消 - ç°è‰²
            &.status-cancelled {
              background-color: #f5f5f5;
              color: #999;
            }
            // å¤„理中 (兼容旧数据) - è“è‰²
            &.status-in-progress {
              background-color: #e3f2fd;
              color: #007aff;
            }
            // é»˜è®¤æ ·å¼
            &.status-default {
              background-color: #f5f5f5;
              color: #666;
            }
          }
        }
        .task-actions {
          display: flex;
          padding: 20rpx;
          .action-btn {
            flex: 1;
            height: 70rpx;
            border-radius: 10rpx;
            font-size: 26rpx;
            margin: 0 5rpx;
            background-color: #f0f0f0;
        // ä»»åŠ¡ç¼–å·å•ç‹¬ä¸€è¡Œ
        .task-code-row {
          margin-bottom: 15rpx;
          padding: 10rpx 0;
          border-bottom: 1rpx dashed #e0e0e0;
          .task-code {
            font-size: 28rpx;
            color: #333;
            &.primary {
              background-color: #007AFF;
              color: white;
            }
            &.cancel {
              background-color: #ff3b30;
              color: white;
            }
            &.disabled {
              opacity: 0.5;
            }
            &:first-child {
              margin-left: 0;
            }
            font-weight: 500;
            font-family: monospace;
          }
        }
        .task-info {
          .info-row {
            display: flex;
            margin-bottom: 15rpx;
            &:last-child {
              margin-right: 0;
              margin-bottom: 0;
            }
            .info-item {
              flex: 1;
              display: flex;
              .label {
                font-size: 26rpx;
                color: #666;
                margin-right: 10rpx;
                white-space: nowrap;
              }
              .value {
                font-size: 26rpx;
                flex: 1;
                word-break: break-all;
              }
            }
          }
        }
      }
      .no-data {
        text-align: center;
        padding: 100rpx 0;
        color: #999;
        text {
          display: block;
          margin-top: 20rpx;
      .task-actions {
        display: flex;
        padding: 20rpx;
        .action-btn {
          flex: 1;
          height: 70rpx;
          border-radius: 10rpx;
          font-size: 26rpx;
          margin: 0 5rpx;
          background-color: #f0f0f0;
          color: #333;
          &.primary {
            background-color: #007aff;
            color: white;
          }
          &.cancel {
            background-color: #ff3b30;
            color: white;
          }
          &.disabled {
            opacity: 0.5;
          }
          &:first-child {
            margin-left: 0;
          }
          &:last-child {
            margin-right: 0;
          }
        }
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
      }
    }
  }
}
</style>
app/pages/message/index.vue
@@ -2,6 +2,10 @@
  <view class="message-container">
    <view class="message-header">
      <view class="header-title">消息中心</view>
      <view class="subscribe-btn" v-if="!subscribed" @click="subscribeMessage">
        <uni-icons type="bell" size="20" color="#007AFF"></uni-icons>
        <text>订阅通知</text>
      </view>
    </view>
    
    <scroll-view class="message-list-scroll" scroll-y="true">
@@ -35,13 +39,15 @@
<script>
  import { getMyMessages, markAsRead } from '@/api/message'
  import { formatDateTime } from '@/utils/common'
  import subscribeManager from '@/utils/subscribe'
  
  export default {
    data() {
      return {
        // æ¶ˆæ¯åˆ—表
        messages: [],
        loading: false
        loading: false,
        subscribed: true,
      }
    },
    computed: {
@@ -61,6 +67,8 @@
    },
    onLoad() {
      this.loadMessages()
      // è‡ªåŠ¨è®¢é˜…ï¼ˆå¦‚æžœæœªè®¢é˜…åˆ™æ˜¾ç¤ºç¡®è®¤å¼¹çª—ï¼‰
      // this.autoSubscribeOnLaunch()
    },
    onShow() {
      // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°æ¶ˆæ¯
@@ -74,6 +82,21 @@
      })
    },
    methods: {
      // è‡ªåŠ¨è®¢é˜…ï¼ˆé¡µé¢åŠ è½½æ—¶è°ƒç”¨ï¼‰
      autoSubscribeOnLaunch() {
        subscribeManager.autoSubscribe()
          .then((result) => {
            if (result.skipped) {
              console.log('用户已订阅,无需重复订阅')
            } else if (result.success) {
              console.log('自动订阅成功')
            }
          })
          .catch((error) => {
            console.log('自动订阅取消或失败:', error)
          })
      },
      // åŠ è½½æ¶ˆæ¯åˆ—è¡¨
      async loadMessages() {
        try {
@@ -157,6 +180,23 @@
      formatMessageTime(dateTime) {
        if (!dateTime) return ''
        return formatDateTime(dateTime, 'MM-DD HH:mm')
      },
      // è®¢é˜…任务通知
      subscribeMessage() {
        subscribeManager.subscribeWithConfirm()
          .then((result) => {
            if (result.success) {
              uni.showToast({
                title: '订阅成功,您将收到任务通知',
                icon: 'success',
                duration: 2000
              })
            }
          })
          .catch((error) => {
            console.log('订阅失败:', error)
          })
      }
    }
  }
@@ -199,6 +239,24 @@
      font-size: 36rpx;
      font-weight: bold;
    }
    .subscribe-btn {
      display: flex;
      align-items: center;
      padding: 10rpx 20rpx;
      background-color: #f0f9ff;
      border-radius: 30rpx;
      text {
        margin-left: 8rpx;
        font-size: 26rpx;
        color: #007AFF;
      }
      &:active {
        opacity: 0.7;
      }
    }
  }
  
  .message-list-scroll {
app/pagesTask/create-emergency.vue
@@ -332,7 +332,7 @@
import { listAvailableVehicles, getUserBoundVehicle } from "@/api/vehicle"
import { searchHospitals, searchHospitalsByDeptRegion } from "@/api/hospital"
import DepartureSelector from './components/DepartureSelector.vue'
import { calculateDistance, baiduDistanceByAddress, baiduPlaceSuggestion } from "@/api/map"
import { calculateTianDiTuDistance } from "@/api/map"
import { listBranchUsers } from "@/api/system/user"
import { searchIcd10 } from "@/api/icd10"
import { calculateTransferPrice } from "@/api/price"
@@ -878,7 +878,7 @@
      
      // è°ƒç”¨ç™¾åº¦åœ°å›¾API计算距离
      const region = this.selectedRegion || '广州'
      baiduDistanceByAddress(fromAddress, region, toAddress, region)
      calculateTianDiTuDistance(fromAddress,  toAddress)
        .then(response => {
          uni.hideLoading()
          
@@ -1160,7 +1160,7 @@
      
      // ä½¿ç”¨uni-app的GPS定位功能
      uni.getLocation({
        type: 'gcj02', // è¿”回国测局坐标,适用于国内地图
        type: 'gcj02', // è¿”回国测局坐标,适用于国内地图 ç«æ˜Ÿåæ ‡
        success: (res) => {
          console.log('获取到GPS坐标:', res)
          const latitude = res.latitude
@@ -1626,7 +1626,7 @@
      })
      
      // è°ƒç”¨ç™¾åº¦åœ°å›¾API计算距离
      baiduDistanceByAddress(fromAddress, fromCity, toAddress, toCity)
      calculateTianDiTuDistance(fromAddress, toAddress)
        .then(response => {
          uni.hideLoading()
          
app/pagesTask/detail.vue
@@ -37,10 +37,10 @@
      <view class="detail-section">
        <view class="section-title">时间信息</view>
        <view class="info-item">
          <view class="label">计划开始时间</view>
          <view class="label">预约时间</view>
          <view class="value">{{ displayPlannedStartTime }}</view>
        </view>
        <view class="info-item">
        <view class="info-item" v-if="taskDetail.plannedEndTime">
          <view class="label">计划结束时间</view>
          <view class="value">{{ displayPlannedEndTime }}</view>
        </view>
@@ -466,28 +466,48 @@
        if (!this.taskDetail || !this.taskDetail.plannedStartTime) {
          return '未设置'
        }
        return formatDateTime(this.taskDetail.plannedStartTime, 'YYYY-MM-DD HH:mm')
        const formatted = formatDateTime(this.taskDetail.plannedStartTime, 'YYYY-MM-DD HH:mm')
        // å¦‚果年份是1900,表示无效日期,显示为未设置
        if (formatted && formatted.startsWith('1900')) {
          return '未设置'
        }
        return formatted
      },
      // æ˜¾ç¤ºè®¡åˆ’结束时间
      displayPlannedEndTime() {
        if (!this.taskDetail || !this.taskDetail.plannedEndTime) {
          return '未设置'
        }
        return formatDateTime(this.taskDetail.plannedEndTime, 'YYYY-MM-DD HH:mm')
        const formatted = formatDateTime(this.taskDetail.plannedEndTime, 'YYYY-MM-DD HH:mm')
        // å¦‚果年份是1900,表示无效日期,显示为未设置
        if (formatted && formatted.startsWith('1900')) {
          return '未设置'
        }
        return formatted
      },
      // æ˜¾ç¤ºå®žé™…开始时间
      displayActualStartTime() {
        if (!this.taskDetail || !this.taskDetail.actualStartTime) {
          return '未设置'
        }
        return formatDateTime(this.taskDetail.actualStartTime, 'YYYY-MM-DD HH:mm')
        const formatted = formatDateTime(this.taskDetail.actualStartTime, 'YYYY-MM-DD HH:mm')
        // å¦‚果年份是1900,表示无效日期,显示为未设置
        if (formatted && formatted.startsWith('1900')) {
          return '未设置'
        }
        return formatted
      },
      // æ˜¾ç¤ºå®žé™…结束时间
      displayActualEndTime() {
        if (!this.taskDetail || !this.taskDetail.actualEndTime) {
          return '未设置'
        }
        return formatDateTime(this.taskDetail.actualEndTime, 'YYYY-MM-DD HH:mm')
        const formatted = formatDateTime(this.taskDetail.actualEndTime, 'YYYY-MM-DD HH:mm')
        // å¦‚果年份是1900,表示无效日期,显示为未设置
        if (formatted && formatted.startsWith('1900')) {
          return '未设置'
        }
        return formatted
      }
    },
    onLoad(options) {
app/utils/subscribe.js
New file
@@ -0,0 +1,327 @@
import { getWechatConfig } from '@/api/wechat'
/**
 * å¾®ä¿¡è®¢é˜…消息工具类
 * ç»Ÿä¸€ç®¡ç†è®¢é˜…消息相关功能
 */
class SubscribeManager {
  constructor() {
    this.wechatConfig = {
      taskNotifyTemplateId: ''
    }
    this.configLoaded = false
  }
  /**
   * åŠ è½½å¾®ä¿¡é…ç½®
   * @returns {Promise}
   */
  loadWechatConfig() {
    return new Promise((resolve, reject) => {
      if (this.configLoaded && this.wechatConfig.taskNotifyTemplateId) {
        resolve(this.wechatConfig)
        return
      }
      getWechatConfig()
        .then((response) => {
          if (response.code === 200 && response.data) {
            this.wechatConfig = response.data
            this.configLoaded = true
            console.log('微信配置加载成功:', this.wechatConfig)
            resolve(this.wechatConfig)
          } else {
            console.warn('加载微信配置失败,使用默认配置')
            reject(new Error('加载微信配置失败'))
          }
        })
        .catch((error) => {
          console.error('加载微信配置失败:', error)
          reject(error)
        })
    })
  }
  /**
   * æ£€æŸ¥æœ¬åœ°è®¢é˜…状态(从缓存读取)
   * @returns {boolean}
   */
  checkLocalSubscribeStatus() {
    return true;// uni.getStorageSync('hasSubscribedTaskNotify') || false
  }
  /**
   * æ£€æŸ¥å¾®ä¿¡å®˜æ–¹è®¢é˜…状态(真实状态)
   * @returns {Promise<boolean>}
   */
  checkWechatSubscribeStatus() {
    return new Promise((resolve) => {
      // #ifdef MP-WEIXIN
      // éœ€è¦å…ˆåŠ è½½é…ç½®èŽ·å–æ¨¡æ¿ID
      this.loadWechatConfig()
        .then(() => {
          const templateId = this.wechatConfig.taskNotifyTemplateId
          if (!templateId) {
            console.warn('模板ID未配置,无法检查订阅状态')
            resolve(false)
            return
          }
          wx.getSetting({
            withSubscriptions: true,
            success: (res) => {
              console.log('微信订阅状态查询结果:', res)
              // æ£€æŸ¥subscriptionsSetting中是否有该模板ID的记录
              if (res.subscriptionsSetting && res.subscriptionsSetting.mainSwitch) {
                const subscribeStatus = res.subscriptionsSetting.mainSwitch;
                resolve(subscribeStatus)
                // 'accept' è¡¨ç¤ºç”¨æˆ·åŒæ„è®¢é˜…,'reject' è¡¨ç¤ºæ‹’绝,'ban' è¡¨ç¤ºè¢«å°ç¦
                // const isSubscribed = subscribeStatus === 'accept'
                // console.log(`模板ID ${templateId} è®¢é˜…状态:`, subscribeStatus, '是否已订阅:', isSubscribed)
                // resolve(isSubscribed)
              } else {
                console.log('未找到订阅设置信息,视为未订阅')
                resolve(false)
              }
            },
            fail: (err) => {
              console.error('获取微信设置失败:', err)
              resolve(false)
            }
          })
        })
        .catch((error) => {
          console.error('加载配置失败,无法检查订阅状态:', error)
          resolve(false)
        })
      // #endif
      // #ifndef MP-WEIXIN
      console.log('非微信小程序环境,无法检查订阅状态')
      resolve(false)
      // #endif
    })
  }
  /**
   * æ£€æŸ¥è®¢é˜…状态(综合检查)
   * å…ˆæ£€æŸ¥æœ¬åœ°çŠ¶æ€ï¼Œå†æ£€æŸ¥å¾®ä¿¡å®˜æ–¹çŠ¶æ€
   * @returns {Promise<{local: boolean, wechat: boolean, needResubscribe: boolean}>}
   */
  async checkSubscribeStatus() {
    const localStatus = this.checkLocalSubscribeStatus()
    const wechatStatus = await this.checkWechatSubscribeStatus()
    // å¦‚果本地显示已订阅,但微信官方显示未订阅,需要重新订阅
    const needResubscribe =  wechatStatus
    if (needResubscribe) {
      console.warn('本地状态与微信官方状态不一致,需要重新订阅')
      // æ¸…除本地记录
      uni.removeStorageSync('hasSubscribedTaskNotify')
    }
    return {
      local: localStatus,
      wechat: wechatStatus,
      needResubscribe: needResubscribe,
      isSubscribed: wechatStatus // ä»¥å¾®ä¿¡å®˜æ–¹çŠ¶æ€ä¸ºå‡†
    }
  }
  /**
   * æ˜¾ç¤ºè®¢é˜…确认弹窗
   * @returns {Promise}
   */
  showSubscribeConfirm() {
    return new Promise((resolve, reject) => {
      // #ifdef MP-WEIXIN
      wx.showModal({
        title: '开启任务通知',
        content: '勾选「总是保持以上选择」,后续新任务将自动推送~',
        confirmText: '去开启',
        cancelText: '暂不开启',
        success(res) {
          if (res.confirm) {
            resolve()
          } else {
            reject(new Error('用户取消'))
          }
        },
        fail() {
          reject(new Error('弹窗失败'))
        }
      })
      // #endif
      // #ifndef MP-WEIXIN
      uni.showToast({
        title: '仅支持微信小程序',
        icon: 'none'
      })
      reject(new Error('仅支持微信小程序'))
      // #endif
    })
  }
  /**
   * è®¢é˜…任务通知
   * @param {Object} options é…ç½®é€‰é¡¹
   * @param {boolean} options.showConfirm æ˜¯å¦æ˜¾ç¤ºç¡®è®¤å¼¹çª—,默认true
   * @param {Function} options.onSuccess æˆåŠŸå›žè°ƒ
   * @param {Function} options.onReject æ‹’绝回调
   * @param {Function} options.onFail å¤±è´¥å›žè°ƒ
   * @returns {Promise}
   */
  async subscribeTaskNotify(options = {}) {
    const {
      showConfirm = true,
      onSuccess,
      onReject,
      onFail
    } = options
    try {
      // åŠ è½½é…ç½®
      await this.loadWechatConfig()
      // æ£€æŸ¥é…ç½®æ˜¯å¦åŠ è½½
      if (!this.wechatConfig.taskNotifyTemplateId) {
        uni.showToast({
          title: '配置加载中,请稍后重试',
          icon: 'none'
        })
        throw new Error('配置未加载')
      }
      // æ˜¾ç¤ºç¡®è®¤å¼¹çª—(如果需要)
      if (showConfirm) {
        await this.showSubscribeConfirm()
      }
      // å‘起订阅
      return new Promise((resolve, reject) => {
        // #ifdef MP-WEIXIN
        wx.requestSubscribeMessage({
          tmplIds: [this.wechatConfig.taskNotifyTemplateId],
          success: (res) => {
            console.log('订阅消息授权结果:', res)
            const templateId = this.wechatConfig.taskNotifyTemplateId
            if (res[templateId] === 'accept') {
              // è®°å½•已订阅
              uni.setStorageSync('hasSubscribedTaskNotify', true)
              uni.showToast({
                title: '订阅成功',
                icon: 'success'
              })
              if (onSuccess) onSuccess()
              resolve({ success: true, action: 'accept' })
            } else if (res[templateId] === 'reject') {
              uni.showToast({
                title: '您拒绝了订阅',
                icon: 'none'
              })
              if (onReject) onReject()
              resolve({ success: false, action: 'reject' })
            } else {
              // å…¶ä»–情况(ban等)
              resolve({ success: false, action: res[templateId] })
            }
          },
          fail: (err) => {
            console.error('订阅消息失败:', err)
            uni.showToast({
              title: '订阅失败',
              icon: 'none'
            })
            if (onFail) onFail(err)
            reject(err)
          }
        })
        // #endif
        // #ifndef MP-WEIXIN
        uni.showToast({
          title: '仅支持微信小程序',
          icon: 'none'
        })
        reject(new Error('仅支持微信小程序'))
        // #endif
      })
    } catch (error) {
      console.error('订阅流程异常:', error)
      throw error
    }
  }
  /**
   * å¿«é€Ÿè®¢é˜…(带确认弹窗)
   * @returns {Promise}
   */
  subscribeWithConfirm() {
    return this.subscribeTaskNotify({ showConfirm: true })
  }
  /**
   * ç›´æŽ¥è®¢é˜…(不显示确认弹窗)
   * @returns {Promise}
   */
  subscribeDirect() {
    return this.subscribeTaskNotify({ showConfirm: false })
  }
  /**
   * è‡ªåŠ¨è®¢é˜…ï¼ˆæ™ºèƒ½æ£€æŸ¥ï¼‰
   * å¦‚果已订阅则跳过,未订阅则显示确认弹窗
   * @param {Object} options é…ç½®é€‰é¡¹
   * @param {boolean} options.force æ˜¯å¦å¼ºåˆ¶æ˜¾ç¤ºè®¢é˜…弹窗,默认false
   * @returns {Promise}
   */
  async autoSubscribe(options = {}) {
    const { force = false } = options
    try {
      // ç»¼åˆæ£€æŸ¥è®¢é˜…状态(本地 + å¾®ä¿¡å®˜æ–¹ï¼‰
      const status = await this.checkSubscribeStatus()
      console.log('订阅状态检查结果:', status)
      // å¦‚果微信官方状态显示已订阅,且不强制订阅
      if (status.isSubscribed && !force) {
        console.log('用户已订阅过(微信官方状态),跳过自动订阅')
        return { success: true, action: 'already_subscribed', skipped: true, status }
      }
      // å¦‚果需要重新订阅或未订阅
      if (status.needResubscribe) {
        console.log('检测到订阅状态失效,触发重新订阅流程')
      } else {
        console.log('用户未订阅,触发自动订阅流程')
      }
      // æ˜¾ç¤ºç¡®è®¤å¼¹çª—并订阅 ç›´æŽ¥é»˜è®¤è®¢é˜…
      const result = await this.subscribeWithConfirm();
      return { ...result, status }
    } catch (error) {
      console.log('自动订阅流程异常:', error)
      return { success: false, action: 'error', error }
    }
  }
  /**
   * é‡ç½®è®¢é˜…状态
   */
  resetSubscribeStatus() {
    uni.removeStorageSync('hasSubscribedTaskNotify')
  }
}
// åˆ›å»ºå•例
const subscribeManager = new SubscribeManager()
export default subscribeManager
ruoyi-admin/pom.xml
@@ -57,13 +57,17 @@
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!-- ä»£ç ç”Ÿæˆ-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-generator</artifactId>
        </dependency>
        <!-- æ”¯ä»˜æ¨¡å— -->
        <dependency>
            <groupId>com.ruoyi</groupId>
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
@@ -1,5 +1,6 @@
package com.ruoyi.web.controller.system;
import java.io.ByteArrayInputStream;
import java.util.*;
import java.text.SimpleDateFormat;
import java.text.ParseException;
@@ -7,11 +8,16 @@
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.service.*;
import com.ruoyi.common.config.TencentMapConfig;
import com.ruoyi.common.config.BaiduMapConfig;
import com.ruoyi.common.config.TiandituMapConfig;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -31,6 +37,10 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.http.HttpUtils;
import javax.annotation.Resource;
/**
 * è½¦è¾†GPS坐标Controller
@@ -67,6 +77,13 @@
    
    @Autowired
    private TiandituMapConfig tiandituMapConfig;
    @Resource(name = "tiandituMapService")
    private IMapService tiandituMapService;
    @Resource(name = "baiduMapService")
    private IMapService baiduMapService;
   /**
     * æŸ¥è¯¢è½¦è¾†GPS坐标列表
@@ -478,7 +495,29 @@
            return AjaxResult.error("距离计算失败:" + e.getMessage());
        }
    }
    /**
     * ç™¾åº¦åœ°å›¾é€†åœ°å€è§£æžæŽ¥å£ä»£ç†
     */
    @Anonymous()
    @GetMapping("/baidu/reverseGeocoding")
    public AjaxResult baiduResolveAddress(Double lng, Double lat) {
        try {
            // æ£€æŸ¥å‚æ•°
            if (lat == null || lng == null) {
                return AjaxResult.error("参数不完整,缺少经纬度坐标");
            }
           String response= baiduMapService.reverseGeocoding(lng, lat);
            // å‘送HTTP请求
            Map<String,String> objResult=new HashMap<>();
            objResult.put("address",response);
            return AjaxResult.success("查询成功", objResult);
        }catch (Exception e){
            logger.error("百度逆地址解析失败: lat={}, lng={}", lat, lng, e);
            return AjaxResult.error("百度逆地址解析失败:" + e.getMessage());
        }
    }
    /**
     * ç™¾åº¦åœ°å›¾åœ°ç†ç¼–码接口代理(地址转坐标)
     */
@@ -490,22 +529,11 @@
            if (address == null || address.trim().isEmpty()) {
                return AjaxResult.error("参数不完整,缺少地址信息");
            }
            // æž„建百度地图地理编码API URL
            String url = "https://api.map.baidu.com/geocoding/v3/";
            String params = "address=" + URLEncoder.encode(address, StandardCharsets.UTF_8.toString()) +
                           (city != null && !city.trim().isEmpty() ?
                            "&city=" + URLEncoder.encode(city, StandardCharsets.UTF_8.toString()) : "") +
                           "&output=json" +
                           "&ak=" + baiduMapConfig.getAk();
            logger.info("百度地图地理编码请求: address={}, city={}", address, city);
            // å‘送HTTP请求
            String response = HttpUtils.sendGet(url, params);
            Map<String,Double> objResult=baiduMapService.geocoding(address, city);
            
            // è¿”回结果
            return AjaxResult.success("查询成功", response);
            return AjaxResult.success("查询成功", objResult);
        } catch (Exception e) {
            logger.error("百度地图地理编码失败", e);
            return AjaxResult.error("地理编码失败:" + e.getMessage());
@@ -759,18 +787,11 @@
            if (address == null || address.trim().isEmpty()) {
                return AjaxResult.error("参数不完整,缺少地址信息");
            }
            // æž„建天地图地理编码API URL
            String url = "http://api.tianditu.gov.cn/geocoder";
            String params = "ds={\"keyWord\":\"" + address + "\"}&tk=" + tiandituMapConfig.getTk();
            logger.info("天地图地理编码请求: address={}", address);
            // å‘送HTTP请求
            String response = HttpUtils.sendGet(url, params);
            Map<String,Double> resultMap =tiandituMapService.geocoding(address,"");
            // è¿”回结果
            return AjaxResult.success("查询成功", response);
            return AjaxResult.success("查询成功", resultMap);
        } catch (Exception e) {
            logger.error("天地图地理编码失败", e);
            return AjaxResult.error("地理编码失败:" + e.getMessage());
@@ -795,20 +816,13 @@
                Double.isInfinite(lat) || Double.isInfinite(lon)) {
                return AjaxResult.error("参数无效,经纬度坐标格式错误");
            }
            String address=this.tiandituMapService.reverseGeocoding(lon, lat);
            
            // æž„建天地图逆地理编码API URL
            String url = "http://api.tianditu.gov.cn/geocoder";
            String params = "postStr={\"lon\":" + lon + ",\"lat\":" + lat + ",\"ver\":1}" +
                           "&type=geocode" +
                           "&tk=" + tiandituMapConfig.getTk();
            logger.info("天地图逆地理编码请求: lon={}, lat={}", lon, lat);
            // å‘送HTTP请求
            String response = HttpUtils.sendGet(url, params);
            Map<String,String> resultMap = new HashMap<>();
            resultMap.put("address",address);
            // è¿”回结果
            return AjaxResult.success("查询成功", response);
            return AjaxResult.success("查询成功",resultMap);
        } catch (Exception e) {
            logger.error("天地图逆地理编码失败: lon={}, lat={}", lon, lat, e);
            return AjaxResult.error("逆地理编码失败:" + e.getMessage());
@@ -937,56 +951,21 @@
                toAddress == null || toAddress.trim().isEmpty()) {
                return AjaxResult.error("参数不完整,缺少起点或终点地址");
            }
            logger.info("开始计算地址距离: fromAddress={}, toAddress={}", fromAddress, toAddress);
            // ç¬¬ä¸€æ­¥ï¼šèµ·ç‚¹åœ°å€è½¬åæ ‡
            String geocodingUrl1 = "http://api.tianditu.gov.cn/geocoder";
            String geocodingParams1 = "ds={\"keyWord\":\"" + fromAddress + "\"}" +
                                     "&tk=" + tiandituMapConfig.getTk();
            String geocodingResponse1 = HttpUtils.sendGet(geocodingUrl1, geocodingParams1);
            logger.info("起点地理编码响应: {}", geocodingResponse1);
            // è§£æžèµ·ç‚¹åæ ‡
            com.alibaba.fastjson2.JSONObject geocodingJson1 = com.alibaba.fastjson2.JSONObject.parseObject(geocodingResponse1);
            if (!"0".equals(geocodingJson1.getString("status"))) {
                logger.error("起点地理编码失败: {}", geocodingResponse1);
                return AjaxResult.error("起点地址解析失败");
            }
            com.alibaba.fastjson2.JSONObject location1 = geocodingJson1.getJSONObject("location");
            if (location1 == null) {
                return AjaxResult.error("起点地址未找到对应坐标");
            }
            double fromLon = location1.getDouble("lon");
            double fromLat = location1.getDouble("lat");
            logger.info("起点坐标: lon={}, lat={}", fromLon, fromLat);
            // ç¬¬äºŒæ­¥ï¼šç»ˆç‚¹åœ°å€è½¬åæ ‡
            String geocodingUrl2 = "http://api.tianditu.gov.cn/geocoder";
            String geocodingParams2 = "ds={\"keyWord\":\"" + toAddress + "\"}" +
                                     "&tk=" + tiandituMapConfig.getTk();
            String geocodingResponse2 = HttpUtils.sendGet(geocodingUrl2, geocodingParams2);
            logger.info("终点地理编码响应: {}", geocodingResponse2);
            // è§£æžç»ˆç‚¹åæ ‡
            com.alibaba.fastjson2.JSONObject geocodingJson2 = com.alibaba.fastjson2.JSONObject.parseObject(geocodingResponse2);
            if (!"0".equals(geocodingJson2.getString("status"))) {
                logger.error("终点地理编码失败: {}", geocodingResponse2);
                return AjaxResult.error("终点地址解析失败");
            }
            com.alibaba.fastjson2.JSONObject location2 = geocodingJson2.getJSONObject("location");
            if (location2 == null) {
                return AjaxResult.error("终点地址未找到对应坐标");
            }
            double toLon = location2.getDouble("lon");
            double toLat = location2.getDouble("lat");
            Map<String,Double> fromLngLat= tiandituMapService.geocoding(fromAddress, null);
            double fromLng = fromLngLat.get("lng");
            double fromLat = fromLngLat.get("lat");
            logger.info("起点坐标: lon={}, lat={}", fromLng, fromLat);
            Map<String,Double> toLngLat = tiandituMapService.geocoding(toAddress, null);
            double toLon = toLngLat.get("lng");
            double toLat = toLngLat.get("lat");
            logger.info("终点坐标: lon={}, lat={}", toLon, toLat);
            
            // ç¬¬ä¸‰æ­¥ï¼šè°ƒç”¨è·¯å¾„规划接口计算距离
            String routeUrl = "http://api.tianditu.gov.cn/drive";
            String orig = fromLon + "," + fromLat;
            String orig = fromLng + "," + fromLat;
            String dest = toLon + "," + toLat;
            String routeParams = "postStr={\"orig\":\"" + orig + "\",\"dest\":\"" + dest + "\",\"style\":\"0\"}" +
                                "&tk=" + tiandituMapConfig.getTk();
@@ -994,53 +973,314 @@
            logger.info("路径规划请求: orig={}, dest={}", orig, dest);
            String routeResponse = HttpUtils.sendGet(routeUrl, routeParams);
            logger.info("路径规划响应: {}", routeResponse);
            // è§£æžè·ç¦»ç»“æžœ
            com.alibaba.fastjson2.JSONObject routeJson = com.alibaba.fastjson2.JSONObject.parseObject(routeResponse);
            if (!"0".equals(routeJson.getString("status"))) {
                logger.error("路径规划失败: {}", routeResponse);
                return AjaxResult.error("路径规划失败");
            //<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            //<result dest="113.3472,23.16957" mid="" orig="113.373308,23.136699">
            //
            //    <parameters>
            //
            //        <orig>113.373308,23.136699</orig>
            //
            //        <dest>113.3472,23.16957</dest>
            //
            //        <mid/>
            //
            //        <key/>
            //
            //        <width>600</width>
            //
            //        <height>400</height>
            //
            //        <style>0</style>
            //
            //        <version/>
            //
            //        <sort/>
            //
            //    </parameters>
            //
            //    <routes count="9" time="0.0">
            //
            //        <item id="0">
            //
            //            <strguide>从棠安路向西出发,沿棠安路走100米并左转,</strguide>
            //
            //            <signage/>
            //
            //            <streetName>棠安路</streetName>
            //
            //            <nextStreetName/>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37323,23.13677</turnlatlon>
            //
            //        </item>
            //
            //        <item id="1">
            //
            //            <strguide>èµ°100米并向广园快速/省道303/华南快速/省道81/环城高速/华观路/岑村/省道3/广深沿江方向进入科韵中路,</strguide>
            //
            //            <signage>广园快速/省道303/华南快速/省道81/环城高速/华观路/岑村/省道3/广深沿江</signage>
            //
            //            <streetName/>
            //
            //            <nextStreetName>科韵中路</nextStreetName>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37185,23.13699</turnlatlon>
            //
            //        </item>
            //
            //        <item id="2">
            //
            //            <strguide>沿科韵中路走0.6公里并向广园快速(西行)/省道303/华南快速方向进入广园快速路,</strguide>
            //
            //            <signage>广园快速(西行)/省道303/华南快速</signage>
            //
            //            <streetName>科韵中路</streetName>
            //
            //            <nextStreetName>广园快速路</nextStreetName>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37107,23.13648</turnlatlon>
            //
            //        </item>
            //
            //        <item id="3">
            //
            //            <strguide>沿广园快速路走1.9公里并从华南快速/广州南站/广州北站/环城高速(北环)/广州长隆/帽峰山/华南快速(北行)/国道4/广州环城高速/华南快速(西)/京港澳高速/广州机场高速/广河高速/韶关/省道81/省道8方向匝道进入华南立交桥,</strguide>
            //
            //            <signage>华南快速/广州南站/广州北站/环城高速(北环)/广州长隆/帽峰山/华南快速(北行)/国道4/广州环城高速/华南快速(西)/京港澳高速/广州机场高速/广河高速/韶关/省道81/省道8方向匝道</signage>
            //
            //            <streetName>广园快速路</streetName>
            //
            //            <nextStreetName>华南立交桥</nextStreetName>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37139,23.14206</turnlatlon>
            //
            //        </item>
            //
            //        <item id="4">
            //
            //            <strguide>沿华南立交桥盘桥向北走0.6公里并直行到省道4/华南快速,</strguide>
            //
            //            <signage/>
            //
            //            <streetName>华南立交桥</streetName>
            //
            //            <nextStreetName>省道4/华南快速</nextStreetName>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.35685,23.14669</turnlatlon>
            //
            //        </item>
            //
            //        <item id="5">
            //
            //            <strguide>沿省道4/华南快速走0.6公里并从天河客运站/省道15/元岗/佛山/深圳/广州环城高速/沈海高速支线/华南快速管理中心/岑村立交/省道81方向匝道进入岑村立交,</strguide>
            //
            //            <signage>天河客运站/省道15/元岗/佛山/深圳/广州环城高速/沈海高速支线/华南快速管理中心/岑村立交/省道81方向匝道</signage>
            //
            //            <streetName>省道4/华南快速</streetName>
            //
            //            <nextStreetName>岑村立交</nextStreetName>
            //
            //            <tollStatus>1</tollStatus>
            //
            //            <turnlatlon>113.35518,23.15157</turnlatlon>
            //
            //        </item>
            //
            //        <item id="6">
            //
            //            <strguide>沿岑村立交盘桥向西北走0.5公里并从天河客运站/元岗/广汕路/岑村路/市交警支队出口驶离并进入长兴路,</strguide>
            //
            //            <signage>天河客运站/元岗/广汕路/岑村路/市交警支队出口</signage>
            //
            //            <streetName>岑村立交</streetName>
            //
            //            <nextStreetName>长兴路</nextStreetName>
            //
            //            <tollStatus>1</tollStatus>
            //
            //            <turnlatlon>113.35731,23.15636</turnlatlon>
            //
            //        </item>
            //
            //        <item id="7">
            //
            //            <strguide>沿长兴路走2.8公里并右转到长湴中路,</strguide>
            //
            //            <signage/>
            //
            //            <streetName>长兴路</streetName>
            //
            //            <nextStreetName>长湴中路</nextStreetName>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.35829,23.16086</turnlatlon>
            //
            //        </item>
            //
            //        <item id="8">
            //
            //            <strguide>沿长湴中路走100米到达目的地。</strguide>
            //
            //            <signage/>
            //
            //            <streetName>长湴中路</streetName>
            //
            //            <nextStreetName/>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.34714,23.17047</turnlatlon>
            //
            //        </item>
            //
            //    </routes>
            //
            //    <simple>
            //
            //        <item id="0">
            //
            //            <strguide>从棠安路出发,进入科韵中路。</strguide>
            //
            //            <streetNames>棠安路</streetNames>
            //
            //            <lastStreetName/>
            //
            //            <linkStreetName>科韵中路</linkStreetName>
            //
            //            <signage/>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37323,23.13677</turnlatlon>
            //
            //            <streetLatLon>113.37323,23.13677;113.37291,23.13683;113.37272,23.13687;113.37272,23.13687;113.37265,23.13688;113.37265,23.13688;113.37227,23.13696;113.37227,23.13696;113.37209,23.13699;113.37201,23.13696;113.37201,23.13696;113.37185,23.13699;</streetLatLon>
            //
            //            <streetDistance>144.0</streetDistance>
            //
            //            <segmentNumber>0</segmentNumber>
            //
            //        </item>
            //
            //        <item id="1">
            //
            //            <strguide>沿科韵中路行驶,向广园快速(西行)/省道303/华南快速方向,进入省道4/华南快速。</strguide>
            //
            //            <streetNames/>
            //
            //            <lastStreetName>科韵中路</lastStreetName>
            //
            //            <linkStreetName>省道4/华南快速</linkStreetName>
            //
            //            <signage>广园快速(西行)/省道303/华南快速</signage>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.37107,23.13648</turnlatlon>
            //
            //            <streetLatLon>113.37185,23.13699;113.37174,23.13683;113.3714,23.13649;113.37134,23.13645;113.37126,23.13643;113.37116,23.13644;113.37107,23.13648;113.37107,23.13648;113.37103,23.13657;113.37091,23.13683;113.37091,23.13683;113.37086,23.13693;113.3708,23.13705;113.3708,23.13705;113.37075,23.13717;113.37075,23.13717;113.37069,23.1373;113.37069,23.1373;113.37067,23.13735;113.37067,23.13735;113.3706,23.13755;113.37057,23.13765;113.37055,23.13779;113.37054,23.13794;113.37054,23.13804;113.37054,23.13814;113.37054,23.13831;113.37055,23.13844;113.37055,23.13844;113.37056,23.13855;113.37058,23.13865;113.3706,23.13878;113.37062,23.13884;113.37068,23.13911;113.37073,23.13935;113.37075,23.13944;113.37079,23.1396;113.3708,23.13964;113.37083,23.13975;113.37083,23.13975;113.37084,23.1398;113.37084,23.1398;113.37087,23.13998;113.3709,23.1401;113.37097,23.14039;113.371,23.14051;113.371,23.14051;113.3711,23.1409;113.37114,23.14105;113.37116,23.14115;113.3712,23.14127;113.37121,23.14135;113.37124,23.14146;113.37125,23.14153;113.37126,23.14155;113.37129,23.14167;113.37129,23.14167;113.37134,23.14187;113.37136,23.14194;113.37139,23.14206;</streetLatLon>
            //
            //            <streetDistance>748.0</streetDistance>
            //
            //            <segmentNumber>1-2</segmentNumber>
            //
            //        </item>
            //
            //        <item id="2">
            //
            //            <strguide>沿省道4/华南快速,岑村立交,行驶到广园快速路,在天河客运站/元岗/广汕路/岑村路/市交警支队出口驶离,进入长湴中路。</strguide>
            //
            //            <streetNames>省道4/华南快速,岑村立交,</streetNames>
            //
            //            <lastStreetName>广园快速路</lastStreetName>
            //
            //            <linkStreetName>长湴中路</linkStreetName>
            //
            //            <signage>天河客运站/元岗/广汕路/岑村路/市交警支队出口</signage>
            //
            //            <tollStatus>2</tollStatus>
            //
            //            <turnlatlon>113.35731,23.15636</turnlatlon>
            //
            //            <streetLatLon>113.37139,23.14206;113.37145,23.14219;113.37151,23.1423;113.37157,23.14241;113.37164,23.14249;113.37171,23.14256;113.37175,23.1426;113.37179,23.14263;113.37182,23.14265;113.37186,23.14267;113.37192,23.14269;113.37196,23.1427;113.37206,23.14271;113.37216,23.14269;113.37222,23.14267;113.37229,23.14264;113.37234,23.14261;113.37239,23.14257;113.37243,23.14251;113.37247,23.14246;113.3725,23.14237;113.37251,23.14231;113.37251,23.14226;113.37251,23.14222;113.37249,23.14215;113.37248,23.14212;113.37247,23.14208;113.37245,23.14203;113.37242,23.14198;113.37235,23.14191;113.37226,23.14182;113.37215,23.14174;113.37206,23.14169;113.37202,23.14167;113.37183,23.1416;113.37169,23.14157;113.37157,23.14155;113.37145,23.14153;113.37131,23.14151;113.37131,23.14151;113.37125,23.14153;113.37115,23.14156;113.37049,23.14178;113.37036,23.14183;113.37022,23.14188;113.37006,23.14194;113.36995,23.14197;113.36995,23.14197;113.36986,23.14199;113.36979,23.142;113.36956,23.14203;113.36956,23.14203;113.36949,23.14205;113.36949,23.14205;113.3693,23.14213;113.3693,23.14213;113.3692,23.14218;113.3692,23.14218;113.3691,23.14223;113.3691,23.14223;113.36837,23.1425;113.36837,23.1425;113.36735,23.14293;113.36735,23.14293;113.36683,23.14314;113.36683,23.14314;113.36454,23.14409;113.36454,23.14409;113.36432,23.14418;113.36413,23.14426;113.3637,23.14444;113.3637,23.14444;113.36214,23.14505;113.36138,23.14532;113.36138,23.14532;113.36131,23.14535;113.36131,23.14535;113.36113,23.14541;113.36113,23.14541;113.36101,23.14545;113.36089,23.1455;113.36071,23.14556;113.36071,23.14556;113.36062,23.14559;113.36052,23.14562;113.36052,23.14562;113.36024,23.1457;113.36015,23.14573;113.36015,23.14573;113.35987,23.14581;113.35969,23.14587;113.35818,23.1463;113.35818,23.1463;113.35777,23.14642;113.35777,23.14642;113.35766,23.14645;113.35766,23.14645;113.35762,23.14647;113.35762,23.14647;113.35736,23.14655;113.35736,23.14655;113.35685,23.14669;113.35685,23.14669;113.35657,23.14695;113.35642,23.14713;113.35642,23.14713;113.35637,23.14718;113.3562,23.14738;113.35586,23.14787;113.35582,23.14793;113.35572,23.14812;113.35572,23.14812;113.35557,23.14838;113.35554,23.14844;113.35554,23.14844;113.35545,23.14854;113.35533,23.14872;113.35533,23.14872;113.35529,23.14878;113.35529,23.14878;113.355,23.14912;113.35492,23.14923;113.35486,23.1493;113.35483,23.14936;113.3548,23.14942;113.3548,23.14942;113.35473,23.14964;113.35472,23.14973;113.35472,23.14973;113.35471,23.14981;113.35473,23.15003;113.35485,23.15044;113.35509,23.1512;113.35517,23.15145;113.35518,23.15151;113.35518,23.15157;113.35518,23.15157;113.35523,23.15169;113.35523,23.15169;113.35526,23.15178;113.35526,23.15178;113.35531,23.15192;113.35531,23.15192;113.35534,23.15199;113.35541,23.15216;113.35547,23.15233;113.35554,23.1525;113.35568,23.15287;113.35584,23.15323;113.35591,23.15342;113.35598,23.15358;113.35609,23.15379;113.35617,23.15394;113.35629,23.15421;113.35646,23.15457;113.35648,23.15461;113.35652,23.15471;113.35654,23.15475;113.35673,23.15515;113.35677,23.15524;113.35677,23.15524;113.35693,23.15556;113.35697,23.15564;113.35709,23.1559;113.35709,23.1559;113.35731,23.15636;113.35731,23.15636;113.35756,23.15669;113.35775,23.15697;113.3579,23.15725;113.35805,23.15752;113.35805,23.15752;113.35841,23.15824;113.35865,23.15872;113.3587,23.15889;113.3587,23.15889;113.35879,23.15912;113.35883,23.15927;113.35885,23.15938;113.35886,23.15948;113.35884,23.1596;113.35882,23.15978;113.35878,23.15994;113.35868,23.16016;113.35859,23.16034;113.35855,23.16041;113.35849,23.1605;113.35843,23.16062;113.35829,23.16086;</streetLatLon>
            //
            //            <streetDistance>3669.0</streetDistance>
            //
            //            <segmentNumber>3-6</segmentNumber>
            //
            //        </item>
            //
            //        <item id="3">
            //
            //            <strguide>沿长湴中路行驶到目的地。</strguide>
            //
            //            <streetNames>长湴中路</streetNames>
            //
            //            <lastStreetName/>
            //
            //            <linkStreetName/>
            //
            //            <signage/>
            //
            //            <tollStatus>0</tollStatus>
            //
            //            <turnlatlon>113.34714,23.17047</turnlatlon>
            //
            //            <streetLatLon>113.34714,23.17047;113.34711,23.1703;113.34708,23.16988;113.34708,23.16988;113.34705,23.16957;</streetLatLon>
            //
            //            <streetDistance>101.0</streetDistance>
            //
            //            <segmentNumber>7-8</segmentNumber>
            //
            //        </item>
            //
            //    </simple>
            //
            //    <distance>7.48</distance>
            //
            //    <duration>548.0</duration>
            //
            //    <routelatlon>113.373308,23.136699;113.37323,23.13677;113.37272,23.13687;113.37265,23.13688;113.37227,23.13696;113.37209,23.13699;113.37201,23.13696;113.37185,23.13699;113.37185,23.13699;113.3714,23.13649;113.37126,23.13643;113.37116,23.13644;113.37107,23.13648;113.37107,23.13648;113.37091,23.13683;113.37075,23.13717;113.37057,23.13765;113.37054,23.13794;113.37055,23.13844;113.3706,23.13878;113.3708,23.13964;113.37083,23.13975;113.371,23.14051;113.37125,23.14153;113.37129,23.14167;113.37139,23.14206;113.37139,23.14206;113.37164,23.14249;113.37182,23.14265;113.37206,23.14271;113.37222,23.14267;113.37234,23.14261;113.3725,23.14237;113.37249,23.14215;113.37242,23.14198;113.37206,23.14169;113.37183,23.1416;113.37131,23.14151;113.37049,23.14178;113.36995,23.14197;113.36956,23.14203;113.36949,23.14205;113.3693,23.14213;113.3692,23.14218;113.3691,23.14223;113.36837,23.1425;113.36735,23.14293;113.36683,23.14314;113.36454,23.14409;113.3637,23.14444;113.36138,23.14532;113.36113,23.14541;113.36052,23.14562;113.35818,23.1463;113.35766,23.14645;113.35762,23.14647;113.35685,23.14669;113.35685,23.14669;113.35642,23.14713;113.35637,23.14718;113.35572,23.14812;113.35554,23.14844;113.35533,23.14872;113.35529,23.14878;113.35486,23.1493;113.3548,23.14942;113.35473,23.14964;113.35473,23.15003;113.35518,23.15151;113.35518,23.15157;113.35518,23.15157;113.35526,23.15178;113.35554,23.1525;113.35598,23.15358;113.35652,23.15471;113.35731,23.15636;113.35731,23.15636;113.35805,23.15752;113.3587,23.15889;113.35885,23.15938;113.35878,23.15994;113.35855,23.16041;113.35829,23.16086;113.35829,23.16086;113.35774,23.16199;113.35695,23.16368;113.35625,23.1651;113.35567,23.16616;113.35546,23.16638;113.35484,23.1669;113.3547,23.16697;113.35444,23.1671;113.35392,23.16726;113.35328,23.16732;113.35126,23.16726;113.35046,23.16736;113.34949,23.16765;113.34651,23.16884;113.34571,23.16921;113.34393,23.17017;113.34387,23.17021;113.34252,23.17092;113.34213,23.17112;113.34192,23.17139;113.34177,23.17169;113.34261,23.17145;113.34427,23.17102;113.34559,23.17069;113.34714,23.17047;113.34714,23.17047;113.34708,23.16988;113.34708,23.16988;113.34705,23.16957;113.3472,23.16957;</routelatlon>
            //
            //    <mapinfo>
            //
            //        <center>113.35754,23.15406</center>
            //
            //        <scale>10</scale>
            //
            //    </mapinfo>
            //</result>
            //解析这个xml æ‹¿åˆ°distance和duration
            if(routeResponse!=null) {
                org.dom4j.Document doc = DocumentHelper.parseText(routeResponse);
                // 2. èŽ·å–æ ¹èŠ‚ç‚¹ <result>
                Element rootElement = doc.getRootElement();
                // 3. ç›´æŽ¥èŽ·å–æ ¹èŠ‚ç‚¹ä¸‹çš„ <distance> å­èŠ‚ç‚¹æ–‡æœ¬å€¼
                String distance = rootElement.elementText("distance");
                // å¯é€‰ï¼šè½¬ä¸ºDouble类型(若需数值计算)
                Double distanceValue = Double.parseDouble(distance);
                Map<String, Double> resultMap = new HashMap<>();
                resultMap.put("distance", distanceValue*1000);
                // 4. ç›´æŽ¥èŽ·å–æ ¹èŠ‚ç‚¹ä¸‹çš„ <duration> å­èŠ‚ç‚¹æ–‡æœ¬å€¼
                return AjaxResult.success("计算成功", resultMap);
            }
            // æå–距离信息
            com.alibaba.fastjson2.JSONObject result = routeJson.getJSONObject("result");
            if (result == null) {
                logger.error("路径规划结果为空");
                return AjaxResult.error("路径规划失败");
            else{
                return AjaxResult.error("计算距离失败:无返回结果");
            }
            com.alibaba.fastjson2.JSONArray routes = result.getJSONArray("routes");
            if (routes == null || routes.isEmpty()) {
                logger.error("未找到路线信息");
                return AjaxResult.error("未找到路线信息");
            }
            com.alibaba.fastjson2.JSONObject route = routes.getJSONObject(0);
            double distance = route.getDoubleValue("distance"); // è·ç¦»ï¼Œå•位:米
            double duration = route.getDoubleValue("duration"); // æ—¶é•¿ï¼Œå•位:秒
            logger.info("计算成功: è·ç¦»={}ç±³, æ—¶é•¿={}秒", distance, duration);
            // æž„建返回结果
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("distance", (int)distance); // è·ç¦»ï¼ˆç±³ï¼‰
            resultMap.put("duration", (int)duration); // æ—¶é•¿ï¼ˆç§’)
            resultMap.put("distanceKm", String.format("%.1f", distance / 1000.0)); // è·ç¦»ï¼ˆå…¬é‡Œï¼‰
            resultMap.put("durationMin", (int)(duration / 60)); // æ—¶é•¿ï¼ˆåˆ†é’Ÿï¼‰
            // èµ·ç‚¹åæ ‡
            Map<String, Object> fromLocation = new HashMap<>();
            fromLocation.put("lon", fromLon);
            fromLocation.put("lat", fromLat);
            resultMap.put("fromLocation", fromLocation);
            // ç»ˆç‚¹åæ ‡
            Map<String, Object> toLocation = new HashMap<>();
            toLocation.put("lon", toLon);
            toLocation.put("lat", toLat);
            resultMap.put("toLocation", toLocation);
            return AjaxResult.success("计算成功", resultMap);
        } catch (Exception e) {
            logger.error("计算地址距离失败", e);
            return AjaxResult.error("计算距离失败:" + e.getMessage());
@@ -1099,29 +1339,26 @@
            // å‘送HTTP请求
            String response = HttpUtils.sendGet(url, params);
            logger.debug("天地图普通搜索响应: {}", response);
            // è§£æžå“åº”
            com.alibaba.fastjson2.JSONObject jsonResponse = com.alibaba.fastjson2.JSONObject.parseObject(response);
            if (!"0".equals(jsonResponse.getString("status"))) {
                logger.error("普通搜索失败: {}", response);
            // {"count":27,"resultType":1,"prompt":[{"type":4,"admins":[{"adminName":"广州市","adminCode":156440100}]}],
            //"pois":[{"address":"广东省广州市天河区棠下街道子午馆天河棠下店","phone":"","poiType":"101","name":"子午馆(天河棠下店)","source":"0","hotPointID":"17163AD907B29F24","lonlat":"113.36952,23.134056"},{"address":"博汇街6号","phone":"","poiType":"101","name":"天河棠下·智汇park","source":"0","hotPointID":"C2635A7054C1EBB7","lonlat":"113.37003,23.14308"},{"address":"科新路143~147号","phone":"020-88524715","poiType":"101","name":"重庆德庄火锅(天河棠下店)","source":"0","hotPointID":"6919E1CFA6F6EF20","lonlat":"113.37627,23.127416"},{"address":"博汇街6号","phone":"020-87085713","poiType":"101","name":"天河棠下·智汇PAPK停车场","source":"0","hotPointID":"4AC84F8E1CB34C4E","lonlat":"113.36925,23.14256"},{"address":"广园东路博汇街6号天河棠下·智汇PAPK地下停车场","phone":"020-87085713","poiType":"101","name":"天河棠下·智汇PAPK特斯拉超级充电站","source":"0","hotPointID":"70FC2D0A2D061E4B","lonlat":"113.3705,23.14359"},{"address":"棠德南路321","phone":"","poiType":"101","name":"糖园天河棠下店","source":"0","hotPointID":"B97F4B20719EEBD1","lonlat":"113.38009,23.13434"},{"address":"中山大道棠下路东闸大街自编98号2号店","phone":"13590139682","poiType":"101","name":"花甲王天河棠下店","source":"0","hotPointID":"174C40E7FBAE1187","lonlat":"113.37556,23.12844"},{"address":"棠下二社涌边路69号光辉大厦","phone":"020-66351119","poiType":"101","name":"索特来文艺酒店天河棠下店","source":"0","hotPointID":"362052A27B9A1AC8","lonlat":"113.37161,23.12794"},{"address":"中山大道西493号","phone":"020-38288788","poiType":"101","name":"中国电信天河棠下福善营业厅","source":"0","hotPointID":"D50C7774CFC6AA09","lonlat":"113.37455,23.12855"},{"address":"广东省广州市天河区棠下街道愿达语言培训中心天河棠下校区","phone":"","poiType":"101","name":"愿达语言培训中心天河棠下校区","source":"0","hotPointID":"3CEE80F76558BDE4","lonlat":"113.373743,23.128796"}],"lineData":[],"status":{"cndesc":"服务正常","infocode":1000},"keyWord":"广州天河棠下"}
            com.alibaba.fastjson.JSONObject jsonResponse = com.alibaba.fastjson.JSONObject.parseObject(response);
            JSONObject objStatus=jsonResponse.getJSONObject("status");
            if (objStatus == null || objStatus.getInteger("infocode") != 1000) {
                logger.error("天地图普通搜索失败: {}", response);
                return AjaxResult.error("地址搜索失败");
            }
            // æå–搜索结果
            com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("pois");
            if (results == null || results.isEmpty()) {
                logger.info("未找到匹配的地址");
                return AjaxResult.success("查询成功", new ArrayList<>());
            }
            JSONArray results = jsonResponse.getJSONArray("pois");
            
            // æž„建返回结果
            List<Map<String, Object>> suggestions = new ArrayList<>();
            for (int i = 0; i < results.size(); i++) {
                com.alibaba.fastjson2.JSONObject item = results.getJSONObject(i);
                JSONObject item = results.getJSONObject(i);
                
                Map<String, Object> suggestion = new HashMap<>();
                suggestion.put("name", item.getString("name")); // åç§°
                suggestion.put("address", item.getString("addr")); // åœ°å€
                suggestion.put("address", item.getString("address")); // åœ°å€
                suggestion.put("province", item.getString("prov")); // çœ
                suggestion.put("city", item.getString("city")); // å¸‚
                suggestion.put("district", item.getString("county")); // åŒºåŽ¿
@@ -1130,11 +1367,12 @@
                suggestion.put("type", item.getString("type")); // ç±»åž‹
                
                // ç»çº¬åº¦ä¿¡æ¯
                com.alibaba.fastjson2.JSONObject location = item.getJSONObject("lonlat");
                String location = item.getString("lonlat");
                if (location != null) {
                    Map<String, Object> locationMap = new HashMap<>();
                    locationMap.put("lon", location.getDouble("lon"));
                    locationMap.put("lat", location.getDouble("lat"));
                    String[] locationArray = location.split(",");
                    locationMap.put("lng",Double.parseDouble(locationArray[0]));
                    locationMap.put("lat",Double.parseDouble(locationArray[1]));
                    suggestion.put("location", locationMap);
                }
                
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleManagementController.java
@@ -199,7 +199,7 @@
    @PutMapping("/status/{id}")
    public AjaxResult updateStatus(@PathVariable("id") Long id, @RequestParam String status) {
        try {
            int result = sysTaskVehicleService.updateTaskVehicleStatus(id, status);
            int result = sysTaskVehicleService.updateSysTaskVehicleStatus(id, status);
            if (result > 0) {
                return success("状态更新成功");
            } else {
@@ -211,6 +211,22 @@
    }
    /**
     * æ‰¹é‡èŽ·å–è½¦è¾†å½“å‰ä»»åŠ¡çŠ¶æ€
     * ä¼˜åŒ–接口:减少HTTP请求次数
     */
    @PreAuthorize("@ss.hasPermi('task:vehicle:query')")
    @PostMapping("/currentStatus")
    public AjaxResult batchGetCurrentTaskStatus(@RequestBody List<Long> vehicleIds) {
        try {
            java.util.Map<Long, java.util.Map<String, Object>> statusMap = sysTaskVehicleService.batchGetVehicleCurrentTaskStatus(vehicleIds);
            return success(statusMap);
        } catch (Exception e) {
            logger.error("批量获取车辆任务状态失败", e);
            return error("获取状态失败:" + e.getMessage());
        }
    }
    /**
     * åˆ†é…è½¦è¾†è¯·æ±‚对象
     */
    public static class AssignVehicleRequest {
ruoyi-admin/src/main/java/com/ruoyi/web/controller/wechat/WechatController.java
@@ -2,12 +2,29 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
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.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.config.WechatConfig;
import com.ruoyi.common.utils.WechatUtils;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * å¾®ä¿¡æŽ¥å£Controller
@@ -20,6 +37,18 @@
    
    @Autowired
    private WechatConfig wechatConfig;
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private SysTaskEmergencyMapper sysTaskEmergencyMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private IWechatTaskNotifyService wechatTaskNotifyService;
    
    /**
     * èŽ·å–å¾®ä¿¡AccessToken
@@ -39,4 +68,55 @@
            return error("获取微信AccessToken异常:" + e.getMessage());
        }
    }
    /**
     * èŽ·å–å¾®ä¿¡é…ç½®ä¿¡æ¯ï¼ˆåŒ¿åè®¿é—®ï¼‰
     * è¿”回前端需要的配置信息,如订阅消息模板ID等
     */
    @Anonymous
    @GetMapping("/config")
    public AjaxResult getConfig() {
        Map<String, String> config = new HashMap<>();
        config.put("taskNotifyTemplateId", wechatConfig.getTaskNotifyTemplateId());
        config.put("taskDetailPage", wechatConfig.getTaskDetailPage());
        return success(config);
    }
    /**
     * åŒ¿åæŽ¥å£ï¼šæ ¹æ®ä»»åŠ¡ID手动触发一次任务微信通知
     * æ–¹ä¾¿é€šè¿‡Postman调试,前提是任务已有执行人/创建人并绑定了openId
     */
    @Anonymous
    @PostMapping("/task/notify")
    public AjaxResult notifyTaskByWechat(@RequestParam("taskId") Long taskId) {
        if (taskId == null) {
            return error("任务ID不能为空");
        }
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task == null) {
            return error("任务不存在");
        }
        // ç®€åŒ–:给任务创建人发一条(如果绑定了openId),方便快速验证模板和跳转
        if (task.getCreatorId() == null) {
            return error("任务未指定创建人");
        }
        SysUser creator = sysUserMapper.selectUserById(task.getCreatorId());
        if (creator == null || StringUtils.isEmpty(creator.getOpenId())) {
            return error("创建人未绑定微信openId");
        }
        // è°ƒç”¨ç»Ÿä¸€çš„微信通知服务
        List<Long> userIds = new ArrayList<>();
        userIds.add(task.getCreatorId());
        int successCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds);
        if (successCount == 0) {
            return error("微信通知发送失败");
        }
        return success("已触发微信通知,成功发送条数:" + successCount);
    }
}
ruoyi-admin/src/main/resources/application-dev.yml
@@ -34,8 +34,8 @@
            maxWait: 60000
            # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
            connectTimeout: 30000
            # é…ç½®ç½‘络超时时间
            socketTimeout: 60000
            # é…ç½®ç½‘络超时时间(增加到5分钟,避免大数据量查询超时)
            socketTimeout: 300000
            # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
ruoyi-admin/src/main/resources/application-prod.yml
@@ -34,8 +34,8 @@
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®ç½‘络超时时间(增加到5分钟,避免大数据量查询超时)
      socketTimeout: 300000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
ruoyi-admin/src/main/resources/application-test.yml
@@ -34,8 +34,8 @@
            maxWait: 60000
            # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
            connectTimeout: 30000
            # é…ç½®ç½‘络超时时间
            socketTimeout: 60000
            # é…ç½®ç½‘络超时时间(增加到5分钟,避免大数据量查询超时)
            socketTimeout: 300000
            # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
ruoyi-admin/src/main/resources/application.yml
@@ -168,6 +168,11 @@
transferConfigWeixin:
  appId: wx40692cc44953a8cb
  appSecret: 9638b7d8bb988e4daaac7ac35457f296
  # ä»»åŠ¡é€šçŸ¥æ¨¡æ¿ID(你给的那个模型ID)
  taskNotifyTemplateId: 4mJGnvzjPpQednzNwVgghN5CZ_jSirZToisISOpLfMU
  # ä»»åŠ¡è¯¦æƒ…é¡µé¢è·¯å¾„ï¼ˆç”¨äºŽæ‹¼æŽ¥ ?id=taskId)
  # åˆ†åŒ…页面路径格式:分包根目录/页面路径(不要前导斜杠)
  taskDetailPage: pagesTask/detail
# è…¾è®¯åœ°å›¾é…ç½®
tencent:
  map:
@@ -187,7 +192,7 @@
# provider: åœ°å›¾æœåŠ¡æä¾›å•†ï¼Œå¯é€‰å€¼ï¼šbaidu(百度地图)、tianditu(天地图)
map:
  service:
    provider: baidu  # é»˜è®¤ä½¿ç”¨ç™¾åº¦åœ°å›¾
    provider: tianditu  # é»˜è®¤ä½¿ç”¨ç™¾åº¦åœ°å›¾ tianditu /baidu
# æ”¯ä»˜é…ç½®
payment:
ruoyi-common/src/main/java/com/ruoyi/common/config/MapServiceConfig.java
@@ -16,7 +16,7 @@
     * å¯é€‰å€¼: baidu, tianditu
     * é»˜è®¤: baidu
     */
    private String provider = "baidu";
    private String provider = "tianditu";
    public String getProvider() {
        return provider;
ruoyi-common/src/main/java/com/ruoyi/common/config/WechatConfig.java
@@ -19,6 +19,12 @@
    /** å¾®ä¿¡å°ç¨‹åºAppSecret */
    private String appSecret;
    /** ä»»åŠ¡é€šçŸ¥æ¨¡æ¿ID */
    private String taskNotifyTemplateId;
    /** ä»»åŠ¡è¯¦æƒ…é¡µé¢è·¯å¾„ */
    private String taskDetailPage;
    public String getAppId() {
        return appId;
    }
@@ -34,4 +40,20 @@
    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }
    public String getTaskNotifyTemplateId() {
        return taskNotifyTemplateId;
    }
    public void setTaskNotifyTemplateId(String taskNotifyTemplateId) {
        this.taskNotifyTemplateId = taskNotifyTemplateId;
    }
    public String getTaskDetailPage() {
        return taskDetailPage;
    }
    public void setTaskDetailPage(String taskDetailPage) {
        this.taskDetailPage = taskDetailPage;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java
@@ -21,6 +21,7 @@
    public static String YYYY = "yyyy";
    public static String YYYY_MM = "yyyy-MM";
    public static String YYYYMMDD="yyyyMMdd";
    public static String YYYY_MM_DD = "yyyy-MM-dd";
ruoyi-common/src/main/java/com/ruoyi/common/utils/PlateNumberExtractor.java
New file
@@ -0,0 +1,52 @@
package com.ruoyi.common.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
public class PlateNumberExtractor {
    // çœä»½ç®€ç§°æ­£åˆ™ç‰‡æ®µ
    private static final String PROVINCE_ABBRS = "京|æ´¥|沪|渝|冀|晋|è¾½|吉|黑|苏|浙|皖|闽|èµ£|鲁|豫|鄂|湘|粤|琼|川|è´µ|云|陕|甘|青|蒙|桂|宁|新|藏|港|æ¾³|台";
    // è½¦ç‰Œå·æ­£åˆ™ï¼šçœä»½ç®€ç§° + A-Z字母 + 5-6位字母/数字(含新能源车牌)
    private static final String PLATE_REGEX = "([" + PROVINCE_ABBRS + "][A-Z][A-Z0-9]{5,6})";
    private static final Pattern PLATE_PATTERN = Pattern.compile(PLATE_REGEX);
    /**
     * æå–字符串中所有符合规则的车牌号
     * @param input è¾“入字符串
     * @return è½¦ç‰Œå·åˆ—表(无匹配则返回空列表)
     */
    public static List<String> extractPlateNumbers(String input) {
        List<String> plateNumbers = new ArrayList<>();
        if (input == null || input.isEmpty()) {
            return plateNumbers;
        }
        Matcher matcher = PLATE_PATTERN.matcher(input);
        while (matcher.find()) {
            System.out.println(matcher.group(1));
            plateNumbers.add(matcher.group(1));
        }
        return plateNumbers;
    }
    public static String extractPlateNumber(String input) {
        Matcher matcher = PLATE_PATTERN.matcher(input);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return "";
    }
//    public static void main(String[] args) {
//        String input = "xxx粤VSX120,测试京A88888,沪B123456(新能源)";
//        String plates = extractPlateNumber(input);
//        System.out.println("提取的车牌号:" + plates);
//        // è¾“出:[粤VSX120, äº¬A88888, æ²ªB123456]
//    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/WechatUtils.java
@@ -200,4 +200,87 @@
        }
        return userAgent.toLowerCase().contains("micromessenger");
    }
    /**
     * æˆªæ–­thing类型字段,微信订阅消息thing类型最长20个字符
     *
     * @param value åŽŸå§‹å­—ç¬¦ä¸²
     * @return æˆªæ–­åŽçš„字符串(最长20字符)
     */
    public static String truncateThingValue(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        // å¾®ä¿¡thing类型最长20个字符
        final int MAX_THING_LENGTH = 20;
        if (value.length() <= MAX_THING_LENGTH) {
            return value;
        }
        // æˆªæ–­å¹¶æ·»åŠ çœç•¥å·ï¼Œç¡®ä¿æ€»é•¿åº¦ä¸è¶…è¿‡20
        return value.substring(0, MAX_THING_LENGTH - 1) + "…";
    }
    /**
     * å‘送小程序订阅消息
     *
     * @param accessToken å¾®ä¿¡æŽ¥å£è°ƒç”¨å‡­è¯
     * @param touser æŽ¥æ”¶äººopenId
     * @param templateId æ¨¡æ¿ID
     * @param page å°ç¨‹åºè·³è½¬é¡µé¢
     * @param data æ¨¡æ¿æ•°æ®ï¼Œkey为字段名(如thing1、time27),value为字段值
     * @return å¾®ä¿¡è¿”回结果JSON
     */
    public static JSONObject sendSubscribeMessage(String accessToken,
                                                  String touser,
                                                  String templateId,
                                                  String page,
                                                  Map<String, String> data) {
        try {
            if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(touser) || StringUtils.isEmpty(templateId)) {
                log.error("发送订阅消息参数不完整,accessToken={}, touser={}, templateId={}", accessToken, touser, templateId);
                return null;
            }
            String url = WECHAT_API_BASE_URL_SERVER + "/cgi-bin/message/subscribe/send?access_token=" + accessToken;
            Map<String, Object> body = new HashMap<>();
            body.put("touser", touser);
            body.put("template_id", templateId);
            if (StringUtils.isNotEmpty(page)) {
                body.put("page", page);
            }
             body.put("miniprogram_state", "formal");
            Map<String, Object> dataNode = new HashMap<>();
            if (data != null && !data.isEmpty()) {
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    Map<String, String> valueNode = new HashMap<>();
                    valueNode.put("value", entry.getValue());
                    dataNode.put(entry.getKey(), valueNode);
                }
            }
            body.put("data", dataNode);
            String jsonBody = JSON.toJSONString(body);
            String response = HttpUtils.sendPost(url, jsonBody);
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject == null) {
                log.error("发送订阅消息返回为空");
                return null;
            }
            if (jsonObject.getIntValue("errcode") != 0) {
                log.error("发送订阅消息失败: {}", jsonObject.toJSONString());
            } else {
                log.info("发送订阅消息成功,touser={}, templateId={}", touser, templateId);
            }
            return jsonObject;
        } catch (Exception e) {
            log.error("发送订阅消息异常: {}", e.getMessage(), e);
            return null;
        }
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
@@ -108,7 +108,7 @@
            URLConnection connection = realUrl.openConnection();
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
//            connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
            connection.connect();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
            String line;
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/CmsVehicleSyncTask.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.stream.Collectors;
import com.ruoyi.common.utils.PlateNumberExtractor;
import com.ruoyi.system.domain.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -135,6 +136,7 @@
    //对车牌处理的通用方法
    private String getPlateNo(String plateNo){
        if (StringUtils.isNotEmpty(plateNo)) {
                // ä»Žè½¦è¾†åç§°ä¸­æå–车牌号(假设格式为"★车牌号(地区)")
            if(plateNo.contains("(")) {
@@ -143,7 +145,9 @@
                plateNo = plateNo.replace("★", "").replace("☆", "").split("(")[0];
            }
            }
            return plateNo;
        //xxx粤VSX120
        plateNo = plateNo.replaceAll("[^a-zA-Z0-9]", "");
        return PlateNumberExtractor.extractPlateNumber(plateNo);
    }
    /**
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleGpsSegmentMileageTask.java
@@ -2,7 +2,6 @@
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;
@@ -25,42 +24,41 @@
    private ISysConfigService configService;
    
    /**
     * æœåŠ¡å¯åŠ¨æ—¶æ‰§è¡Œè¡¥å¿è®¡ç®—
     * æ£€æŸ¥æœ€è¿‘7天内未被处理的GPS坐标并进行补偿计算
     * æ‰§è¡ŒGPS分段里程补偿计算
     * æ£€æŸ¥æœ€è¿‘N天内未被处理的GPS坐标并进行补偿计算
     * å»ºè®®é…ç½®ä¸ºæ¯å¤©æ‰§è¡Œä¸€æ¬¡,例如凌晨2点
     */
    @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天");
                    }
    public void executeCompensationCalculation() {
        executeCompensationCalculation("7");
    }
    /**
     * æ‰§è¡ŒGPS分段里程补偿计算(带参数)
     *
     * @param params å‚数字符串,格式:回溯天数(如:7表示回溯7天)
     */
    public void executeCompensationCalculation(String params) {
        try {
            // è§£æžå‚æ•°:回溯天数
            int lookbackDays = 7; // é»˜è®¤7天
            if (params != null && !params.trim().isEmpty()) {
                try {
                    lookbackDays = Integer.parseInt(params.trim());
                } catch (NumberFormatException e) {
                    logger.warn("参数格式错误,使用默认值7天: {}", params);
                }
                // æ‰§è¡Œè¡¥å¿è®¡ç®—
                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();
            logger.info("========== å¼€å§‹æ‰§è¡ŒGPS分段里程补偿计算 - å›žæº¯{}天 ==========", lookbackDays);
            // æ‰§è¡Œè¡¥å¿è®¡ç®—
            int successCount = segmentMileageService.compensateCalculation(lookbackDays);
            logger.info("========== GPS分段里程补偿计算完成 - æˆåŠŸå¤„ç† {} è¾†è½¦ ==========", successCount);
        } catch (Exception e) {
            logger.error("GPS分段里程补偿计算失败", e);
        }
    }
    /**
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleSyncTask.java
@@ -1,5 +1,6 @@
package com.ruoyi.quartz.task;
import com.ruoyi.common.utils.PlateNumberExtractor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,9 +87,13 @@
     */
    private String extractPlateNumber(String deviceName, String remark) {
        if (StringUtils.isNotEmpty(deviceName)) {
            String plateNumber =PlateNumberExtractor.extractPlateNumber(deviceName);
            if(StringUtils.isNotEmpty(plateNumber))return plateNumber;
            return deviceName;
        }
        if (StringUtils.isNotEmpty(remark)) {
            String plateNumber =PlateNumberExtractor.extractPlateNumber(remark);
            if(StringUtils.isNotEmpty(plateNumber))return plateNumber;
            return remark;
        }
        return null;
ruoyi-system/src/main/java/com/ruoyi/system/config/MapServiceConfiguration.java
@@ -41,7 +41,7 @@
    @Primary
    public IMapService mapService() {
        String provider = mapServiceConfig.getProvider();
        logger.info("使用的地图服务进行地理编码:{}",provider);
        if ("tianditu".equalsIgnoreCase(provider)) {
            logger.info("使用天地图服务进行地理编码");
            return tiandituMapService;
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java
@@ -108,6 +108,10 @@
    @Excel(name = "部门名称")
    private String deptName;
    /** è½¦ç‰Œå·(列表显示第一辆车) */
    @Excel(name = "车牌号")
    private String vehicleNo;
    /** åˆ é™¤æ ‡å¿—(0代表存在 2代表删除) */
    private String delFlag;
@@ -308,6 +312,14 @@
        return deptName;
    }
    public void setVehicleNo(String vehicleNo) {
        this.vehicleNo = vehicleNo;
    }
    public String getVehicleNo() {
        return vehicleNo;
    }
    public void setDelFlag(String delFlag) {
        this.delFlag = delFlag;
    }
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
@@ -132,6 +132,9 @@
    /** æ˜¯å¦éœ€è¦é‡æ–°åŒæ­¥ï¼š0-不需要,1-需要重新同步(车辆或人员变更) */
    private Integer needResync;
    /** æ—§ç³»ç»ŸServiceOrdNo(转运单编号) */
    private String legacyServiceOrdNo;
    public Long getId() {
@@ -454,6 +457,14 @@
        this.needResync = needResync;
    }
    public String getLegacyServiceOrdNo() {
        return legacyServiceOrdNo;
    }
    public void setLegacyServiceOrdNo(String legacyServiceOrdNo) {
        this.legacyServiceOrdNo = legacyServiceOrdNo;
    }
    @Override
    public String toString() {
        return "SysTaskEmergency{" +
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java
@@ -22,6 +22,9 @@
    /** ä»»åŠ¡çŠ¶æ€ */
    private String taskStatus;
    /** è½¦ç‰Œå· */
    private String vehicleNo;
    /** åˆ›å»ºäººID */
    private Long creatorId;
@@ -74,6 +77,14 @@
        this.taskStatus = taskStatus;
    }
    public String getVehicleNo() {
        return vehicleNo;
    }
    public void setVehicleNo(String vehicleNo) {
        this.vehicleNo = vehicleNo;
    }
    public Long getCreatorId() {
        return creatorId;
    }
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
@@ -7,13 +7,21 @@
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysMessage;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.event.TaskCreatedEvent;
import com.ruoyi.system.event.TaskAssignedEvent;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import com.ruoyi.system.mapper.SysMessageMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import java.util.HashMap;
/**
 * ä»»åŠ¡æ¶ˆæ¯ç›‘å¬å™¨
@@ -32,6 +40,15 @@
    
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private SysTaskEmergencyMapper sysTaskEmergencyMapper;
    @Autowired
    private IWechatTaskNotifyService wechatTaskNotifyService;
    /**
     * ç›‘听任务创建事件
@@ -93,7 +110,7 @@
                return;
            }
            
            // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送消息
            // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送站内消息
            for (int i = 0; i < event.getAssigneeIds().size(); i++) {
                Long assigneeId = event.getAssigneeIds().get(i);
                
@@ -104,7 +121,7 @@
                    continue;
                }
                
                // åˆ›å»ºæ¶ˆæ¯
                // åˆ›å»ºç«™å†…消息
                SysMessage message = new SysMessage();
                message.setMessageType("PUSH");
                message.setMessageTitle("任务推送");
@@ -123,6 +140,15 @@
                sysMessageMapper.insertSysMessage(message);
                log.info("任务分配消息已保存,消息ID:{},接收人:{}", message.getMessageId(), assignee.getNickName());
            }
            // å‘送微信订阅消息(排除创建人)
            try {
                SysTask task = sysTaskMapper.selectSysTaskByTaskId(event.getTaskId());
                Long creatorId = task != null ? task.getCreatorId() : null;
                wechatTaskNotifyService.sendTaskNotifyMessage(event.getTaskId(), event.getAssigneeIds(), creatorId);
            } catch (Exception e) {
                log.error("处理任务分配事件时发送微信订阅消息失败", e);
            }
            
        } catch (Exception e) {
            log.error("处理任务分配事件失败", e);
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java
@@ -25,7 +25,7 @@
     */
    List<Map<String, Object>> selectTransferOrders(@Param("startDate") String startDate);
    
    /**
    /**ServiceOrdNo
     * æ ¹æ®æœåŠ¡å•ID和调度单ID查询转运单数据
     * 
     * @param serviceOrdID æœåŠ¡å•ID
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java
@@ -64,11 +64,12 @@
                                                       @Param("endTime") Date endTime);
    /**
     * æŸ¥è¯¢æ‰€æœ‰æ´»è·ƒè½¦è¾†ID列表
     * æŸ¥è¯¢æ´»è·ƒè½¦è¾†ID列表
     * 
     * @param startTime èµ·å§‹æ—¶é—´
     * @return è½¦è¾†ID列表
     */
    public List<Long> selectActiveVehicleIds();
    public List<Long> selectActiveVehicleIds(@Param("startTime") Date startTime);
    
    /**
     * æŸ¥è¯¢æœªè¢«è®¡ç®—çš„GPS坐标(不在tb_vehicle_gps_calculated表中的记录)
ruoyi-system/src/main/java/com/ruoyi/system/service/IMapService.java
@@ -18,4 +18,13 @@
     * @return GPS坐标,包含lng和lat,如果获取失败返回null
     */
    Map<String, Double> geocoding(String address, String city);
    /**
     * GPS坐标转地址(逆向地理编码)
     *
     * @param lng ç»åº¦
     * @param lat çº¬åº¦
     * @return åœ°å€ï¼Œå¦‚果获取失败返回null
     */
    String reverseGeocoding(double lng, double lat);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -55,7 +55,7 @@
     * @param updateTime æ›´æ–°æ—¶é—´
     * @return ç»“æžœ
     */
    public int insertTask(TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId, Long userId,String userName, Long deptId, Date createTime, Date updateTime);
    public int insertTask(TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime);
    /**
     * ä¿®æ”¹ä»»åŠ¡ç®¡ç†
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskVehicleService.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.service;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.SysTaskVehicle;
/**
@@ -147,4 +148,21 @@
     * @return ç»“æžœ
     */
    public int updateTaskVehicleStatus(Long id, String status);
    /**
     * æ‰¹é‡èŽ·å–è½¦è¾†å½“å‰ä»»åŠ¡çŠ¶æ€
     *
     * @param vehicleIds è½¦è¾†ID列表
     * @return Map<车辆ID, Map<"taskCode": ä»»åŠ¡ç¼–å·, "taskStatus": ä»»åŠ¡çŠ¶æ€>>
     */
    public Map<Long, Map<String, Object>> batchGetVehicleCurrentTaskStatus(List<Long> vehicleIds);
    /**
     * æ›´æ–°ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€ï¼ˆæ–°æ–¹æ³•ï¼‰
     *
     * @param id å…³è”ID
     * @param status æ–°çŠ¶æ€
     * @return ç»“æžœ
     */
    public int updateSysTaskVehicleStatus(Long id, String status);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatTaskNotifyService.java
New file
@@ -0,0 +1,33 @@
package com.ruoyi.system.service;
import com.ruoyi.system.domain.SysTask;
import java.util.List;
/**
 * å¾®ä¿¡ä»»åŠ¡é€šçŸ¥æœåŠ¡æŽ¥å£
 *
 * @author ruoyi
 * @date 2025-11-30
 */
public interface IWechatTaskNotifyService {
    /**
     * å‘送任务通知消息给指定用户列表
     *
     * @param taskId ä»»åŠ¡ID
     * @param userIds æŽ¥æ”¶ç”¨æˆ·ID列表
     * @param excludeUserId æŽ’除的用户ID(可选,如创建人)
     * @return æˆåŠŸå‘é€çš„æ¶ˆæ¯æ•°é‡
     */
    int sendTaskNotifyMessage(Long taskId, List<Long> userIds, Long excludeUserId);
    /**
     * å‘送任务通知消息给指定用户列表(不排除任何用户)
     *
     * @param taskId ä»»åŠ¡ID
     * @param userIds æŽ¥æ”¶ç”¨æˆ·ID列表
     * @return æˆåŠŸå‘é€çš„æ¶ˆæ¯æ•°é‡
     */
    int sendTaskNotifyMessage(Long taskId, List<Long> userIds);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/AdditionalFeeSyncServiceImpl.java
@@ -101,7 +101,7 @@
                .orElse(null);
            
            if (fee == null) {
                log.error("新系统附加费用记录不存在,feeId: {}", feeId);
                log.info("新系统附加费用记录不存在,feeId: {}", feeId);
                return false;
            }
            
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/BaiduMapServiceImpl.java
@@ -82,4 +82,24 @@
            return null;
        }
    }
    @Override
    public String reverseGeocoding(double lng, double lat) {
        //调用百度地址反向解析接口
        String url = "https://api.map.baidu.com/reverse_geocoding/v3/";
        String params = "location=" + lat + "," + lng +
                "&output=json" +
                "&ak=" + baiduMapConfig.getAk();
        String response = HttpUtils.sendGet(url, params);
        //"{\"status\":0,\"result\":{\"location\":{\"lng\":113.25330399999999,\"lat\":23.04668300960378},\"formatted_address\":\"广东省佛山市南海区桂城街道\",\"edz\":{\"name\":\"\"},\"business\":\"\",\"business_info\":[],\"addressComponent\":{\"country\":\"中国\",\"country_code\":0,\"region_code_iso\":\"CHN\",\"country_code_iso\":\"CHN\",\"country_code_iso2\":\"CN\",\"province\":\"广东省\",\"city\":\"佛山市\",\"city_level\":2,\"district\":\"南海区\",\"town\":\"桂城街道\",\"town_code\":\"440605011\",\"distance\":\"\",\"direction\":\"\",\"adcode\":\"440605\",\"street\":\"\",\"street_number\":\"\"},\"pois\":[],\"roads\":[],\"poiRegions\":[],\"sematic_description\":\"\",\"formatted_address_poi\":\"\",\"cityCode\":138}}"
        JSONObject jsonObject = JSONObject.parseObject(response);
        if (jsonObject.getInteger("status") != 0) {
            logger.warn("百度地图地址反向解析失败: lng={}, lat={}, status={}, message={}",
                    lng, lat, jsonObject.getInteger("status"), jsonObject.getString("message"));
            return null;
        }
        String address = jsonObject.getJSONObject("result").getString("formatted_address");
        logger.info("百度地图地址反向解析成功: lng={}, lat={}, address={}", lng, lat, address);
        return address;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/GpsCollectServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.utils.PlateNumberExtractor;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.service.IGpsCollectService;
@@ -258,10 +259,21 @@
    private String extractPlateNumber(String deviceName, String remark) {
        // è¿™é‡Œå¯ä»¥æ ¹æ®å®žé™…情况实现车牌号提取逻辑
        // ä¾‹å¦‚:从字符串中匹配车牌号格式
        //aaxx粤VSX120
        //在这里提取 ç²¤VSX120
        if (StringUtils.isNotEmpty(deviceName)) {
            String plateNo =PlateNumberExtractor.extractPlateNumber(deviceName);
            if (plateNo != null) {
                return plateNo;
            }
            return deviceName;
        }
        if (StringUtils.isNotEmpty(remark)) {
            String plateNo =PlateNumberExtractor.extractPlateNumber(remark);
            if (plateNo != null) {
                return plateNo;
            }
            return remark;
        }
        return null;
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -18,6 +18,8 @@
import com.ruoyi.system.mapper.LegacyTransferSyncMapper;
import com.ruoyi.system.mapper.VehicleInfoMapper;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import org.apache.poi.ss.usermodel.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,6 +31,7 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
 * æ—§ç³»ç»Ÿè½¬è¿å•同步Service业务层处理
@@ -61,6 +64,9 @@
    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private IWechatTaskNotifyService wechatTaskNotifyService;
    
    /**
     * åŒæ­¥æŒ‡å®šæ—¥æœŸèŒƒå›´çš„æ—§ç³»ç»Ÿè½¬è¿å•到新系统
@@ -199,11 +205,11 @@
     */
    private boolean syncSingleTransferOrder(String serviceOrdID, String dispatchOrdID, Map<String, Object> order) {
        log.info("开始同步单个转运单: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
        String sysTaskCode="";
        try {
            // æž„造TaskCreateVO对象
            TaskCreateVO createTaskVo = buildCreateTaskVo(serviceOrdID, dispatchOrdID, order);
            sysTaskCode = createTaskVo.getTaskCode();
            if (createTaskVo == null) {
                log.error("构造TaskCreateVO失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                return false;
@@ -215,10 +221,13 @@
                     createTaskVo.getPatient() != null ? createTaskVo.getPatient().getName() : "未知",
                     createTaskVo.getHospitalOut() != null ? createTaskVo.getHospitalOut().getName() : "未知",
                     createTaskVo.getHospitalIn() != null ? createTaskVo.getHospitalIn().getName() : "未知");
            /**
             * å¼€å•æ—¶é—´
             */
            Date ServiceOrd_CC_Time= getDateValue(order, "ServiceOrd_CC_Time");
            // è°ƒç”¨sysTaskService创建任务
            String serviceOrdClass = getStringValue(order,"ServiceOrdClass");
            String serviceOrdNo = getStringValue(order,"ServiceOrdNo");
            Integer oauserId=getIntegerValue(order,"ServiceOrd_NS_ID");
            SysUser sysUser=sysUserService.selectUserByOaUserId(oauserId);
@@ -227,10 +236,17 @@
            SysDept dept=sysDeptService.selectDeptByServiceClass(serviceOrdClass);
            Long deptId=dept==null?null:dept.getDeptId();
            int result = sysTaskService.insertTask(createTaskVo,serviceOrdID,dispatchOrdID, taskCreatorId,createUserName, deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
            int result = sysTaskService.insertTask(createTaskVo,serviceOrdID,dispatchOrdID, serviceOrdNo, taskCreatorId,createUserName, deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
            if (result > 0) {
                log.info("转运单同步成功: ServiceOrdID={}, DispatchOrdID={}, åˆ›å»ºçš„任务ID={}", serviceOrdID, dispatchOrdID, result);
                try {
                    notifyTransferOrderByWechat((long) result, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
                } catch (Exception e) {
                    log.error("转运单同步成功后发送微信通知失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
                }
                return true;
            } else {
                log.error("转运单同步失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
@@ -238,7 +254,7 @@
            }
            
        } catch (Exception e) {
            log.error("同步单个转运单异常: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
            log.error("同步单个转运单异常: ServiceOrdID={}, DispatchOrdID={},sysTaskCode:{}", serviceOrdID, dispatchOrdID,sysTaskCode, e);
            return false;
        }
    }
@@ -288,7 +304,16 @@
            return false;
        }
    }
    private String getServiceOrdCode(Date ServiceOrd_CC_Time,String serviceOrdClass,String serviceOrdNo){
        //BF20251101-serviceOrdNo;
        //将 ServiceOrd_CC_Time è½¬ä¸º yyyyMMdd æ ¼å¼
        String ServiceOrd_CC_Time_Str= DateUtils.parseDateToStr(DateUtils.YYYYMMDD,ServiceOrd_CC_Time);
        //serviceOrdNo è¿™ä¸ªæ˜¯æ•°å­—,固定3位数 ï¼Œå°†32,转成032;将1转成001
        Integer intServiceNo=Integer.valueOf(serviceOrdNo);
        String ServiceOrdNo_Str=String.format("%03d", intServiceNo);
        return serviceOrdClass+ServiceOrd_CC_Time_Str+"-"+ServiceOrdNo_Str;
    }
    /**
     * æž„造TaskCreateVO对象用于创建任务
     * 
@@ -312,18 +337,17 @@
                log.error("服务单ID不能为空");
                return null;
            }
            String serviceOrdClass = getStringValue(order, "ServiceOrdClass");
            //TODO
            TaskCreateVO createTaskVo = new TaskCreateVO();
            String Old_ServiceOrdID_TXT=getStringValue(order,"Old_ServiceOrdID_TXT");
            if(Old_ServiceOrdID_TXT!=null){
                createTaskVo.setTaskCode(Old_ServiceOrdID_TXT);
            }
            String serviceOrdCode=this.getServiceOrdCode(getDateValue(order, "ServiceOrd_CC_Time"),serviceOrdClass,getStringValue(order, "ServiceOrdNo"));
            createTaskVo.setTaskCode(serviceOrdCode);
            log.info("构造TaskCreateVO: ServiceOrdID={}, DispatchOrdID={},taskCode:{}", serviceOrdID, dispatchOrdID,serviceOrdCode);
            // è®¾ç½®åŸºæœ¬ä¿¡æ¯
            createTaskVo.setTaskType("EMERGENCY_TRANSFER"); // æ€¥æ•‘转运任务
            
            // è®¾ç½®å•据类型和任务类型ID(从旧系统字段映射)
            String serviceOrdClass = getStringValue(order, "ServiceOrdClass");
            if (StringUtils.isNotEmpty(serviceOrdClass)) {
                createTaskVo.setDocumentTypeId(serviceOrdClass);
            }
@@ -526,7 +550,7 @@
            
            // è®¾ç½®åˆ›å»ºæ—¶é—´
            // è®¾ç½®åˆ›å»ºæ—¶é—´ å¼€å•日期
            Date createTime = getDateValue(order, "ServiceOrd_CC_Time");
            if (createTime != null) {
                createTaskVo.setCreateTime(createTime);
@@ -767,4 +791,72 @@
            return false;
        }
    }
}
    private void notifyTransferOrderByWechat(Long taskId,
                                             String serviceOrdID,
                                             String dispatchOrdID,
                                             String serviceOrdNo,
                                             Date serviceOrdCcTime,
                                             SysDept dept,
                                             Map<String, Object> order) {
        try {
            // èŽ·å–é€šçŸ¥æŽ¥æ”¶äººåˆ—è¡¨
            List<SysUser> receivers = getWechatNotifyUsers(dispatchOrdID, dept);
            if (receivers == null || receivers.isEmpty()) {
                log.info("旧系统同步转运单无可用微信接收人,taskId={}", taskId);
                return;
            }
            // æå–接收人 ID åˆ—表
            List<Long> userIds = new ArrayList<>();
            for (SysUser user : receivers) {
                if (user != null && user.getUserId() != null) {
                    userIds.add(user.getUserId());
                }
            }
            // è°ƒç”¨ç»Ÿä¸€çš„微信通知服务
            int successCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds);
            log.info("旧系统同步转运单微信通知发送完成,taskId={}, æˆåŠŸ={}", taskId, successCount);
        } catch (Exception e) {
            log.error("notifyTransferOrderByWechat发生异常, serviceOrdID={}, dispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
        }
    }
    private List<SysUser> getWechatNotifyUsers(String dispatchOrdID, SysDept dept) {
        try {
            List<SysUser> result = new ArrayList<>();
            List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
            if (assignees != null && !assignees.isEmpty()) {
                for (TaskCreateVO.AssigneeInfo assigneeInfo : assignees) {
                    if (assigneeInfo == null || assigneeInfo.getUserId() == null) {
                        continue;
                    }
                    SysUser user = sysUserService.selectUserById(assigneeInfo.getUserId());
                    if (user != null && StringUtils.isNotEmpty(user.getOpenId())) {
                        result.add(user);
                    }
                }
            }
            if (!result.isEmpty()) {
                return result;
            }
            if (dept == null || StringUtils.isEmpty(dept.getPhone())) {
                return result;
            }
            SysUser leader = sysUserService.selectUserByPhonenumber(dept.getPhone());
            if (leader != null && StringUtils.isNotEmpty(leader.getOpenId())) {
                result.add(leader);
            }
            return result;
        } catch (Exception e) {
            log.error("获取旧系统同步转运单微信通知接收人失败, dispatchOrdID={}", dispatchOrdID, e);
            return new ArrayList<>();
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -277,7 +277,7 @@
        
        // ä¿å­˜æ€¥æ•‘转运扩展信息
        if (result > 0 && "EMERGENCY_TRANSFER".equals(createVO.getTaskType())) {
            saveEmergencyInfo(task.getTaskId(),username, createVO,null,null);
            saveEmergencyInfo(task.getTaskId(),username, createVO,null,null,null);
        }
        
        // ä¿å­˜ç¦ç¥‰è½¦æ‰©å±•信息
@@ -351,7 +351,7 @@
     */
    @Override
    @Transactional
    public int insertTask(TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId, Long userId,String userName, Long deptId, Date createTime, Date updateTime) {
    public int insertTask(TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId, String serviceOrdNo, Long userId,String userName, Long deptId, Date createTime, Date updateTime) {
        SysTask task = new SysTask();
        if(createVO.getTaskCode()!=null){
            task.setTaskCode(createVO.getTaskCode());
@@ -490,7 +490,7 @@
        
        // ä¿å­˜æ€¥æ•‘转运扩展信息
        if (result > 0 && "EMERGENCY_TRANSFER".equals(createVO.getTaskType())) {
            saveEmergencyInfo(task.getTaskId(),userName, createVO, serviceOrderId, dispatchOrderId);
            saveEmergencyInfo(task.getTaskId(),userName, createVO, serviceOrderId, dispatchOrderId, serviceOrdNo);
        }
        
        // ä¿å­˜ç¦ç¥‰è½¦æ‰©å±•信息
@@ -1649,7 +1649,7 @@
     * @param taskId ä»»åŠ¡ID
     * @param createVO ä»»åŠ¡åˆ›å»ºå¯¹è±¡
     */
    private void saveEmergencyInfo(Long taskId,String createUserName, TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId) {
    private void saveEmergencyInfo(Long taskId,String createUserName, TaskCreateVO createVO,String serviceOrderId,String dispatchOrderId, String serviceOrdNo) {
        SysTaskEmergency emergencyInfo = new SysTaskEmergency();
        emergencyInfo.setTaskId(taskId);
        
@@ -1755,6 +1755,9 @@
            emergencyInfo.setDispatchSyncTime(new Date());
            emergencyInfo.setDispatchSyncErrorMsg("旧系统同步过来");
        }
        if(serviceOrdNo!=null){
            emergencyInfo.setLegacyServiceOrdNo(serviceOrdNo);
        }
        // ç³»ç»Ÿå­—段
        emergencyInfo.setCreateTime(DateUtils.getNowDate());
        emergencyInfo.setUpdateTime(DateUtils.getNowDate());
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskVehicleServiceImpl.java
@@ -2,7 +2,9 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,7 +13,9 @@
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.mapper.SysTaskVehicleMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.VehicleInfoMapper;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskVehicle;
import com.ruoyi.system.domain.VehicleInfo;
import com.ruoyi.system.service.ISysTaskVehicleService;
@@ -32,6 +36,9 @@
    
    @Autowired
    private VehicleInfoMapper vehicleInfoMapper;
    @Autowired
    private SysTaskMapper sysTaskMapper;
    /**
     * æŸ¥è¯¢ä»»åŠ¡è½¦è¾†å…³è”
@@ -308,4 +315,63 @@
        taskVehicle.setUpdateTime(DateUtils.getNowDate());
        return sysTaskVehicleMapper.updateSysTaskVehicle(taskVehicle);
    }
    /**
     * æ‰¹é‡èŽ·å–è½¦è¾†å½“å‰ä»»åŠ¡çŠ¶æ€
     *
     * @param vehicleIds è½¦è¾†ID列表
     * @return Map<车辆ID, Map<"taskCode": ä»»åŠ¡ç¼–å·, "taskStatus": ä»»åŠ¡çŠ¶æ€>>
     */
    @Override
    public Map<Long, Map<String, Object>> batchGetVehicleCurrentTaskStatus(List<Long> vehicleIds) {
        Map<Long, Map<String, Object>> resultMap = new HashMap<>();
        if (vehicleIds == null || vehicleIds.isEmpty()) {
            return resultMap;
        }
        try {
            // å¯¹æ¯ä¸ªè½¦è¾†æŸ¥è¯¢å…¶å½“前正在进行的任务
            for (Long vehicleId : vehicleIds) {
                if (vehicleId == null) {
                    continue;
                }
                // æŸ¥è¯¢è½¦è¾†çš„æ´»è·ƒä»»åŠ¡ï¼ˆæœªå®Œæˆã€æœªå–æ¶ˆçš„ä»»åŠ¡ï¼‰
                List<SysTask> activeTasks = sysTaskMapper.selectActiveTasksByVehicleId(vehicleId);
                if (activeTasks != null && !activeTasks.isEmpty()) {
                    // å–第一个活跃任务(最新的)
                    SysTask currentTask = activeTasks.get(0);
                    Map<String, Object> taskInfo = new HashMap<>();
                    taskInfo.put("taskCode", currentTask.getTaskCode());
                    taskInfo.put("taskStatus", currentTask.getTaskStatus());
                    taskInfo.put("taskId", currentTask.getTaskId());
                    resultMap.put(vehicleId, taskInfo);
                } else {
                    // æ²¡æœ‰æ´»è·ƒä»»åŠ¡
                    resultMap.put(vehicleId, null);
                }
            }
        } catch (Exception e) {
            logger.error("批量查询车辆任务状态失败", e);
        }
        return resultMap;
    }
    /**
     * æ›´æ–°ä»»åŠ¡è½¦è¾†å…³è”çŠ¶æ€ï¼ˆæ–°æ–¹æ³•ï¼‰
     *
     * @param id å…³è”ID
     * @param status æ–°çŠ¶æ€
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int updateSysTaskVehicleStatus(Long id, String status) {
        return updateTaskVehicleStatus(id, status);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TiandituMapServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.config.TiandituMapConfig;
import com.ruoyi.system.service.IMapService;
@@ -62,14 +63,9 @@
                return null;
            }
            
            // æå–坐标
            JSONObject result = jsonObject.getJSONObject("result");
            if (result == null) {
                logger.warn("天地图地理编码响应无result: address={}", address);
                return null;
            }
            
            JSONObject location = result.getJSONObject("location");
            JSONObject location = jsonObject.getJSONObject("location");
            if (location == null) {
                logger.warn("天地图地理编码响应无location: address={}", address);
                return null;
@@ -91,4 +87,39 @@
            return null;
        }
    }
    /**
     * ç›´æŽ¥è¿”回地址
     * @param lng ç»åº¦
     * @param lat çº¬åº¦
     * @return
     */
    @Override
    public String reverseGeocoding(double lng, double lat) {
        // æž„建天地图逆地理编码API URL
        String url = "http://api.tianditu.gov.cn/geocoder";
        String params = "postStr={\"lon\":" + lng + ",\"lat\":" + lat + ",\"ver\":1}" +
                "&type=geocode" +
                "&tk=" + tiandituMapConfig.getTk();
        logger.info("天地图逆地理编码请求: lon={}, lat={}", lng, lat);
        // å‘送HTTP请求
        String response = HttpUtils.sendGet(url, params);
        // "{\"msg\":\"ok\",\"location\":{\"score\":100,\"level\":\"门址\",\"lon\":\"116.290158\",\"lat\":\"39.894696\",\"keyWord\":\"北京市海淀区莲花池西路28号\"},\"searchVersion\":\"7.4.3V\",\"status\":\"0\"}"
        logger.info("天地图逆地理编码响应: {}", response);
        com.alibaba.fastjson.JSONObject obj= com.alibaba.fastjson.JSONObject.parseObject(response);
        if (obj.getInteger("status") !=0) {
            logger.error("天地图逆地理编码失败: {}", response);
            return null;
        }
        com.alibaba.fastjson.JSONObject location = obj.getJSONObject("result");
        if (location == null) {
            logger.error("天地图逆地理编码响应无location: {}", response);
            return null;
        }
        return location.getString("formatted_address");
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java
@@ -96,33 +96,55 @@
    @Override
    public int batchCalculateSegmentMileage(Date startTime, Date endTime) {
        try {
            // æŸ¥è¯¢æ‰€æœ‰æ´»è·ƒè½¦è¾†
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds();
            logger.info("开始批量计算GPS分段里程 - æ—¶é—´èŒƒå›´: {} åˆ° {}", startTime, endTime);
            // æŸ¥è¯¢åœ¨æŒ‡å®šæ—¶é—´èŒƒå›´å†…有GPS数据的所有车辆
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(startTime);
            
            if (vehicleIds == null || vehicleIds.isEmpty()) {
                logger.info("没有找到活跃车辆");
                return 0;
            }
            
            logger.info("找到 {} è¾†æ´»è·ƒè½¦è¾†ï¼Œå¼€å§‹é€è¾†è®¡ç®—...", vehicleIds.size());
            int successCount = 0;
            for (Long vehicleId : vehicleIds) {
            int failedCount = 0;
            // é€è¾†è®¡ç®—,包含错误处理和重试机制
            for (int i = 0; i < vehicleIds.size(); i++) {
                Long vehicleId = vehicleIds.get(i);
                try {
                    logger.info("正在处理车辆 {} ({}/{})", vehicleId, i + 1, vehicleIds.size());
                    int segmentCount = calculateVehicleSegmentMileage(vehicleId, startTime, endTime);
                    if (segmentCount > 0) {
                        successCount++;
                        logger.info("车辆 {} è®¡ç®—成功,生成 {} ä¸ªåˆ†æ®µè®°å½•", vehicleId, segmentCount);
                    } else {
                        logger.debug("车辆 {} æ— æœ‰æ–°çš„GPS分段数据", vehicleId);
                    }
                } catch (Exception e) {
                    logger.error("计算车辆 {} çš„分段里程失败", vehicleId, e);
                    failedCount++;
                    logger.error("计算车辆 {} çš„分段里程失败 ({}/{})", vehicleId, i + 1, vehicleIds.size(), e);
                    // ä¸ä¸­æ–­æ•´ä¸ªæ‰¹å¤„理,继续处理下一辆车
                }
                // æ¯å¤„理10辆车输出一次进度
                if ((i + 1) % 10 == 0) {
                    logger.info("批量计算进度: {}/{}, æˆåŠŸ: {}, å¤±è´¥: {}",
                               i + 1, vehicleIds.size(), successCount, failedCount);
                }
            }
            
            logger.info("批量分段里程计算完成 - æ—¶é—´èŒƒå›´: {} åˆ° {}, æ€»è½¦è¾†æ•°: {}, æˆåŠŸ: {}",
                       startTime, endTime, vehicleIds.size(), successCount);
            logger.info("批量分段里程计算完成 - æ—¶é—´èŒƒå›´: {} åˆ° {}, æ€»è½¦è¾†æ•°: {}, æˆåŠŸ: {}, å¤±è´¥: {}",
                       startTime, endTime, vehicleIds.size(), successCount, failedCount);
            return successCount;
            
        } catch (Exception e) {
            logger.error("批量计算分段里程失败", e);
            throw new RuntimeException("批量计算失败: " + e.getMessage());
            logger.error("批量计算分段里程失败 - æ—¶é—´èŒƒå›´: {} åˆ° {}", startTime, endTime, e);
            throw new RuntimeException("批量计算失败: " + e.getMessage(), e);
        }
    }
@@ -138,7 +160,7 @@
            logger.info("开始补偿计算 - å›žæº¯å¤©æ•°: {}, æ—¶é—´èŒƒå›´: {} åˆ° {}", lookbackDays, startTime, endTime);
            
            // æŸ¥è¯¢æ‰€æœ‰æ´»è·ƒè½¦è¾†
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds();
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(startTime);
            
            if (vehicleIds == null || vehicleIds.isEmpty()) {
                logger.info("没有找到活跃车辆");
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java
@@ -181,8 +181,14 @@
    @Override
    public int batchCalculateMileageStats(Date statDate) {
        try {
            // è®¡ç®—查询开始时间(7天前)
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(statDate);
            calendar.add(Calendar.DAY_OF_MONTH, -7);
            Date startTime = calendar.getTime();
            // æŸ¥è¯¢æ‰€æœ‰æ´»è·ƒè½¦è¾†
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds();
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(startTime);
            
            if (vehicleIds == null || vehicleIds.isEmpty()) {
                logger.info("没有找到活跃车辆");
@@ -485,8 +491,14 @@
    @Override
    public int batchAggregateFromSegmentMileage(Date statDate) {
        try {
            // è®¡ç®—查询开始时间(7天前)
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(statDate);
            calendar.add(Calendar.DAY_OF_MONTH, -7);
            Date startTime = calendar.getTime();
            // æŸ¥è¯¢æ‰€æœ‰æ´»è·ƒè½¦è¾†
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds();
            List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds(startTime);
            
            if (vehicleIds == null || vehicleIds.isEmpty()) {
                logger.info("没有找到活跃车辆");
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java
New file
@@ -0,0 +1,169 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.config.WechatConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.WechatUtils;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * å¾®ä¿¡ä»»åŠ¡é€šçŸ¥æœåŠ¡å®žçŽ°
 * ç»Ÿä¸€ç®¡ç†æ‰€æœ‰ä»»åŠ¡ç›¸å…³çš„å¾®ä¿¡è®¢é˜…æ¶ˆæ¯å‘é€é€»è¾‘
 *
 * @author ruoyi
 * @date 2025-11-30
 */
@Service
public class WechatTaskNotifyServiceImpl implements IWechatTaskNotifyService {
    private static final Logger log = LoggerFactory.getLogger(WechatTaskNotifyServiceImpl.class);
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private SysTaskEmergencyMapper sysTaskEmergencyMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private WechatConfig wechatConfig;
    /**
     * å‘送任务通知消息给指定用户列表
     *
     * @param taskId ä»»åŠ¡ID
     * @param userIds æŽ¥æ”¶ç”¨æˆ·ID列表
     * @param excludeUserId æŽ’除的用户ID(可选,如创建人)
     * @return æˆåŠŸå‘é€çš„æ¶ˆæ¯æ•°é‡
     */
    @Override
    public int sendTaskNotifyMessage(Long taskId, List<Long> userIds, Long excludeUserId) {
        if (taskId == null || userIds == null || userIds.isEmpty()) {
            log.warn("发送微信任务通知参数不完整,taskId={}, userIds={}", taskId, userIds);
            return 0;
        }
        // æŸ¥è¯¢ä»»åŠ¡ä¿¡æ¯
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task == null) {
            log.warn("微信通知失败,任务不存在,taskId={}", taskId);
            return 0;
        }
        // æŸ¥è¯¢æ€¥æ•‘信息
        SysTaskEmergency emergency = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
        // èŽ·å–å¾®ä¿¡AccessToken
        String accessToken = WechatUtils.getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
        if (StringUtils.isEmpty(accessToken)) {
            log.error("获取微信AccessToken失败,无法发送任务通知");
            return 0;
        }
        // æž„造消息数据
        Map<String, String> data = buildTaskNotifyData(task, emergency);
        // èŽ·å–æ¨¡æ¿é…ç½®
        String page = wechatConfig.getTaskDetailPage() + "?id=" + taskId;
        String templateId = wechatConfig.getTaskNotifyTemplateId();
        // å‘送消息给每个用户
        int successCount = 0;
        for (Long userId : userIds) {
            // æŽ’除指定用户
            if (excludeUserId != null && excludeUserId.equals(userId)) {
                log.debug("跳过排除用户,userId={}", userId);
                continue;
            }
            // æŸ¥è¯¢ç”¨æˆ·ä¿¡æ¯
            SysUser user = sysUserMapper.selectUserById(userId);
            if (user == null || StringUtils.isEmpty(user.getOpenId())) {
                log.debug("用户未绑定微信openId,跳过发送,userId={}", userId);
                continue;
            }
            // å‘送订阅消息
            try {
                WechatUtils.sendSubscribeMessage(accessToken, user.getOpenId(), templateId, page, data);
                successCount++;
                log.info("微信任务通知发送成功,taskId={}, userId={}, userName={}", taskId, userId, user.getUserName());
            } catch (Exception e) {
                log.error("微信任务通知发送失败,taskId={}, userId={}", taskId, userId, e);
            }
        }
        log.info("微信任务通知批量发送完成,taskId={}, æ€»æ•°={}, æˆåŠŸ={}", taskId, userIds.size(), successCount);
        return successCount;
    }
    /**
     * å‘送任务通知消息给指定用户列表(不排除任何用户)
     *
     * @param taskId ä»»åŠ¡ID
     * @param userIds æŽ¥æ”¶ç”¨æˆ·ID列表
     * @return æˆåŠŸå‘é€çš„æ¶ˆæ¯æ•°é‡
     */
    @Override
    public int sendTaskNotifyMessage(Long taskId, List<Long> userIds) {
        return sendTaskNotifyMessage(taskId, userIds, null);
    }
    /**
     * æž„造任务通知的消息数据
     *
     * @param task ä»»åŠ¡ä¿¡æ¯
     * @param emergency æ€¥æ•‘信息(可选)
     * @return æ¶ˆæ¯æ•°æ®Map
     */
    private Map<String, String> buildTaskNotifyData(SysTask task, SysTaskEmergency emergency) {
        // äº‹é¡¹æè¿°
        String problemDesc = "您有新的转运任务,请及时处理";
        // åœ°ç‚¹ä¿¡æ¯
        String location;
        if (emergency != null && StringUtils.isNotEmpty(emergency.getHospitalOutName()) && StringUtils.isNotEmpty(emergency.getHospitalInName())) {
            location = emergency.getHospitalOutName() + " â†’ " + emergency.getHospitalInName();
        } else if (StringUtils.isNotEmpty(task.getDepartureAddress()) && StringUtils.isNotEmpty(task.getDestinationAddress())) {
            location = task.getDepartureAddress() + " â†’ " + task.getDestinationAddress();
        } else {
            location = "未知地点";
        }
        // æ—¶é—´ä¿¡æ¯
        String startTimeStr;
        if (task.getPlannedStartTime() != null) {
            startTimeStr = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm", task.getPlannedStartTime());
        } else {
            startTimeStr = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm", new java.util.Date());
        }
        // è¿å•号
        String waybillNo = StringUtils.isNotEmpty(task.getTaskCode()) ? task.getTaskCode() : String.valueOf(task.getTaskId());
        // ç»„装数据(应用thing类型字段的长度限制)
        Map<String, String> data = new HashMap<>();
        // data.put("thing1", WechatUtils.truncateThingValue(problemDesc));  // æˆªæ–­thing类型字段
        // data.put("thing10", WechatUtils.truncateThingValue(location));  // æˆªæ–­thing类型字段
        data.put("time8", startTimeStr);
        data.put("character_string1", waybillNo);
        return data;
    }
}
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml
@@ -10,6 +10,7 @@
        <result property="ServiceOrdUserID" column="ServiceOrdUserID" />
        <result property="ServiceOrdAreaType" column="ServiceOrdAreaType" />
        <result property="ServiceOrdType" column="ServiceOrdType" />
        <result property="ServiceOrdNo" column="ServiceOrdNo" />
        <result property="ServiceOrdTraTxnPrice" column="ServiceOrdTraTxnPrice" />
        <result property="ServiceOrdPtOutHospID" column="ServiceOrdPtOutHospID" />
        <result property="ServiceOrdPtServicesID" column="ServiceOrdPtServicesID" />
@@ -55,6 +56,7 @@
        SELECT 
            a.ServiceOrdID,
            a.Old_ServiceOrdID_TXT,
            a.ServiceOrdNo,
            a.ServiceOrdTraVia,
            a.ServiceOrdApptDate,
            a.ServiceOrd_NS_ID,
@@ -102,6 +104,7 @@
            a.ServiceOrdID,
            a.Old_ServiceOrdID_TXT,
            a.ServiceOrdTraVia,
            a.ServiceOrdNo,
            a.ServiceOrdApptDate,
            a.ServiceOrdUserID,
            a.ServiceOrd_NS_ID,
ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml
@@ -45,6 +45,7 @@
        <result property="dispatchSyncTime"        column="dispatch_sync_time"      />
        <result property="dispatchSyncErrorMsg"    column="dispatch_sync_error_msg" />
        <result property="needResync"              column="need_resync"             />
        <result property="legacyServiceOrdNo"      column="legacy_service_ord_no"   />
        <result property="createTime"              column="create_time"             />
        <result property="updateTime"              column="update_time"             />
        <result property="createBy"                column="create_by"               />
@@ -59,7 +60,7 @@
               hospital_in_department_id, hospital_in_bed_number, hospital_in_address, hospital_in_longitude, 
               hospital_in_latitude, transfer_distance, transfer_price, passenger_contact, 
               passenger_phone, disease_ids, document_type_id, task_type_id, legacy_service_ord_id, legacy_dispatch_ord_id, 
               sync_status, sync_time, sync_error_msg, dispatch_sync_status, dispatch_sync_time, dispatch_sync_error_msg, need_resync,
               sync_status, sync_time, sync_error_msg, dispatch_sync_status, dispatch_sync_time, dispatch_sync_error_msg, need_resync, legacy_service_ord_no,
               create_time, update_time, create_by, update_by
        from sys_task_emergency
    </sql>
@@ -116,6 +117,7 @@
            <if test="dispatchSyncTime != null">dispatch_sync_time,</if>
            <if test="dispatchSyncErrorMsg != null">dispatch_sync_error_msg,</if>
            <if test="needResync != null">need_resync,</if>
            <if test="legacyServiceOrdNo != null">legacy_service_ord_no,</if>
            <if test="createTime != null">create_time,</if>
            <if test="updateTime != null">update_time,</if>
            <if test="createBy != null">create_by,</if>
@@ -161,6 +163,7 @@
            <if test="dispatchSyncTime != null">#{dispatchSyncTime},</if>
            <if test="dispatchSyncErrorMsg != null">#{dispatchSyncErrorMsg},</if>
            <if test="needResync != null">#{needResync},</if>
            <if test="legacyServiceOrdNo != null">#{legacyServiceOrdNo},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="updateTime != null">#{updateTime},</if>
            <if test="createBy != null">#{createBy},</if>
@@ -209,6 +212,7 @@
            <if test="dispatchSyncTime != null">dispatch_sync_time = #{dispatchSyncTime},</if>
            <if test="dispatchSyncErrorMsg != null">dispatch_sync_error_msg = #{dispatchSyncErrorMsg},</if>
            <if test="needResync != null">need_resync = #{needResync},</if>
            <if test="legacyServiceOrdNo != null">legacy_service_ord_no = #{legacyServiceOrdNo},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
        </trim>
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
@@ -34,6 +34,7 @@
        <result property="creatorName"      column="creator_name"      />
        <result property="assigneeName"     column="assignee_name"     />
        <result property="deptName"         column="dept_name"         />
        <result property="vehicleNo"        column="vehicle_no"        />
        <collection property="assignedVehicles" ofType="SysTaskVehicle">
            <result property="id"            column="tv_id"             />
            <result property="taskId"        column="tv_task_id"        />
@@ -57,6 +58,14 @@
               t.actual_start_time, t.actual_end_time, t.creator_id, t.assignee_id, t.dept_id,
               t.create_time, t.update_time, t.create_by, t.update_by, t.remark, t.del_flag, t.legacy_synced,
               u1.nick_name as creator_name, u2.nick_name as assignee_name, d.dept_name,
               (
                   select v2.vehicle_no
                   from sys_task_vehicle tv2
                   left join tb_vehicle_info v2 on tv2.vehicle_id = v2.vehicle_id
                   where tv2.task_id = t.task_id
                   order by tv2.assign_time asc
                   limit 1
               ) as vehicle_no,
               tv.id as tv_id, tv.task_id as tv_task_id, tv.vehicle_id as tv_vehicle_id,
               v.vehicle_no as tv_vehicle_no, v.vehicle_type as tv_vehicle_type,
               v.vehicle_brand as tv_vehicle_brand, v.vehicle_model as tv_vehicle_model,
@@ -77,6 +86,7 @@
            <if test="taskCode != null  and taskCode != ''"> and t.task_code like concat('%', #{taskCode}, '%')</if>
            <if test="taskType != null  and taskType != ''"> and t.task_type = #{taskType}</if>
            <if test="taskStatus != null  and taskStatus != ''"> and t.task_status = #{taskStatus}</if>
            <if test="vehicleNo != null  and vehicleNo != ''"> and v.vehicle_no like concat('%', #{vehicleNo}, '%')</if>
            <!-- ç»¼åˆæŸ¥è¯¢ï¼šå½“前用户所在机构 OR å½“前用户创建 OR åˆ†é…ç»™å½“前用户 -->
            <if test="(creatorId != null and creatorId != 0) or (assigneeId != null and assigneeId != 0) or (deptId != null and deptId != 0)">
                and (
ruoyi-system/src/main/resources/mapper/system/SysTaskVehicleMapper.xml
@@ -36,7 +36,9 @@
        <include refid="selectSysTaskVehicleVo"/>
        <where>  
            <if test="taskId != null "> and tv.task_id = #{taskId}</if>
            <if test="taskCode != null and taskCode != ''"> and t.task_code like concat('%', #{taskCode}, '%')</if>
            <if test="vehicleId != null "> and tv.vehicle_id = #{vehicleId}</if>
            <if test="vehicleNo != null  and vehicleNo != ''"> and v.vehicle_no like concat('%', #{vehicleNo}, '%')</if>
            <if test="status != null  and status != ''"> and tv.status = #{status}</if>
            <if test="assignBy != null  and assignBy != ''"> and tv.assign_by like concat('%', #{assignBy}, '%')</if>
        </where>
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -25,6 +25,9 @@
        <result property="updateBy"     column="update_by"    />
        <result property="updateTime"   column="update_time"  />
        <result property="remark"       column="remark"       />
        <result property="openId"       column="open_id"       />
        <result property="unionId"     column="union_id"     />
        <result property="wechatNickname" column="wechat_nickname" />
        <association property="dept"    javaType="SysDept"         resultMap="deptResult" />
        <collection  property="roles"   javaType="java.util.List"  resultMap="RoleResult" />
    </resultMap>
@@ -49,7 +52,7 @@
    </resultMap>
    
    <sql id="selectUserVo">
        select u.user_id, u.dept_id, u.user_name,u.oa_user_id, u.oa_order_class, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.oa_user_id, u.create_by, u.create_time, u.remark,
        select u.user_id, u.dept_id, u.user_name,u.oa_user_id, u.oa_order_class, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.oa_user_id, u.create_by, u.create_time, u.remark,u.open_id,u.union_id,u.wechat_nickname,
        d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        from sys_user u
ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
@@ -30,7 +30,7 @@
    <select id="selectVehicleGpsList" parameterType="VehicleGps" resultMap="VehicleGpsResult">
        <include refid="selectVehicleGpsVo"/>
        <where>  
            <if test="vehicleNo != null  and vehicleNo != ''"> and vehicle_no = #{vehicleNo}</if>
            <if test="vehicleNo != null  and vehicleNo != ''"> and v.vehicle_no like concat('%', #{vehicleNo}, '%')</if>
            <if test="longitude != null "> and longitude = #{longitude}</if>
            <if test="latitude != null "> and latitude = #{latitude}</if>
            <if test="speed != null "> and speed = #{speed}</if>
@@ -130,10 +130,11 @@
        order by collect_time
    </select>
    <!-- æŸ¥è¯¢æ´»è·ƒè½¦è¾†ID(优化:添加LIMIT限制,避免大表扫描) -->
    <select id="selectActiveVehicleIds" resultType="Long">
        select distinct vehicle_id
        from tb_vehicle_gps
        where collect_time &gt;= DATE_SUB(NOW(), INTERVAL 7 DAY)
        where collect_time &gt;= #{startTime}
        order by vehicle_id
    </select>
    
ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml
@@ -83,7 +83,7 @@
               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
        from tb_vehicle_mileage_stats s
        where s.vehicle_id = #{vehicleId} and s.stat_date = #{statDate}
        where s.vehicle_id = #{vehicleId} and DATE(s.stat_date) = DATE(#{statDate})
        limit 1
    </select>
@@ -130,6 +130,17 @@
            <if test="dataSource != null">#{dataSource},</if>
            NOW()
         </trim>
        ON DUPLICATE KEY UPDATE
            vehicle_no = VALUES(vehicle_no),
            total_mileage = VALUES(total_mileage),
            task_mileage = VALUES(task_mileage),
            non_task_mileage = VALUES(non_task_mileage),
            task_ratio = VALUES(task_ratio),
            gps_point_count = VALUES(gps_point_count),
            task_count = VALUES(task_count),
            segment_count = VALUES(segment_count),
            data_source = VALUES(data_source),
            update_time = NOW()
    </insert>
    <update id="updateVehicleMileageStats" parameterType="VehicleMileageStats">
ruoyi-ui/src/api/task.js
@@ -248,6 +248,15 @@
  })
}
// æ‰¹é‡èŽ·å–è½¦è¾†å½“å‰ä»»åŠ¡çŠ¶æ€
export function batchGetVehicleCurrentTaskStatus(vehicleIds) {
  return request({
    url: '/task/vehicle/management/currentStatus',
    method: 'post',
    data: vehicleIds
  })
}
// ========== ä»»åŠ¡æ”¯ä»˜ç›¸å…³API ==========
// èŽ·å–ä»»åŠ¡æ”¯ä»˜ä¿¡æ¯
ruoyi-ui/src/views/system/gps/index.vue
@@ -2,14 +2,12 @@
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="车牌号" prop="vehicleNo">
        <el-select v-model="queryParams.vehicleNo" placeholder="请选择车牌号" clearable size="small">
          <el-option
            v-for="item in vehicleOptions"
            :key="item.vehicleId"
            :label="item.vehicleNo"
            :value="item.vehicleNo"
          />
        </el-select>
        <el-input
          v-model="queryParams.vehicleNo"
          placeholder="请输入车牌号"
          clearable
          size="small"
        />
      </el-form-item>
      <el-form-item label="采集时间" prop="collectTime">
        <el-date-picker
ruoyi-ui/src/views/task/general/index.vue
@@ -29,6 +29,14 @@
          />
        </el-select>
      </el-form-item>
      <el-form-item label="车牌号" prop="vehicleNo">
        <el-input
          v-model="queryParams.vehicleNo"
          placeholder="请输入车牌号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="计划开始时间">
        <el-date-picker
          v-model="dateRange"
@@ -118,6 +126,12 @@
          <dict-tag :options="dict.type.sys_task_status" :value="scope.row.taskStatus"/>
        </template>
      </el-table-column>
      <el-table-column label="车牌号" align="center" prop="vehicleNo" width="120">
        <template slot-scope="scope">
          <span v-if="scope.row.vehicleNo">{{ scope.row.vehicleNo }}</span>
          <span v-else style="color: #C0C4CC;">--</span>
        </template>
      </el-table-column>
      <el-table-column label="出发地址" align="center" prop="departureAddress" show-overflow-tooltip />
      <el-table-column label="目的地址" align="center" prop="destinationAddress" show-overflow-tooltip />
      <el-table-column label="预计公里数" align="center" prop="estimatedDistance" width="120">
@@ -126,12 +140,7 @@
          <span v-else style="color: #C0C4CC;">--</span>
        </template>
      </el-table-column>
      <el-table-column label="计划开始时间" align="center" prop="plannedStartTime" width="180">
        <template slot-scope="scope">
          <span v-if="scope.row.plannedStartTime">{{ parseTime(scope.row.plannedStartTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
          <span v-else style="color: #C0C4CC;">--</span>
        </template>
      </el-table-column>
     
      <el-table-column label="创建人" align="center" prop="creatorName" />
      <el-table-column label="执行人" align="center" prop="assigneeName" />
@@ -418,6 +427,7 @@
        taskCode: null,
        taskType: null,
        taskStatus: null,
        vehicleNo: null,
        plannedStartTimeBegin: null,
        plannedStartTimeEnd: null,
      },
ruoyi-ui/src/views/task/vehicle/index.vue
@@ -1,25 +1,14 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="任务" prop="taskId">
        <el-select v-model="queryParams.taskId" placeholder="请选择任务" clearable filterable style="width: 200px">
          <el-option
            v-for="task in taskList"
            :key="task.taskId"
            :label="task.taskCode + ' - ' + getTaskTypeName(task.taskType)"
            :value="task.taskId"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="车辆" prop="vehicleId">
        <el-select v-model="queryParams.vehicleId" placeholder="请选择车辆" clearable filterable style="width: 200px">
          <el-option
            v-for="vehicle in vehicleList"
            :key="vehicle.vehicleId"
            :label="vehicle.vehicleNo + ' - ' + vehicle.deptName"
            :value="vehicle.vehicleId"
          />
        </el-select>
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
      <el-form-item label="任务编号" prop="taskCode">
        <el-input
          v-model="queryParams.taskCode"
          placeholder="请输入任务编号"
          clearable
          @keyup.enter.native="handleQuery"
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="车牌号" prop="vehicleNo">
        <el-input
@@ -27,6 +16,7 @@
          placeholder="请输入车牌号"
          clearable
          @keyup.enter.native="handleQuery"
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="关联状态" prop="status">
@@ -108,9 +98,20 @@
        </template>
      </el-table-column>
      <el-table-column label="车牌号" align="center" prop="vehicleNo" />
      <el-table-column label="车辆类型" align="center" prop="vehicleType">
      <el-table-column label="当前任务状态" align="center" prop="currentTaskStatus" width="180">
        <template slot-scope="scope">
          <dict-tag :options="dict.type.sys_vehicle_type" :value="scope.row.vehicleType"/>
          <div v-if="scope.row.currentTaskCode">
            <el-tag type="success" size="mini">
              <i class="el-icon-loading"></i> ä»»åС䏭
            </el-tag>
            <div style="font-size: 12px; color: #409EFF; margin-top: 5px;">
              {{ scope.row.currentTaskCode }}
            </div>
          </div>
          <el-tag v-else type="info" size="mini">
            <i class="el-icon-circle-check"></i> ç©ºé—²
          </el-tag>
        </template>
      </el-table-column>
      
@@ -149,6 +150,12 @@
            @click="handleStatusChange(scope.row)"
            v-hasPermi="['task:vehicle:edit']"
          >状态变更</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-tickets"
            @click="handleViewTaskList(scope.row)"
          >任务清单</el-button>
        </template>
      </el-table-column>
    </el-table>
@@ -248,16 +255,68 @@
        <el-button @click="cancelStatusChange">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
    <!-- è½¦è¾†ä»»åŠ¡æ¸…å•å¯¹è¯æ¡† -->
    <el-dialog :title="'车辆任务清单 - ' + currentVehicleNo" :visible.sync="taskListOpen" width="1200px" append-to-body>
      <el-table v-loading="taskListLoading" :data="vehicleTaskList" max-height="500">
        <el-table-column label="任务编号" align="center" prop="taskCode" width="180">
          <template slot-scope="scope">
            <el-button
              type="text"
              @click="handleViewTaskDetail(scope.row.taskId)"
              style="font-family: 'Courier New', monospace; font-size: 13px;"
            >{{ scope.row.taskCode }}</el-button>
          </template>
        </el-table-column>
        <el-table-column label="任务类型" align="center" prop="taskType" width="120">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_task_type" :value="scope.row.taskType"/>
          </template>
        </el-table-column>
        <el-table-column label="计划开始时间" align="center" prop="plannedStartTime" width="160">
          <template slot-scope="scope">
            <span v-if="scope.row.plannedStartTime">{{ parseTime(scope.row.plannedStartTime, '{y}-{m}-{d} {h}:{i}') }}</span>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
        <el-table-column label="出发地址" align="center" prop="departureAddress" show-overflow-tooltip min-width="150" />
        <el-table-column label="目标地址" align="center" prop="destinationAddress" show-overflow-tooltip min-width="150" />
        <el-table-column label="预计公里数" align="center" prop="estimatedDistance" width="110">
          <template slot-scope="scope">
            <span v-if="scope.row.estimatedDistance">{{ scope.row.estimatedDistance }} km</span>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
        <el-table-column label="任务状态" align="center" prop="taskStatus" width="100">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_task_status" :value="scope.row.taskStatus"/>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="100">
          <template slot-scope="scope">
            <el-button
              size="mini"
              type="text"
              icon="el-icon-view"
              @click="handleViewTaskDetail(scope.row.taskId)"
            >查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div slot="footer" class="dialog-footer">
        <el-button @click="taskListOpen = false">关 é—­</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import { listTaskVehicle, getTaskVehicle, delTaskVehicle, addTaskVehicle, updateTaskVehicle, updateTaskVehicleStatus, listTask } from "@/api/task";
import { listTaskVehicle, getTaskVehicle, delTaskVehicle, addTaskVehicle, updateTaskVehicle, updateTaskVehicleStatus, listTask, getTask, batchGetVehicleCurrentTaskStatus } from "@/api/task";
import { listVehicle } from "@/api/system/vehicle";
export default {
  name: "TaskVehicle",
  dicts: ['sys_task_vehicle_status', 'sys_vehicle_type', 'sys_task_type'],
  dicts: ['sys_task_vehicle_status', 'sys_vehicle_type', 'sys_task_type', 'sys_task_status'],
  data() {
    return {
      // é®ç½©å±‚
@@ -288,12 +347,19 @@
      open: false,
      // æ˜¯å¦æ˜¾ç¤ºçŠ¶æ€å˜æ›´å¼¹å‡ºå±‚
      statusOpen: false,
      // æ˜¯å¦æ˜¾ç¤ºä»»åŠ¡æ¸…å•å¼¹å‡ºå±‚
      taskListOpen: false,
      // è½¦è¾†ä»»åŠ¡åˆ—è¡¨
      vehicleTaskList: [],
      // è½¦è¾†ä»»åŠ¡åˆ—è¡¨åŠ è½½çŠ¶æ€
      taskListLoading: false,
      // å½“前查看的车牌号
      currentVehicleNo: '',
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        taskId: null,
        vehicleId: null,
        taskCode: null,
        vehicleNo: null,
        status: null,
        assignBy: null,
@@ -334,7 +400,39 @@
      listTaskVehicle(this.queryParams).then(response => {
        this.taskVehicleList = response.rows;
        this.total = response.total;
        // ä¸ºæ¯ä¸ªè½¦è¾†åŠ è½½å½“å‰ä»»åŠ¡çŠ¶æ€
        this.loadCurrentTaskStatus();
        this.loading = false;
      });
    },
    /** åŠ è½½è½¦è¾†å½“å‰ä»»åŠ¡çŠ¶æ€ï¼ˆä¼˜åŒ–ï¼šæ‰¹é‡æŸ¥è¯¢ï¼‰ */
    loadCurrentTaskStatus() {
      // æå–所有车辆ID
      const vehicleIds = this.taskVehicleList
        .map(item => item.vehicleId)
        .filter(id => id != null);
      if (vehicleIds.length === 0) {
        return;
      }
      // æ‰¹é‡æŸ¥è¯¢è½¦è¾†å½“前任务状态
      batchGetVehicleCurrentTaskStatus(vehicleIds).then(response => {
        const statusMap = response.data || {};
        // æ›´æ–°æ¯ä¸ªè½¦è¾†çš„任务状态
        this.taskVehicleList.forEach(item => {
          if (item.vehicleId && statusMap[item.vehicleId]) {
            const taskInfo = statusMap[item.vehicleId];
            this.$set(item, 'currentTaskCode', taskInfo.taskCode);
            this.$set(item, 'currentTaskStatus', taskInfo.taskStatus);
          } else {
            this.$set(item, 'currentTaskCode', null);
            this.$set(item, 'currentTaskStatus', null);
          }
        });
      }).catch(error => {
        console.error('加载车辆任务状态失败:', error);
      });
    },
    /** èŽ·å–ä»»åŠ¡åˆ—è¡¨ */
@@ -484,6 +582,61 @@
      this.statusOpen = false;
      this.statusForm = {};
    },
    /** æŸ¥çœ‹è½¦è¾†ä»»åŠ¡æ¸…å• */
    handleViewTaskList(row) {
      this.currentVehicleNo = row.vehicleNo;
      this.taskListOpen = true;
      this.taskListLoading = true;
      // æŸ¥è¯¢è¯¥è½¦è¾†çš„æ‰€æœ‰ä»»åŠ¡å…³è”
      listTaskVehicle({
        vehicleId: row.vehicleId,
        pageNum: 1,
        pageSize: 1000
      }).then(response => {
        const taskVehicles = response.rows || [];
        // èŽ·å–æ‰€æœ‰ä»»åŠ¡ID
        const taskIds = [...new Set(taskVehicles.map(item => item.taskId).filter(id => id))];
        if (taskIds.length === 0) {
          this.vehicleTaskList = [];
          this.taskListLoading = false;
          return;
        }
        // æ‰¹é‡æŸ¥è¯¢ä»»åŠ¡è¯¦æƒ…
        const taskPromises = taskIds.map(taskId =>
          getTask(taskId).then(res => res.data).catch(() => null)
        );
        Promise.all(taskPromises).then(tasks => {
          // è¿‡æ»¤æŽ‰ç©ºå€¼å¹¶æŒ‰æ—¶é—´æŽ’序
          this.vehicleTaskList = tasks
            .filter(task => task !== null)
            .sort((a, b) => {
              const timeA = new Date(a.plannedStartTime || a.createTime).getTime();
              const timeB = new Date(b.plannedStartTime || b.createTime).getTime();
              return timeB - timeA; // é™åºæŽ’列
            });
          this.taskListLoading = false;
        }).catch(error => {
          console.error('查询任务详情失败:', error);
          this.$modal.msgError('加载任务清单失败');
          this.taskListLoading = false;
        });
      }).catch(error => {
        console.error('查询车辆任务关联失败:', error);
        this.$modal.msgError('加载任务清单失败');
        this.taskListLoading = false;
      });
    },
    /** æŸ¥çœ‹ä»»åŠ¡è¯¦æƒ… */
    handleViewTaskDetail(taskId) {
      // è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…é¡µé¢
      this.$router.push('/task/general-detail/index/' + taskId);
    },
    /** ä»»åŠ¡é€‰æ‹©å˜åŒ– */
    handleTaskChange(taskId) {
      if (taskId) {
sql/add_gps_collect_time_index.sql
New file
@@ -0,0 +1,19 @@
-- ä¸ºtb_vehicle_gps表的collect_time字段添加索引
-- ç”¨äºŽä¼˜åŒ–GPS分段里程计算时的查询性能,避免全表扫描导致超时
-- æ£€æŸ¥ç´¢å¼•是否已存在(MySQL 5.5兼容语法)
-- å¦‚果索引已存在,请忽略下面的CREATE INDEX语句
-- æ·»åŠ collect_time索引(用于时间范围查询)
CREATE INDEX idx_collect_time ON tb_vehicle_gps(collect_time);
-- æ·»åŠ vehicle_id和collect_time的复合索引(用于单车辆时间范围查询)
CREATE INDEX idx_vehicle_collect_time ON tb_vehicle_gps(vehicle_id, collect_time);
-- ç´¢å¼•说明:
-- 1. idx_collect_time: ç”¨äºŽselectActiveVehicleIds查询,加速按时间过滤
-- 2. idx_vehicle_collect_time: ç”¨äºŽselectGpsDataByTimeRange和selectUncalculatedGps查询
--    æŒ‰è½¦è¾†ID+时间范围查询时可显著提升性能
-- éªŒè¯ç´¢å¼•创建成功
SHOW INDEX FROM tb_vehicle_gps WHERE Key_name IN ('idx_collect_time', 'idx_vehicle_collect_time');
sql/add_legacy_service_ord_no.sql
New file
@@ -0,0 +1,17 @@
-- ----------------------------
-- æ·»åŠ æ—§ç³»ç»ŸServiceOrdNo字段,用于保存从旧系统同步的转运单编号
-- ----------------------------
-- åœ¨æ€¥æ•‘转运扩展表中添加legacy_service_ord_no字段
ALTER TABLE sys_task_emergency
ADD COLUMN legacy_service_ord_no VARCHAR(50) NULL COMMENT '旧系统ServiceOrdNo(转运单编号)' AFTER need_resync;
-- ä¸ºè¯¥å­—段添加索引以便快速查询
ALTER TABLE sys_task_emergency
ADD INDEX idx_legacy_service_ord_no (legacy_service_ord_no);
-- æŸ¥è¯¢ç¤ºä¾‹ï¼š
-- SELECT task_id, legacy_service_ord_id, legacy_service_ord_no, legacy_dispatch_ord_id
-- FROM sys_task_emergency
-- WHERE legacy_service_ord_no IS NOT NULL
-- ORDER BY id DESC;
sql/fix_duplicate_mileage_stats.sql
New file
@@ -0,0 +1,32 @@
-- ä¿®å¤è½¦è¾†é‡Œç¨‹ç»Ÿè®¡è¡¨ä¸­çš„重复数据
-- æ‰§è¡Œå‰è¯·å…ˆå¤‡ä»½æ•°æ®ï¼
-- 1. æŸ¥çœ‹é‡å¤æ•°æ®
SELECT vehicle_id, stat_date, COUNT(*) as count
FROM tb_vehicle_mileage_stats
GROUP BY vehicle_id, stat_date
HAVING COUNT(*) > 1;
-- 2. åˆ é™¤é‡å¤è®°å½•,保留最新的一条(根据stats_id最大的保留)
DELETE t1 FROM tb_vehicle_mileage_stats t1
INNER JOIN (
    SELECT vehicle_id, stat_date, MAX(stats_id) as max_id
    FROM tb_vehicle_mileage_stats
    GROUP BY vehicle_id, stat_date
    HAVING COUNT(*) > 1
) t2 ON t1.vehicle_id = t2.vehicle_id
    AND t1.stat_date = t2.stat_date
    AND t1.stats_id < t2.max_id;
-- 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';
-- å¦‚果唯一索引不存在,重新创建
-- ALTER TABLE tb_vehicle_mileage_stats
-- ADD UNIQUE KEY `uk_vehicle_date` (`vehicle_id`, `stat_date`);
sql/gps_compensation_job.sql
New file
@@ -0,0 +1,38 @@
-- GPS分段里程补偿计算定时任务配置
-- ç”¨äºŽè¡¥å¿å› æœåŠ¡æ•…éšœã€é‡å¯ç­‰åŽŸå› å¯¼è‡´é—æ¼çš„GPS数据计算
-- GPS分段里程补偿计算任务(每天凌晨2点执行,回溯7天数据)
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.executeCompensationCalculation(''7'')',
  '0 0 2 * * ?',
  '2',
  '0',
  '1',
  'admin',
  NOW(),
  '每天凌晨2点执行,检查最近7天内未被处理的GPS坐标并进行补偿计算。参数7表示回溯7天,可根据需要调整'
);
-- é…ç½®è¯´æ˜Ž
-- 1. ä»»åŠ¡é»˜è®¤çŠ¶æ€ä¸º'1'(暂停),需要在后台管理系统中手动启动
-- 2. å»ºè®®åœ¨å‡Œæ™¨æ‰§è¡Œï¼Œé¿å…å½±å“ç™½å¤©ä¸šåŠ¡é«˜å³°
-- 3. å›žæº¯å¤©æ•°å¯é€šè¿‡å‚数调整,建议不超过30天,避免数据量过大影响性能
-- 4. è¡¥å¿è®¡ç®—逻辑:
--    - æŸ¥è¯¢æŒ‡å®šæ—¶é—´èŒƒå›´å†…所有车辆
--    - æ£€æŸ¥æ¯è¾†è½¦çš„GPS坐标是否已被分段处理(通过segment_id判断)
--    - å¯¹æœªå¤„理的GPS坐标执行分段里程计算
--    - è‡ªåŠ¨å…³è”ä»»åŠ¡ID和任务编号
-- 5. ä¸Žå®žæ—¶è®¡ç®—任务的区别:
--    - å®žæ—¶è®¡ç®—:处理最近的GPS数据(如最近10分钟)
--    - è¡¥å¿è®¡ç®—:回溯检查历史数据,补充遗漏的计算
-- 6. cron表达式说明:
--    - '0 0 2 * * ?' : æ¯å¤©å‡Œæ™¨2点执行
--    - å¯æ ¹æ®å®žé™…情况调整执行时间
-- ä½¿ç”¨ç¤ºä¾‹
-- 1. é»˜è®¤å›žæº¯7天:executeCompensationCalculation() æˆ– executeCompensationCalculation('7')
-- 2. å›žæº¯3天:executeCompensationCalculation('3')
-- 3. å›žæº¯30天:executeCompensationCalculation('30')
sql/optimize_gps_query_performance.sql
New file
@@ -0,0 +1,71 @@
-- GPS查询性能优化SQL
-- ç”¨äºŽè§£å†³GPS分段里程计算任务查询超时问题
-- 1. æ£€æŸ¥tb_vehicle_gps表的索引
SHOW INDEX FROM tb_vehicle_gps;
-- 2. æ·»åŠ ç»„åˆç´¢å¼•ï¼švehicle_id + collect_time(如果不存在)
-- è¿™ä¸ªç´¢å¼•可以大幅提升按车辆ID和时间范围查询的性能
ALTER TABLE tb_vehicle_gps
ADD INDEX idx_vehicle_collect_time (vehicle_id, collect_time);
-- 3. æ·»åŠ å•åˆ—ç´¢å¼•ï¼šcollect_time(如果不存在)
-- è¿™ä¸ªç´¢å¼•用于优化查询活跃车辆ID的查询
ALTER TABLE tb_vehicle_gps
ADD INDEX idx_collect_time (collect_time);
-- 4. æ£€æŸ¥tb_vehicle_gps_calculated表的索引
SHOW INDEX FROM tb_vehicle_gps_calculated;
-- 5. ä¼˜åŒ–tb_vehicle_gps_calculated表索引
-- æ·»åŠ gps_id索引(如果不存在),用于LEFT JOIN查询优化
ALTER TABLE tb_vehicle_gps_calculated
ADD INDEX idx_gps_id (gps_id);
-- 6. æ·»åŠ vehicle_id索引,用于按车辆查询已计算记录
ALTER TABLE tb_vehicle_gps_calculated
ADD INDEX idx_vehicle_id (vehicle_id);
-- 7. æŸ¥çœ‹è¡¨çš„统计信息
ANALYZE TABLE tb_vehicle_gps;
ANALYZE TABLE tb_vehicle_gps_calculated;
-- 8. æŸ¥çœ‹ç´¢å¼•使用情况
-- æ‰§è¡Œä»¥ä¸‹æŸ¥è¯¢æ¥éªŒè¯ç´¢å¼•是否被正确使用
EXPLAIN SELECT DISTINCT vehicle_id
FROM tb_vehicle_gps
WHERE collect_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY vehicle_id;
EXPLAIN SELECT g.gps_id, g.vehicle_id, g.device_id, g.longitude, g.latitude,
       g.altitude, g.speed, g.direction, g.collect_time
FROM tb_vehicle_gps g
LEFT JOIN tb_vehicle_gps_calculated c ON g.gps_id = c.gps_id
WHERE g.vehicle_id = 1
  AND g.collect_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
  AND g.collect_time <= NOW()
  AND c.gps_id IS NULL
ORDER BY g.collect_time;
-- 9. æ€§èƒ½ä¼˜åŒ–建议
/*
执行结果说明:
- type应该是ref或range,不应该是ALL(全表扫描)
- key应该显示使用了相应的索引
- rows应该尽可能少
如果发现索引未被使用,可能需要:
1. æ›´æ–°è¡¨ç»Ÿè®¡ä¿¡æ¯ï¼šANALYZE TABLE
2. æ£€æŸ¥MySQL版本是否支持索引优化
3. è€ƒè™‘增加MySQL的innodb_buffer_pool_size配置
*/
-- 10. æ¸…理历史GPS数据(可选,定期执行)
-- å»ºè®®ä¿ç•™æœ€è¿‘30-90天的数据,避免表过大影响查询性能
/*
DELETE FROM tb_vehicle_gps
WHERE collect_time < DATE_SUB(NOW(), INTERVAL 90 DAY);
-- ä¼˜åŒ–表空间
OPTIMIZE TABLE tb_vehicle_gps;
*/
¾ÉϵͳServiceOrdNo×Ö¶Îͬ²½¹¦ÄÜ˵Ã÷.md
New file
@@ -0,0 +1,244 @@
# æ—§ç³»ç»ŸServiceOrdNo字段同步功能说明
## ä¸€ã€åŠŸèƒ½æ¦‚è¿°
在从旧系统同步转运单(ServiceOrder)到新系统时,除了已有的`ServiceOrdID`(服务单ID)和`DispatchOrdID`(调度单ID),现在还会同步`ServiceOrdNo`(转运单编号)字段,并保存到`sys_task_emergency`表中。
## äºŒã€æ•°æ®åº“变更
### æ–°å¢žå­—段
在`sys_task_emergency`表中添加以下字段:
```sql
-- æ—§ç³»ç»ŸServiceOrdNo(转运单编号)
ALTER TABLE sys_task_emergency
ADD COLUMN legacy_service_ord_no VARCHAR(50) NULL COMMENT '旧系统ServiceOrdNo(转运单编号)' AFTER need_resync;
-- æ·»åŠ ç´¢å¼•
ALTER TABLE sys_task_emergency
ADD INDEX idx_legacy_service_ord_no (legacy_service_ord_no);
```
### å­—段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | ç¤ºä¾‹ |
|--------|------|------|------|
| legacy_service_ord_no | VARCHAR(50) | æ—§ç³»ç»Ÿè½¬è¿å•编号 | "123"、"045"等 |
**注意**:`ServiceOrdNo`字段在旧系统中是数字类型,在生成任务编号时会被格式化为3位数字字符串(如 32 â†’ "032",1 â†’ "001")。
## ä¸‰ã€ä»£ç å˜æ›´
### 1. å®žä½“类修改
**文件**: `SysTaskEmergency.java`
新增属性:
```java
/** æ—§ç³»ç»ŸServiceOrdNo(转运单编号) */
private String legacyServiceOrdNo;
```
新增getter/setter方法。
### 2. Mapper XML修改
**文件**: `SysTaskEmergencyMapper.xml`
- åœ¨`resultMap`中添加字段映射
- åœ¨`selectSysTaskEmergencyVo`中添加查询字段
- åœ¨`insertSysTaskEmergency`中添加插入逻辑
- åœ¨`updateSysTaskEmergency`中添加更新逻辑
### 3. Service接口修改
**文件**: `ISysTaskService.java`
修改方法签名,添加`serviceOrdNo`参数:
```java
public int insertTask(TaskCreateVO createVO, String serviceOrderId,
                     String dispatchOrderId, String serviceOrdNo,
                     Long userId, String userName, Long deptId,
                     Date createTime, Date updateTime);
```
### 4. Service实现修改
**文件**: `SysTaskServiceImpl.java`
1. ä¿®æ”¹`insertTask`方法签名,添加`serviceOrdNo`参数
2. ä¿®æ”¹`saveEmergencyInfo`方法签名,添加`serviceOrdNo`参数
3. åœ¨`saveEmergencyInfo`方法中保存`serviceOrdNo`:
```java
if(serviceOrdNo!=null){
    emergencyInfo.setLegacyServiceOrdNo(serviceOrdNo);
}
```
### 5. åŒæ­¥æœåŠ¡ä¿®æ”¹
**文件**: `LegacyTransferSyncServiceImpl.java`
在`syncSingleTransferOrder`方法中:
1. ä»Žæ—§ç³»ç»ŸæŸ¥è¯¢ç»“果中提取`ServiceOrdNo`:
```java
String serviceOrdNo = getStringValue(order, "ServiceOrdNo");
```
2. è°ƒç”¨`insertTask`方法时传递`serviceOrdNo`参数:
```java
int result = sysTaskService.insertTask(createTaskVo, serviceOrdID,
                                      dispatchOrdID, serviceOrdNo,
                                      taskCreatorId, createUserName,
                                      deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
```
## å››ã€æ•°æ®æµè½¬
```
旧系统 ServiceOrder è¡¨
    â†“ (查询)
LegacyTransferSyncServiceImpl
    â†“ (提取)
ServiceOrdNo å­—段
    â†“ (传递)
SysTaskService.insertTask()
    â†“ (保存)
SysTaskEmergency.legacy_service_ord_no
```
## äº”、使用场景
### 1. æ•°æ®è¿½æº¯
通过`legacy_service_ord_no`字段可以快速在旧系统中定位原始转运单数据。
### 2. ä»»åŠ¡ç¼–å·ç”Ÿæˆ
`ServiceOrdNo`字段用于生成新系统的任务编号(`task_code`),格式为:
```
{ServiceOrdClass}{YYYYMMDD}-{ServiceOrdNo(3位)}
```
例如:`BF20251101-032`
### 3. æ•°æ®æŸ¥è¯¢
查询已同步的转运单及其编号:
```sql
SELECT
    t.task_id,
    t.task_code,
    e.legacy_service_ord_id,
    e.legacy_service_ord_no,
    e.legacy_dispatch_ord_id,
    e.sync_status
FROM sys_task t
JOIN sys_task_emergency e ON t.task_id = e.task_id
WHERE e.legacy_service_ord_no IS NOT NULL
ORDER BY t.create_time DESC;
```
按`ServiceOrdNo`查询特定转运单:
```sql
SELECT
    t.task_id,
    t.task_code,
    t.task_status,
    e.patient_name,
    e.hospital_out_name,
    e.hospital_in_name
FROM sys_task t
JOIN sys_task_emergency e ON t.task_id = e.task_id
WHERE e.legacy_service_ord_no = '032';
```
## å…­ã€æµ‹è¯•验证
### æµ‹è¯•步骤
1. **执行数据库脚本**
   ```bash
   mysql -u用户名 -p数据库名 < sql/add_legacy_service_ord_no.sql
   ```
2. **重启应用**
   ç¡®ä¿æ–°ä»£ç ç”Ÿæ•ˆã€‚
3. **触发同步任务**
   - æ–¹å¼1:手动调用定时任务
   - æ–¹å¼2:等待自动定时任务执行
4. **验证数据**
   ```sql
   SELECT task_id, legacy_service_ord_id, legacy_service_ord_no
   FROM sys_task_emergency
   WHERE sync_status = 2
   ORDER BY id DESC
   LIMIT 10;
   ```
### é¢„期结果
- `legacy_service_ord_no`字段应包含从旧系统同步的`ServiceOrdNo`值
- è¯¥å­—段不为空且格式正确(通常为1-3位数字)
- ä¸Ž`legacy_service_ord_id`一一对应
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **字段可空性**:`legacy_service_ord_no`字段允许为NULL,因为:
   - æ—§ä»»åŠ¡å¯èƒ½æ²¡æœ‰è¯¥å­—æ®µ
   - ä»Žæ–°ç³»ç»Ÿåˆ›å»ºçš„任务不会有此字段
2. **数据类型**:虽然`ServiceOrdNo`在旧系统中是数字,但在新系统中使用VARCHAR类型存储,以保持原始格式。
3. **向后兼容**:此修改不影响已存在的数据和功能,只对新同步的转运单有效。
4. **任务编号生成**:`ServiceOrdNo`用于生成任务编号,但如果该字段为空,系统会自动生成新的任务编号。
## å…«ã€ç›¸å…³æ–‡ä»¶æ¸…单
### æ•°æ®åº“脚本
- `sql/add_legacy_service_ord_no.sql` - æ·»åŠ å­—æ®µçš„SQL脚本
### Java文件
- `ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java`
### Mapper文件
- `ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml`
### æ–‡æ¡£
- `旧系统ServiceOrdNo字段同步功能说明.md` - æœ¬æ–‡æ¡£
## ä¹ã€å¸¸è§é—®é¢˜
### Q1: ServiceOrdNo字段为什么是VARCHAR类型?
**A**: è™½ç„¶åœ¨æ—§ç³»ç»Ÿä¸­æ˜¯æ•°å­—类型,但使用VARCHAR可以:
- ä¿ç•™åŽŸå§‹æ ¼å¼ï¼ˆå¦‚å‰å¯¼é›¶ï¼‰
- é¿å…ç±»åž‹è½¬æ¢é”™è¯¯
- æ›´çµæ´»åœ°å¤„理特殊情况
### Q2: å¦‚果旧系统中ServiceOrdNo为空怎么办?
**A**: ä»£ç ä¸­æœ‰NULL检查,如果`ServiceOrdNo`为空,该字段在新系统中也为NULL,不影响其他业务。
### Q3: å·²åŒæ­¥çš„历史数据会更新这个字段吗?
**A**: ä¸ä¼šã€‚此功能只对新同步的转运单有效。如需更新历史数据,需要编写专门的数据迁移脚本。
### Q4: è¿™ä¸ªå­—段会影响现有功能吗?
**A**: ä¸ä¼šã€‚这是一个新增字段,不影响任何现有业务逻辑,完全向后兼容。
## åã€åŽç»­ä¼˜åŒ–建议
1. **历史数据回填**:可以编写脚本为已同步的历史任务回填`legacy_service_ord_no`字段。
2. **前端展示**:可以在任务详情页面展示该字段,方便用户查看原始转运单编号。
3. **数据校验**:可以添加数据一致性校验,确保`legacy_service_ord_id`和`legacy_service_ord_no`的对应关系正确。
---
**更新日期**: 2024-11-30
**版本**: v1.0