wlzboy
2026-03-19 c5ac97682e3b4ca748541ace97cb37a2295bd81e
feat: 增加GPS清理后台任务
13个文件已修改
3个文件已添加
840 ■■■■ 已修改文件
GPS 分段里程重复插入问题修复说明.md 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/index.vue 177 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/index.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/utils/TaskUtil.js 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
remark.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/CleanVehicleGpsTask.java 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskStatus.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleGpsService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskStatusConverter.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/clean_gps.sql 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
GPS ·Ö¶ÎÀï³ÌÖØ¸´²åÈëÎÊÌâÐÞ¸´ËµÃ÷.md
New file
@@ -0,0 +1,114 @@
# GPS åˆ†æ®µé‡Œç¨‹é‡å¤æ’入问题修复说明
## é—®é¢˜æè¿°
在执行 GPS åˆ†æ®µé‡Œç¨‹è®¡ç®—时,系统抛出以下异常:
```
org.springframework.jdbc.UncategorizedSQLException:
### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '9-2026-03-15 06:10:00' for key 'tb_vehicle_gps_segment_mileage.uk_vehicle_time'
```
## æ ¹æœ¬åŽŸå› 
数据库表 `tb_vehicle_gps_segment_mileage` å®šä¹‰äº†å”¯ä¸€ç´¢å¼• `uk_vehicle_time (vehicle_id, segment_start_time)`,用于确保同一车辆在同一时间段的分段里程记录只有一条。
虽然在代码层面(`VehicleGpsSegmentMileageServiceImpl.java` ç¬¬ 390 è¡Œï¼‰å·²ç»æœ‰æ£€æŸ¥é€»è¾‘:
```java
if (isSegmentAlreadyCalculated(vehicleId, segmentStartTime, segmentGpsList)) {
    previousSegmentLastPoint = segmentGpsList.get(segmentGpsList.size() - 1);
    continue;
}
```
但在**并发场景**下,仍可能出现竞态条件(Race Condition):
1. **线程 A** æ£€æŸ¥æ—¶é—´æ®µ 2026-03-15 06:10:00,发现不存在
2. **线程 B** åŒæ—¶æ£€æŸ¥åŒä¸€æ—¶é—´æ®µï¼Œä¹Ÿå‘现不存在
3. **线程 A** æ‰§è¡Œ INSERT
4. **线程 B** å°è¯•执行 INSERT,违反唯一索引约束,抛出异常
## è§£å†³æ–¹æ¡ˆ
修改 MyBatis Mapper XML æ–‡ä»¶ä¸­çš„ INSERT è¯­å¥ï¼Œä½¿ç”¨ MySQL çš„ `ON DUPLICATE KEY UPDATE` è¯­æ³•:
### ä¿®æ”¹æ–‡ä»¶
- `ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml`
### ä¿®æ”¹å†…容
在原有的 `insertVehicleGpsSegmentMileage` æ–¹æ³•中添加 `ON DUPLICATE KEY UPDATE` å­å¥ï¼š
```xml
<insert id="insertVehicleGpsSegmentMileage" parameterType="VehicleGpsSegmentMileage">
    INSERT INTO tb_vehicle_gps_segment_mileage
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <!-- å­—段列表 -->
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <!-- å€¼åˆ—表 -->
    </trim>
    ON DUPLICATE KEY UPDATE
    vehicle_no = VALUES(vehicle_no),
    segment_end_time = VALUES(segment_end_time),
    start_longitude = VALUES(start_longitude),
    start_latitude = VALUES(start_latitude),
    end_longitude = VALUES(end_longitude),
    end_latitude = VALUES(end_latitude),
    segment_distance = VALUES(segment_distance),
    gps_point_count = VALUES(gps_point_count),
    gps_ids = VALUES(gps_ids),
    task_id = VALUES(task_id),
    task_code = VALUES(task_code),
    calculate_method = VALUES(calculate_method),
    update_time = NOW()
</insert>
```
## æŠ€æœ¯ä¼˜åŠ¿
使用 `ON DUPLICATE KEY UPDATE` çš„好处:
1. **原子性操作**:数据库层面保证插入或更新的原子性,避免并发冲突
2. **无需额外查询**:不需要先 SELECT å†å†³å®š INSERT è¿˜æ˜¯ UPDATE
3. **性能更优**:减少一次数据库查询开销
4. **代码简洁**:不需要复杂的异常捕获和重试逻辑
5. **数据一致性**:如果发生重复,自动更新已有记录而不是报错
## å½±å“èŒƒå›´
- **受影响的功能**:GPS åˆ†æ®µé‡Œç¨‹è®¡ç®—
- **受影响的表**:`tb_vehicle_gps_segment_mileage`
- **受影响的接口**:所有调用 `insertVehicleGpsSegmentMileage` çš„æ–¹æ³•
## æµ‹è¯•建议
1. **并发测试**:模拟多个线程同时计算同一车辆的 GPS åˆ†æ®µé‡Œç¨‹
2. **重复数据测试**:手动构造重复的车辆 ID + æ—¶é—´æ®µå¼€å§‹æ—¶é—´ï¼ŒéªŒè¯æ˜¯å¦èƒ½æ­£ç¡®æ›´æ–°
3. **回归测试**:确保正常的插入功能不受影响
## éƒ¨ç½²æ­¥éª¤
1. é‡å¯åº”用服务器即可生效
2. æ— éœ€æ‰§è¡Œä»»ä½• SQL è„šæœ¬
3. æ— éœ€ä¿®æ”¹æ•°æ®åº“表结构
## åŽç»­ä¼˜åŒ–建议
1. **监控日志**:观察是否还有其他并发场景导致的类似问题
2. **事务优化**:对于批处理操作,考虑添加适当的事务隔离级别
3. **锁机制**:如果问题仍然存在,可以考虑使用数据库行锁或分布式锁
## ç›¸å…³æ–‡ä»¶
- `ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsSegmentMileageServiceImpl.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleGpsSegmentMileage.java`
- `sql/vehicle_gps_segment_mileage.sql`
---
**修复日期**: 2026-03-16
**修复人员**: AI Assistant
**问题类型**: å¹¶å‘数据一致性
app/pages/index.vue
@@ -107,24 +107,30 @@
              <view
                class="task-status"
                :class="
                  task.taskStatus === 'PENDING'
                  task.taskStatus === TaskStatus.PENDING
                    ? 'status-pending'
                    : task.taskStatus === 'DEPARTING'
                    : task.taskStatus === TaskStatus.NOT_CONFIRMED
                    ? 'status-not-confirmed'
                    : task.taskStatus === TaskStatus.NOT_DEPARTED
                    ? 'status-not-departed'
                    : task.taskStatus === TaskStatus.PARTIALLY_CONFIRMED
                    ? 'status-partially-confirmed'
                    : task.taskStatus === TaskStatus.DEPARTING
                    ? 'status-departing'
                    : task.taskStatus === 'ARRIVED'
                    : task.taskStatus === TaskStatus.ARRIVED
                    ? 'status-arrived'
                    : task.taskStatus === 'RETURNING'
                    : task.taskStatus === TaskStatus.RETURNING
                    ? 'status-returning'
                    : task.taskStatus === 'COMPLETED'
                    : task.taskStatus === TaskStatus.COMPLETED
                    ? 'status-completed'
                    : task.taskStatus === 'CANCELLED'
                    : task.taskStatus === TaskStatus.CANCELLED
                    ? 'status-cancelled'
                    : task.taskStatus === 'IN_PROGRESS'
                    : task.taskStatus === TaskStatus.IN_PROGRESS
                    ? 'status-in-progress'
                    : 'status-default'
                    : 'status-pending'
                "
              >
                {{ getStatusText(task.status) }}
                {{ getStatusText(task.taskStatus) }}
              </view>
            </view>
@@ -161,8 +167,8 @@
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="task-actions">
            <!-- å¾…处理状态: æ˜¾ç¤ºå‡ºå‘、取消 -->
            <template v-if="task.taskStatus === 'PENDING'">
            <!-- å¾…处理状态:显示出发、取消 -->
            <template v-if="task.taskStatus === TaskStatus.PENDING || task.taskStatus === TaskStatus.NOT_DEPARTED || task.taskStatus === TaskStatus.NOT_CONFIRMED || task.taskStatus === TaskStatus.PARTIALLY_CONFIRMED">
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'depart')"
@@ -177,8 +183,8 @@
              </button>
            </template>
            <!-- å‡ºå‘中状态: æ˜¾ç¤ºå·²åˆ°è¾¾ã€å¼ºåˆ¶ç»“束 -->
            <template v-else-if="task.taskStatus === 'DEPARTING'">
            <!-- å‡ºå‘中状态:显示已到达、强制结束 -->
            <template v-else-if="task.taskStatus === TaskStatus.DEPARTING">
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'arrive')"
@@ -193,8 +199,8 @@
              </button>
            </template>
            <!-- å·²åˆ°è¾¾çŠ¶æ€: æ˜¾ç¤ºå·²è¿”程 -->
            <template v-else-if="task.taskStatus === 'ARRIVED'">
            <!-- å·²åˆ°è¾¾çŠ¶æ€ï¼šæ˜¾ç¤ºå·²è¿”ç¨‹ -->
            <template v-else-if="task.taskStatus === TaskStatus.ARRIVED">
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'return')"
@@ -203,8 +209,8 @@
              </button>
            </template>
            <!-- è¿”程中状态: æ˜¾ç¤ºå·²å®Œæˆ -->
            <template v-else-if="task.taskStatus === 'RETURNING'">
            <!-- è¿”程中状态:显示已完成 -->
            <template v-else-if="task.taskStatus === TaskStatus.RETURNING">
              <button
                class="action-btn primary"
                @click="handleTaskAction(task, 'complete')"
@@ -265,10 +271,40 @@
import { formatDateTime } from "@/utils/common";
import subscribeManager from "@/utils/subscribe";
import { checkTaskCanDepart } from "@/utils/taskValidator";
import { getStatusText as getTaskStatusText, TaskStatus } from "@/utils/TaskUtil";
// ä»»åŠ¡ç±»åž‹æ˜ å°„ï¼ˆä¸´æ—¶å®šä¹‰ï¼Œé¿å…å¯¼å…¥é—®é¢˜ï¼‰
const TASK_TYPE_MAP = {
  MAINTENANCE: "维修保养",
  FUEL: "加油",
  OTHER: "其他",
  EMERGENCY_TRANSFER: "转运任务",
  WELFARE: "福祉车",
  maintenance: "维修保养",
  refuel: "加油",
  inspection: "巡检",
  emergency: "转运任务",
  welfare: "福祉车"
};
// ä»»åŠ¡çŠ¶æ€æ˜ å°„ï¼ˆä¸´æ—¶å®šä¹‰ï¼Œé¿å…å¯¼å…¥é—®é¢˜ï¼‰
const TASK_STATUS_MAP = {
  PENDING: "待处理",
  NOT_CONFIRMED: "完全未确认",
  NOT_DEPARTED: "待出发",
  PARTIALLY_CONFIRMED: "部分确认",
  DEPARTING: "出发中",
  ARRIVED: "已到达",
  RETURNING: "返程中",
  COMPLETED: "已完成",
  CANCELLED: "已取消",
  IN_PROGRESS: "任务中"
};
export default {
  data() {
    return {
      TaskStatus, // æš´éœ² TaskStatus ç»™æ¨¡æ¿ä½¿ç”¨
      // ç”¨æˆ·ç»‘定的车辆信息
      boundVehicle: "",
      boundVehicleId: null,
@@ -307,13 +343,17 @@
    runningTasks() {
      return this.displayedTaskList.filter((task) => {
        // åŒ…含待处理、出发中、已到达、返程中等所有未完成的状态
        return [
          "PENDING",
          "DEPARTING",
          "ARRIVED",
          "RETURNING",
          "IN_PROGRESS",
        ].includes(task.taskStatus);
        const activeStatuses = [
          TaskStatus.NOT_CONFIRMED,
          TaskStatus.NOT_DEPARTED,
          TaskStatus.PARTIALLY_CONFIRMED,
          TaskStatus.PENDING,
          TaskStatus.DEPARTING,
          TaskStatus.ARRIVED,
          TaskStatus.RETURNING,
          TaskStatus.IN_PROGRESS,
        ];
        return activeStatuses.includes(task.taskStatus);
      });
    },
    
@@ -651,6 +691,10 @@
    convertStatus(dbStatus) {
      const statusMap = {
        PENDING: "pending",
        NOT_CONFIRMED: "pending",
        NOT_DEPARTED: "pending",
        PARTIALLY_CONFIRMED: "pending",
        DEPARTING: "processing",
        ARRIVED: "processing",
        RETURNING: "processing",
@@ -754,7 +798,7 @@
            this.$modal
              .confirm("确定要出发吗?")
              .then(() => {
                this.updateTaskStatus(task.taskId, "DEPARTING", "任务已出发");
                this.updateTaskStatus(task.taskId, TaskStatus.DEPARTING, "任务已出发");
              })
              .catch(() => {});
          } catch (error) {
@@ -764,7 +808,7 @@
            this.$modal
              .confirm("检查任务状态失败,是否继续出发?")
              .then(() => {
                this.updateTaskStatus(task.taskId, "DEPARTING", "任务已出发");
                this.updateTaskStatus(task.taskId, TaskStatus.DEPARTING, "任务已出发");
              })
              .catch(() => {});
          }
@@ -781,7 +825,7 @@
          this.$modal
            .confirm("确认已到达目的地?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "ARRIVED", "已到达目的地");
              this.updateTaskStatus(task.taskId, TaskStatus.ARRIVED, "已到达目的地");
            })
            .catch(() => {});
          break;
@@ -791,7 +835,7 @@
          this.$modal
            .confirm("确定要强制结束此任务吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "CANCELLED", "任务已强制结束");
              this.updateTaskStatus(task.taskId, TaskStatus.CANCELLED, "任务已强制结束");
            })
            .catch(() => {});
          break;
@@ -801,7 +845,7 @@
          this.$modal
            .confirm("确认开始返程?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "RETURNING", "已开始返程");
              this.updateTaskStatus(task.taskId, TaskStatus.RETURNING, "已开始返程");
            })
            .catch(() => {});
          break;
@@ -809,7 +853,7 @@
        case "complete":
          // å·²å®Œæˆ -> çŠ¶æ€å˜ä¸ºå·²å®Œæˆ
          // éœ€è¦æ£€æŸ¥æ˜¯å¦ä¸Šä¼ äº†çŸ¥æƒ…同意书
          this.checkConsentAttachmentAndThen(task.taskId, "COMPLETED", "任务已完成");
          this.checkConsentAttachmentAndThen(task.taskId, TaskStatus.COMPLETED, "任务已完成");
          break;
      }
    },
@@ -919,14 +963,14 @@
    getLocationAndUpdateStatus(taskId, status, remark, cancelReason) {
      const that = this;
      // ä½¿ç”¨uni.getLocation获取GPS位置
      // ä½¿ç”¨ uni.getLocation èŽ·å– GPS ä½ç½®
      uni.getLocation({
        type: "gcj02",
        geocode: true,
        altitude: true,
        success: function (res) {
          console.log("GPS定位成功:", res);
          console.log("GPS å®šä½æˆåŠŸ:", res);
          const statusData = {
            taskStatus: status,
            remark: remark,
@@ -990,55 +1034,14 @@
      });
    },
    // èŽ·å–çŠ¶æ€æ ·å¼ç±»
    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] || "未知类型";
      return TASK_TYPE_MAP[type] || "未知类型";
    },
    // èŽ·å–ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      return TASK_STATUS_MAP[status] || "未知";
    },
    clickConfirmsubscribeTaskNotify() {
@@ -1328,6 +1331,18 @@
              background-color: #fff3e0;
              color: #ff9500;
            }
            &.status-not-confirmed {
              background-color: #fff3e0;
              color: #ff9500;
            }
            &.status-not-departed {
              background-color: #fff3e0;
              color: #ff9500;
            }
            &.status-partially-confirmed {
              background-color: #fff3e0;
              color: #ff9500;
            }
            // å‡ºå‘中 - è“è‰²
            &.status-departing {
app/pages/task/index.vue
@@ -158,7 +158,7 @@
                      ? 'status-cancelled'
                      : task.taskStatus === 'IN_PROGRESS'
                      ? 'status-in-progress'
                      : 'status-default'
                      : 'status-pending'
                  "
                >
                  {{ getStatusText(task.taskStatus) }}
@@ -197,7 +197,10 @@
            <!-- æ“ä½œæŒ‰é’® -->
            <view class="task-actions">
              <!-- å¾…处理状态: æ˜¾ç¤ºå‡ºå‘、取消 -->
              <template v-if="task.taskStatus === 'PENDING'">
              <template v-if="task.taskStatus === 'PENDING'
              || task.taskStatus === 'NOT_DEPARTED'
              || task.taskStatus === 'NOT_CONFIRMED'
              || task.taskStatus === 'PARTIALLY_CONFIRMED'">
                <button
                  class="action-btn primary"
                  @click="handleTaskAction(task, 'depart')"
@@ -300,6 +303,35 @@
import { mapState } from "vuex";
import { formatDateTime } from "@/utils/common";
import { checkTaskCanDepart } from "@/utils/taskValidator";
import { getStatusText as getTaskStatusText, getTaskStatusOptions, getTaskTypeText as getTaskTypeTextUtil } from "@/utils/TaskUtil";
// ä»»åŠ¡ç±»åž‹æ˜ å°„ï¼ˆä¸´æ—¶å®šä¹‰ï¼Œé¿å…å¯¼å…¥é—®é¢˜ï¼‰
const TASK_TYPE_MAP = {
  MAINTENANCE: "维修保养",
  FUEL: "加油",
  OTHER: "其他",
  EMERGENCY_TRANSFER: "转运任务",
  WELFARE: "福祉车",
  maintenance: "维修保养",
  refuel: "加油",
  inspection: "巡检",
  emergency: "转运任务",
  welfare: "福祉车"
};
// ä»»åŠ¡çŠ¶æ€æ˜ å°„ï¼ˆä¸´æ—¶å®šä¹‰ï¼Œé¿å…å¯¼å…¥é—®é¢˜ï¼‰
const TASK_STATUS_MAP = {
  PENDING: "待处理",
  NOT_CONFIRMED: "完全未确认",
  NOT_DEPARTED: "待出发",
  PARTIALLY_CONFIRMED: "部分确认",
  DEPARTING: "出发中",
  ARRIVED: "已到达",
  RETURNING: "返程中",
  COMPLETED: "已完成",
  CANCELLED: "已取消",
  IN_PROGRESS: "任务中"
};
export default {
  components: {
@@ -315,8 +347,8 @@
        vehicle: "",
        taskNo: "",
      },
      statusOptions: ["全部状态", "待处理", "处理中", "已完成", "已取消"],
      statusValues: ["", "pending", "processing", "completed", "cancelled"],
      statusOptions: ["全部状态", ...getTaskStatusOptions().map(opt => opt.label)],
      statusValues: ["", ...getTaskStatusOptions().map(opt => opt.value)],
      selectedStatus: "",
      selectedStatusText: "",
      startDate: "",
@@ -415,6 +447,9 @@
        queryParams.taskStatus = "PENDING";
      } else if (this.currentFilter === "processing") {
        queryParams.taskStatusList = [
          "NOT_CONFIRMED",
          "NOT_DEPARTED",
          "PARTIALLY_CONFIRMED",
          "DEPARTING",
          "ARRIVED",
          "RETURNING",
@@ -429,6 +464,9 @@
          const statusMap = {
            pending: "PENDING",
            processing: [
              "NOT_CONFIRMED",
              "NOT_DEPARTED",
              "PARTIALLY_CONFIRMED",
              "DEPARTING",
              "ARRIVED",
              "RETURNING",
@@ -533,6 +571,9 @@
        queryParams.taskStatus = "PENDING";
      } else if (this.currentFilter === "processing") {
        queryParams.taskStatusList = [
          "NOT_CONFIRMED",
          "NOT_DEPARTED",
          "PARTIALLY_CONFIRMED",
          "DEPARTING",
          "ARRIVED",
          "RETURNING",
@@ -1080,19 +1121,9 @@
      });
    },
    getStatusText(status) {
      const statusMap = {
        PENDING: "待处理",
        DEPARTING: "出发中",
        ARRIVED: "已到达",
        RETURNING: "返程中",
        COMPLETED: "已完成",
        CANCELLED: "已取消",
        IN_PROGRESS: "处理中", // å…¼å®¹æ—§æ•°æ®
      };
      return statusMap[status] || "未知";
    },
    // ä½¿ç”¨ TaskUtil ä¸­çš„ getStatusText æ–¹æ³•
    // getStatusText å·²åœ¨ utils/TaskUtil.js ä¸­å®šä¹‰
    // èŽ·å–çŠ¶æ€æ ·å¼ç±»
    getStatusClass(status) {
      const statusClassMap = {
@@ -1106,16 +1137,18 @@
      };
      return statusClassMap[status] || "status-default";
    },
    // ä½¿ç”¨ TaskUtil ä¸­çš„ getTaskTypeText æ–¹æ³•
    // getTaskTypeText å·²åœ¨ utils/TaskUtil.js ä¸­å®šä¹‰
    // èŽ·å–ä»»åŠ¡ç±»åž‹æ–‡æœ¬
    getTaskTypeText(type) {
      const typeMap = {
        MAINTENANCE: "维修保养",
        FUEL: "加油",
        OTHER: "其他",
        EMERGENCY_TRANSFER: "转运任务",
        WELFARE: "福祉车",
      };
      return typeMap[type] || "未知类型";
      return TASK_TYPE_MAP[type] || "未知类型";
    },
    // èŽ·å–ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      return TASK_STATUS_MAP[status] || "未知";
    },
  },
};
app/pagesTask/detail.vue
@@ -71,7 +71,7 @@
            </view>
            <!-- å½“前登录人是该执行人且未就绪时显示就绪按钮 -->
            <view 
              v-if="showAssigneeReadyFeature() && isAssigneeSelf(assignee) && !isAssigneeReady(assignee) && taskDetail.taskStatus === 'PENDING'"
              v-if="showAssigneeReadyFeature() && isAssigneeSelf(assignee) && !isAssigneeReady(assignee) && taskDetail.taskStatus === TaskStatus.PENDING"
              class="assignee-ready-btn"
              :data-user-id="assignee.userId || assignee.oaUserId"
              :data-user-name="assignee.userName"
@@ -275,7 +275,7 @@
      </view>
      
      <!-- å–消信息(仅在任务已取消且有取消原因时显示) -->
      <view class="detail-section" v-if="taskDetail.taskStatus === 'CANCELLED' && taskDetail.emergencyInfo && taskDetail.emergencyInfo.cancelReason">
      <view class="detail-section" v-if="taskDetail.taskStatus === TaskStatus.CANCELLED && taskDetail.emergencyInfo && taskDetail.emergencyInfo.cancelReason">
        <view class="section-title">取消信息</view>
        <view class="info-item">
          <view class="label">取消原因</view>
@@ -465,8 +465,11 @@
    
    <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
    <view class="action-buttons" v-if="taskDetail">
      <!-- å¾…处理状态: æ˜¾ç¤ºå‡ºå‘、取消、强制完成 -->
      <template v-if="taskDetail.taskStatus === 'PENDING' ">
      <!-- å¾…处理状态:显示出发、取消、强制完成 -->
      <template v-if="taskDetail.taskStatus === TaskStatus.PENDING
      || taskDetail.taskStatus === TaskStatus.NOT_DEPARTED
      || taskDetail.taskStatus === TaskStatus.NOT_CONFIRMED
      || taskDetail.taskStatus === TaskStatus.PARTIALLY_CONFIRMED">
        <button 
          v-if="canOperateTask()"
          class="action-btn primary" 
@@ -488,9 +491,9 @@
          å¼ºåˆ¶å®Œæˆ
        </button>
      </template>
      <!-- å‡ºå‘中状态: æ˜¾ç¤ºå·²åˆ°è¾¾ã€å¼ºåˆ¶ç»“束、强制完成 -->
      <template v-else-if="taskDetail.taskStatus === 'DEPARTING'">
      <!-- å‡ºå‘中状态:显示已到达、强制结束、强制完成 -->
      <template v-else-if="taskDetail.taskStatus === TaskStatus.DEPARTING">
        <template v-if="canOperateTask()">
          <button 
            class="action-btn primary" 
@@ -514,8 +517,8 @@
        </template>
      </template>
      
      <!-- å·²åˆ°è¾¾çŠ¶æ€: æ˜¾ç¤ºå·²è¿”程 -->
      <template v-else-if="taskDetail.taskStatus === 'ARRIVED'">
      <!-- å·²åˆ°è¾¾çŠ¶æ€ï¼šæ˜¾ç¤ºå·²è¿”ç¨‹ -->
      <template v-else-if="taskDetail.taskStatus === TaskStatus.ARRIVED">
        <template v-if="canOperateTask()">
          <button 
            class="action-btn primary" 
@@ -526,8 +529,8 @@
        </template>
      </template>
      
      <!-- è¿”程中状态: æ˜¾ç¤ºå·²å®Œæˆ -->
      <template v-else-if="taskDetail.taskStatus === 'RETURNING'">
      <!-- è¿”程中状态:显示已完成 -->
      <template v-else-if="taskDetail.taskStatus === TaskStatus.RETURNING">
        <template v-if="canOperateTask()">
          <button 
            class="action-btn primary" 
@@ -538,8 +541,8 @@
        </template>
      </template>
      
      <!-- å¤„理中状态: æ˜¾ç¤ºå¼ºåˆ¶å®Œæˆã€å–消 -->
      <template v-else-if="taskDetail.taskStatus === 'IN_PROGRESS'">
      <!-- å¤„理中状态:显示强制完成、取消 -->
      <template v-else-if="taskDetail.taskStatus === TaskStatus.IN_PROGRESS">
        <template v-if="canOperateTask()">
           <button 
            class="action-btn primary" 
@@ -579,6 +582,7 @@
  import { checkTaskInvoice } from '@/api/invoice'
  import { formatDateTime } from '@/utils/common'
  import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator'
  import { getStatusText as getTaskStatusText, getTaskTypeText as getTaskTypeTextUtil, TaskStatus } from '@/utils/TaskUtil'
  import AttachmentUpload from './components/AttachmentUpload.vue'
  import config from '@/config'
  
@@ -588,6 +592,7 @@
    },
    data() {
      return {
        TaskStatus, // æš´éœ² TaskStatus ç»™æ¨¡æ¿ä½¿ç”¨
        taskDetail: null,
        taskId: null,
        paymentInfo: null, // æ”¯ä»˜ä¿¡æ¯
@@ -663,10 +668,10 @@
          return ''
        }
        const status = this.taskDetail.taskStatus
        if (status === 'PENDING') return 'pending'
        if (['DEPARTING', 'ARRIVED', 'RETURNING', 'IN_PROGRESS'].includes(status)) return 'in_progress'
        if (status === 'COMPLETED') return 'completed'
        if (status === 'CANCELLED') return 'cancelled'
        if (status === TaskStatus.PENDING || status === TaskStatus.NOT_CONFIRMED || status === TaskStatus.NOT_DEPARTED || status === TaskStatus.PARTIALLY_CONFIRMED) return 'pending'
        if ([TaskStatus.DEPARTING, TaskStatus.ARRIVED, TaskStatus.RETURNING, TaskStatus.IN_PROGRESS].includes(status)) return 'in_progress'
        if (status === TaskStatus.COMPLETED) return 'completed'
        if (status === TaskStatus.CANCELLED) return 'cancelled'
        return ''
      },
      // æ˜¾ç¤ºè®¡åˆ’开始时间
@@ -895,28 +900,12 @@
      
      // èŽ·å–çŠ¶æ€æ–‡æœ¬
      getStatusText(status) {
        const statusMap = {
          'PENDING': '待处理',
          'DEPARTING': '出发中',
          'ARRIVED': '已到达',
          'RETURNING': '返程中',
          'COMPLETED': '已完成',
          'CANCELLED': '已取消',
          'IN_PROGRESS': '处理中' // å…¼å®¹æ—§æ•°æ®
        }
        return statusMap[status] || '未知'
        return getTaskStatusText(status)
      },
      
      // èŽ·å–ä»»åŠ¡ç±»åž‹æ–‡æœ¬
      getTaskTypeText(type) {
        const typeMap = {
          'MAINTENANCE': '维修保养',
          'FUEL': '加油',
          'OTHER': '其他',
          'EMERGENCY_TRANSFER': '转运任务',
          'WELFARE': '福祉车'
        }
        return typeMap[type] || '未知类型'
        return getTaskTypeTextUtil(type)
      },
      
      // èŽ·å–ç”¨æˆ·ç±»åž‹æ ‡ç­¾
app/utils/TaskUtil.js
New file
@@ -0,0 +1,201 @@
/**
 * ä»»åŠ¡å·¥å…·ç±»
 * æä¾›ä»»åŠ¡ç›¸å…³çš„é€šç”¨å·¥å…·æ–¹æ³•
 */
/**
 * ä»»åŠ¡çŠ¶æ€æžšä¸¾
 */
export const TaskStatus = {
  /** å¾…处理 */
  PENDING: 'PENDING',
  /** éƒ¨åˆ†ç¡®è®¤ */
  PARTIALLY_CONFIRMED: 'PARTIALLY_CONFIRMED',
  /** å®Œå…¨æœªç¡®è®¤ */
  NOT_CONFIRMED: 'NOT_CONFIRMED',
  /** æœªå‡ºè½¦ */
  NOT_DEPARTED: 'NOT_DEPARTED',
  /** å‡ºå‘中 */
  DEPARTING: 'DEPARTING',
  /** å·²åˆ°è¾¾ */
  ARRIVED: 'ARRIVED',
  /** è¿”程中 */
  RETURNING: 'RETURNING',
  /** å·²å®Œæˆ */
  COMPLETED: 'COMPLETED',
  /** å·²å–消 */
  CANCELLED: 'CANCELLED',
  /** ä»»åС䏭 (兼容旧数据) */
  IN_PROGRESS: 'IN_PROGRESS'
};
/**
 * ä»»åŠ¡çŠ¶æ€æ–‡æœ¬æ˜ å°„
 */
const STATUS_TEXT_MAP = {
  [TaskStatus.PENDING]: '待处理',
  [TaskStatus.PARTIALLY_CONFIRMED]: '部分确认',
  [TaskStatus.NOT_CONFIRMED]: '完全未确认',
  [TaskStatus.NOT_DEPARTED]: '未出车',
  [TaskStatus.DEPARTING]: '出发中',
  [TaskStatus.ARRIVED]: '已到达',
  [TaskStatus.RETURNING]: '返程中',
  [TaskStatus.COMPLETED]: '已完成',
  [TaskStatus.CANCELLED]: '已取消',
  [TaskStatus.IN_PROGRESS]: '任务中' // å…¼å®¹æ—§æ•°æ®
};
/**
 * èŽ·å–ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
 * @param {string} status - ä»»åŠ¡çŠ¶æ€ä»£ç 
 * @returns {string} ä»»åŠ¡çŠ¶æ€æ–‡æœ¬
 */
export function getStatusText(status) {
  return STATUS_TEXT_MAP[status] || '未知';
}
/**
 * èŽ·å–æ‰€æœ‰ä»»åŠ¡çŠ¶æ€é€‰é¡¹ï¼ˆé€‚ç”¨äºŽé€‰æ‹©å™¨ï¼‰
 * @returns {Array} çŠ¶æ€é€‰é¡¹æ•°ç»„
 */
export function getTaskStatusOptions() {
  return Object.keys(TaskStatus).map(key => ({
    value: TaskStatus[key],
    label: STATUS_TEXT_MAP[TaskStatus[key]]
  }));
}
/**
 * åˆ¤æ–­ä»»åŠ¡æ˜¯å¦å·²å®Œæˆ
 * @param {string} status - ä»»åŠ¡çŠ¶æ€ä»£ç 
 * @returns {boolean} æ˜¯å¦å·²å®Œæˆ
 */
export function isTaskCompleted(status) {
  return status === TaskStatus.COMPLETED;
}
/**
 * åˆ¤æ–­ä»»åŠ¡æ˜¯å¦å·²å–æ¶ˆ
 * @param {string} status - ä»»åŠ¡çŠ¶æ€ä»£ç 
 * @returns {boolean} æ˜¯å¦å·²å–消
 */
export function isTaskCancelled(status) {
  return status === TaskStatus.CANCELLED;
}
/**
 * åˆ¤æ–­ä»»åŠ¡æ˜¯å¦å¤„äºŽè¿›è¡Œä¸­çŠ¶æ€ï¼ˆåŒ…æ‹¬å¾…å¤„ç†ã€å‡ºå‘ä¸­ã€å·²åˆ°è¾¾ã€è¿”ç¨‹ä¸­ç­‰ï¼‰
 * @param {string} status - ä»»åŠ¡çŠ¶æ€ä»£ç 
 * @returns {boolean} æ˜¯å¦è¿›è¡Œä¸­
 */
export function isTaskInProgress(status) {
  const activeStatuses = [
    TaskStatus.PENDING,
    TaskStatus.PARTIALLY_CONFIRMED,
    TaskStatus.NOT_CONFIRMED,
    TaskStatus.NOT_DEPARTED,
    TaskStatus.DEPARTING,
    TaskStatus.ARRIVED,
    TaskStatus.RETURNING,
    TaskStatus.IN_PROGRESS
  ];
  return activeStatuses.includes(status);
}
/**
 * ä»»åŠ¡ç±»åž‹æžšä¸¾
 */
export const TaskType = {
  /** ç»´ä¿®ä¿å…» */
  MAINTENANCE: 'MAINTENANCE',
  /** åŠ æ²¹ */
  FUEL: 'FUEL',
  /** å…¶ä»– */
  OTHER: 'OTHER',
  /** è½¬è¿ä»»åŠ¡ */
  EMERGENCY_TRANSFER: 'EMERGENCY_TRANSFER',
  /** ç¦ç¥‰è½¦ */
  WELFARE: 'WELFARE',
  // æ—§æ ¼å¼ï¼ˆå…¼å®¹ç”¨ï¼‰
  /** ç»´ä¿®ä¿å…»ï¼ˆæ—§ï¼‰ */
  maintenance: 'maintenance',
  /** åŠ æ²¹ï¼ˆæ—§ï¼‰ */
  refuel: 'refuel',
  /** å·¡æ£€ï¼ˆæ—§ï¼‰ */
  inspection: 'inspection',
  /** è½¬è¿ä»»åŠ¡ï¼ˆæ—§ï¼‰ */
  emergency: 'emergency',
  /** ç¦ç¥‰è½¦ï¼ˆæ—§ï¼‰ */
  welfare: 'welfare'
};
/**
 * ä»»åŠ¡ç±»åž‹æ–‡æœ¬æ˜ å°„
 */
const TYPE_TEXT_MAP = {
  // æ–°æ ¼å¼ï¼ˆæ•°æ®åº“类型)
  [TaskType.MAINTENANCE]: '维修保养',
  [TaskType.FUEL]: '加油',
  [TaskType.OTHER]: '其他',
  [TaskType.EMERGENCY_TRANSFER]: '转运任务',
  [TaskType.WELFARE]: '福祉车',
  // æ—§æ ¼å¼ï¼ˆUI ç±»åž‹ï¼‰
  [TaskType.maintenance]: '维修保养',
  [TaskType.refuel]: '加油',
  [TaskType.inspection]: '巡检',
  [TaskType.emergency]: '转运任务',
  [TaskType.welfare]: '福祉车'
};
/**
 * èŽ·å–ä»»åŠ¡ç±»åž‹æ–‡æœ¬
 * @param {string} type - ä»»åŠ¡ç±»åž‹ä»£ç 
 * @returns {string} ä»»åŠ¡ç±»åž‹æ–‡æœ¬
 */
export function getTaskTypeText(type) {
  return TYPE_TEXT_MAP[type] || '未知类型';
}
/**
 * èŽ·å–æ‰€æœ‰ä»»åŠ¡ç±»åž‹é€‰é¡¹ï¼ˆé€‚ç”¨äºŽé€‰æ‹©å™¨ï¼‰
 * @returns {Array} ç±»åž‹é€‰é¡¹æ•°ç»„
 */
export function getTaskTypeOptions() {
  return Object.keys(TaskType).map(key => ({
    value: TaskType[key],
    label: TYPE_TEXT_MAP[TaskType[key]]
  }));
}
// é»˜è®¤å¯¼å‡º
const TaskUtil = {
  TaskStatus,
  TaskType,
  getStatusText,
  getTaskStatusOptions,
  getTaskTypeText,
  getTaskTypeOptions,
  isTaskCompleted,
  isTaskCancelled,
  isTaskInProgress
};
export default TaskUtil;
remark.md
New file
@@ -0,0 +1,6 @@
分公司要配置出发地址
更新表数据
select ServiceBranch,ServiceAddress,ServiceAddress_lat,ServiceAddress_lng,UnitName,UnitShort,ServiceMinPrice,ServiceUnitPrice,ServiceLong from IntroducerUnitData where UnitState>0 and ServiceAddress_lat is not null and ServiceAddress_lng is not null and ServiceBranch='SZ'
update IntroducerUnitData set ServiceAddress='深圳市龙岗区平湖大街159号和风居', ServiceAddress_lat=22.684596,ServiceAddress_lng=114.155988 where UnitID=80
ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
    basename: i18n/messages
  profiles:
    # çŽ¯å¢ƒ dev|test|prod
    active: prod
    active: dev
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/CleanVehicleGpsTask.java
@@ -13,17 +13,43 @@
public class CleanVehicleGpsTask {
    private static final Logger log = LoggerFactory.getLogger(CleanVehicleGpsTask.class);
    /** æ¯æ‰¹åˆ é™¤æ•°é‡ */
    private static final int BATCH_SIZE = 10000;
    /** æ¯æ‰¹é—´é𔿗¶é—´ï¼ˆæ¯«ç§’),避免连续删除对数据库压力过大 */
    private static final long BATCH_INTERVAL_MS = 500;
    @Autowired
    private IVehicleGpsService vehicleGpsService;
    /**
     * æ¸…理车辆GPS历史数据
     * æ¸…理车辆GPS历史数据(保留最近2个月,分批删除)
     */
    public void cleanVehicleGpsData() {
        try {
//            log.info("开始清理车辆GPS历史数据");
            int count = vehicleGpsService.deleteVehicleGpsBeforeDate();
//            log.info("清理车辆GPS历史数据完成,共清理{}条记录", count);
            log.info("开始清理车辆GPS历史数据(保留最近2个月,每批 {} æ¡ï¼‰", BATCH_SIZE);
            int totalCount = 0;
            int batchCount;
            int round = 0;
            do {
                batchCount = vehicleGpsService.deleteVehicleGpsBeforeDateBatch(BATCH_SIZE);
                totalCount += batchCount;
                round++;
                log.info("第 {} æ‰¹æ¸…理完成,本批删除 {} æ¡ï¼Œå·²åˆ é™¤åˆè®¡ {} æ¡", round, batchCount, totalCount);
                if (batchCount == BATCH_SIZE) {
                    // è¿˜æœ‰æ•°æ®ï¼Œç¨ä½œä¼‘眠再继续
                    Thread.sleep(BATCH_INTERVAL_MS);
                }
            } while (batchCount == BATCH_SIZE);
            log.info("清理车辆GPS历史数据完成,共 {} æ‰¹ï¼Œåˆ é™¤ {} æ¡è®°å½•", round, totalCount);
            if (totalCount > 0) {
                log.info("开始执行 OPTIMIZE TABLE,回收磁盘空间(此操作耗时较长,请勿中断)");
                vehicleGpsService.optimizeVehicleGpsTable();
                log.info("OPTIMIZE TABLE æ‰§è¡Œå®Œæˆ");
            } else {
                log.info("本次无可清理的记录,跳过 OPTIMIZE TABLE");
            }
        } catch (Exception e) {
            log.error("清理车辆GPS历史数据异常", e);
        }
ruoyi-system/src/main/java/com/ruoyi/system/domain/enums/TaskStatus.java
@@ -9,7 +9,20 @@
    
    /** å¾…处理 */
    PENDING("PENDING", "待处理"),
    /**
     * éƒ¨åˆ†ç¡®è®¤
     */
    PARTIALLY_CONFIRMED("PARTIALLY_CONFIRMED", "部分确认"),
    /**
     * å®Œå…¨æœªç¡®è®¤
     */
    NOT_CONFIRMED("NOT_CONFIRMED", "完全未确认"),
    /**
     * æœªå‡ºè½¦
     */
    NOT_DEPARTED("NOT_DEPARTED", "未出车"),
    /** å‡ºå‘中 */
    DEPARTING("DEPARTING", "出发中"),
    
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java
@@ -52,6 +52,19 @@
    public int deleteVehicleGpsBeforeDate();
    /**
     * åˆ†æ‰¹åˆ é™¤æŒ‡å®šæ—¥æœŸä¹‹å‰çš„车辆GPS数据(带 LIMIT)
     *
     * @param batchSize æ¯æ‰¹åˆ é™¤æ¡æ•°
     * @return æœ¬æ‰¹åˆ é™¤çš„记录数
     */
    public int deleteVehicleGpsBeforeDateBatch(@Param("batchSize") int batchSize);
    /**
     * ä¼˜åŒ–表,回收DELETE后的磁盘碎片空间
     */
    public void optimizeVehicleGpsTable();
    /**
     * æŸ¥è¯¢è½¦è¾†åœ¨æŒ‡å®šæ—¶é—´èŒƒå›´å†…çš„GPS数据(按采集时间排序)
     * 
     * @param vehicleId è½¦è¾†ID
ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleGpsService.java
@@ -43,4 +43,17 @@
     * @return åˆ é™¤çš„记录数
     */
    public int deleteVehicleGpsBeforeDate();
    /**
     * åˆ†æ‰¹åˆ é™¤æŒ‡å®šæ—¥æœŸä¹‹å‰çš„车辆GPS数据
     *
     * @param batchSize æ¯æ‰¹åˆ é™¤æ¡æ•°
     * @return æœ¬æ‰¹åˆ é™¤çš„记录数
     */
    public int deleteVehicleGpsBeforeDateBatch(int batchSize);
    /**
     * ä¼˜åŒ–表,回收DELETE后的磁盘碎片空间
     */
    public void optimizeVehicleGpsTable();
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleGpsServiceImpl.java
@@ -79,4 +79,20 @@
    public int deleteVehicleGpsBeforeDate() {
        return vehicleGpsMapper.deleteVehicleGpsBeforeDate();
    }
    /**
     * åˆ†æ‰¹åˆ é™¤æŒ‡å®šæ—¥æœŸä¹‹å‰çš„车辆GPS数据
     */
    @Override
    public int deleteVehicleGpsBeforeDateBatch(int batchSize) {
        return vehicleGpsMapper.deleteVehicleGpsBeforeDateBatch(batchSize);
    }
    /**
     * ä¼˜åŒ–表,回收DELETE后的磁盘碎片空间
     */
    @Override
    public void optimizeVehicleGpsTable() {
        vehicleGpsMapper.optimizeVehicleGpsTable();
    }
ruoyi-system/src/main/java/com/ruoyi/system/utils/TaskStatusConverter.java
@@ -45,11 +45,13 @@
        
        switch (legacyStatusCode) {
            case 0:  // æ–°è°ƒåº¦å•(未下发)
            case 1:  // å®Œå…¨æœªç¡®è®¤
            case 2:  // éƒ¨åˆ†å·²ç¡®è®¤
            case 3:  // æœªå‡ºè½¦
                return TaskStatus.PENDING;
            case 1:  // å®Œå…¨æœªç¡®è®¤
                return TaskStatus.NOT_CONFIRMED;
            case 2:  // éƒ¨åˆ†å·²ç¡®è®¤
                return TaskStatus.PARTIALLY_CONFIRMED;
            case 3:  // æœªå‡ºè½¦
                return TaskStatus.NOT_DEPARTED;
            case 4:  // å·²å‡ºè½¦ï¼ˆåŽ»æŽ¥æ‚£è€…é€”ä¸­ï¼‰
                return TaskStatus.DEPARTING;
                
@@ -85,11 +87,32 @@
            log.warn("新系统任务状态为空");
            return null;
        }
        /**
         * 0    0 - æ–°è°ƒåº¦å•(未下发)
         * 1    1 - å®Œå…¨æœªç¡®è®¤
         * 2    2 - éƒ¨åˆ†å·²ç¡®è®¤
         * 3    æœªå‡ºè½¦
         * 4    3 - å·²å‡ºè½¦ï¼ˆåŽ»æŽ¥æ‚£è€…é€”ä¸­ï¼‰
         * 5    å·²å‡ºè½¦ï¼ˆç­‰å¾…患者)
         * 6    4 - å·²å‡ºè½¦ï¼ˆæœåŠ¡ä¸­ï¼‰
         * 7    5 - å·²é€è¾¾ï¼ˆå›žç¨‹ä¸­ï¼‰
         * 8    å·²è¿”回
         * 9    è·‘空单,已返回
         * 10    å–消
         * 11    å·²æäº¤ï¼Œç­‰å¾…审核
         * 12    å®¡æ ¸å®Œæˆ
         * 13    å®¡æ ¸ä¸é€šè¿‡
         * 14    å·²é©»ç‚¹
         */
        switch (taskStatus) {
            case PENDING:
            case NOT_DEPARTED:
                return 3;  // æœªå‡ºè½¦
            case NOT_CONFIRMED:
                return 1;  // å®Œå…¨æœªç¡®è®¤
            case PARTIALLY_CONFIRMED:
                return 2;  // éƒ¨åˆ†ç¡®è®¤
            case PENDING:
                return 0;  // æœªå‡ºè½¦
            case DEPARTING:
                return 4;  // å·²å‡ºè½¦ï¼ˆåŽ»æŽ¥æ‚£è€…é€”ä¸­ï¼‰
                
ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
@@ -112,14 +112,20 @@
    </select>
    <delete id="deleteVehicleGpsBeforeDate">
        delete g from tb_vehicle_gps g
        where g.collect_time &lt; (
            select date_sub(max(collect_time), interval 2 day)
            from tb_vehicle_gps g2
            where g2.vehicle_id = g.vehicle_id
        )
        delete from tb_vehicle_gps
        where collect_time &lt; DATE_SUB(NOW(), INTERVAL 2 MONTH)
    </delete>
    <delete id="deleteVehicleGpsBeforeDateBatch">
        delete from tb_vehicle_gps
        where collect_time &lt; DATE_SUB(NOW(), INTERVAL 2 MONTH)
        LIMIT #{batchSize}
    </delete>
    <update id="optimizeVehicleGpsTable">
        OPTIMIZE TABLE tb_vehicle_gps
    </update>
    <select id="selectGpsDataByTimeRange" resultMap="VehicleGpsResult">
        select gps_id, vehicle_id, device_id, longitude, latitude, altitude, speed, direction, 
               collect_time, device_report_time, platform_process_time, create_time
ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
@@ -99,6 +99,7 @@
        WHERE task_id = #{taskId}
    </select>
        
    <!-- æ’入或更新分段里程记录(避免重复插入) -->
    <insert id="insertVehicleGpsSegmentMileage" parameterType="VehicleGpsSegmentMileage">
        INSERT INTO tb_vehicle_gps_segment_mileage
        <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -133,6 +134,20 @@
            <if test="taskCode != null">#{taskCode},</if>
            <if test="calculateMethod != null">#{calculateMethod},</if>
        </trim>
        ON DUPLICATE KEY UPDATE
        vehicle_no = VALUES(vehicle_no),
        segment_end_time = VALUES(segment_end_time),
        start_longitude = VALUES(start_longitude),
        start_latitude = VALUES(start_latitude),
        end_longitude = VALUES(end_longitude),
        end_latitude = VALUES(end_latitude),
        segment_distance = VALUES(segment_distance),
        gps_point_count = VALUES(gps_point_count),
        gps_ids = VALUES(gps_ids),
        task_id = VALUES(task_id),
        task_code = VALUES(task_code),
        calculate_method = VALUES(calculate_method),
        update_time = NOW()
    </insert>
    <update id="updateVehicleGpsSegmentMileage" parameterType="VehicleGpsSegmentMileage">
sql/clean_gps.sql
@@ -1,27 +1,36 @@
-- ----------------------------
-- æ¸…理车辆GPS历史数据定时任务配置
-- è¯´æ˜Žï¼šæ¯å¤©å‡Œæ™¨2点执行,保留最近2个月数据,超出部分分批删除(每批1万条)
-- ----------------------------
-- å¦‚果已存在同名任务先删除,避免重复插入
DELETE FROM sys_job
WHERE job_name = '清理车辆GPS历史数据' AND job_group = 'DEFAULT';
INSERT INTO sys_job (
    job_name,
    job_group,
    invoke_target,
    cron_expression,
    misfire_policy,
    concurrent,
    status,
    create_by,
    create_time,
    update_by,
    update_time,
    job_name,
    job_group,
    invoke_target,
    cron_expression,
    misfire_policy,
    concurrent,
    status,
    create_by,
    create_time,
    update_by,
    update_time,
    remark
) VALUES (
    '清理车辆GPS历史数据',
    'DEFAULT',
    'cleanVehicleGpsTask.cleanVehicleGpsData()',
    '0 0 1 * * ?',
    '1',
    '0 0 2 * * ?',
    '3',
    '1',
    '0',
    'admin',
    sysdate(),
    'admin',
    sysdate(),
    '每天凌晨1点清理车辆GPS历史数据,只保留每台车最后2天的数据'
    '每天凌晨2点执行,分批删除tb_vehicle_gps中超过2个月的历史数据(每批1万条,批次间隔500ms),删除完成后执行OPTIMIZE TABLE回收磁盘空间'
);