From 6676a35122fd9c97d1b1679c211bc8a9b97f08f2 Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期二, 24 三月 2026 23:17:37 +0800
Subject: [PATCH] feat: 增加日志记录历史消息

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java   |   83 +++++++
 ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml       |   64 ++++++
 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java       |  155 +++++++++++++++
 app/pages/login.vue                                                                |   42 ++++
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java     |   23 ++
 ruoyi-ui/src/views/task/general/detail.vue                                         |  113 +++++++++++
 sql/sys_task_status_history.sql                                                    |   32 +++
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java |   36 +++
 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java                    |    3 
 ruoyi-ui/src/api/task.js                                                           |    8 
 10 files changed, 552 insertions(+), 7 deletions(-)

diff --git a/app/pages/login.vue b/app/pages/login.vue
index e30db61..5f94b25 100644
--- a/app/pages/login.vue
+++ b/app/pages/login.vue
@@ -21,7 +21,7 @@
           <image :src="codeUrl" @click="getCode" class="login-code-img" mode="aspectFit"></image>
         </view>
       </view>
-      <view class="agreement-checkbox">
+      <view class="agreement-checkbox" :class="{ 'agreement-highlight': highlightAgreement }">
         <checkbox-group @change="onAgreementChange">
           <label class="checkbox-label">
             <checkbox :checked="agreedToPolicy" value="agreed" color="#007AFF" class="round-checkbox" style="margin-top: 0;" />
@@ -47,8 +47,18 @@
           <text class="cuIcon-wechat" style="margin-right: 10rpx;"></text>
         鎵嬫満鍙风爜蹇嵎鐧诲綍
         </button>
+        <!-- 鏈悓鎰忓崗璁椂锛屾樉绀烘櫘閫氭寜閽紝鐐瑰嚮鍚庡脊鎻愮ず -->
         <button 
-          v-else-if="isWechat"
+          v-else-if="isWechat && !agreedToPolicy"
+          @click="checkAgreementBeforePhone"
+          class="wechat-login-btn cu-btn block bg-green lg round"
+          style="margin-top: 20rpx;">
+          <text class="cuIcon-wechat" style="margin-right: 10rpx;"></text>
+          鎵嬫満鍙风爜蹇嵎鐧诲綍
+        </button>
+        <!-- 宸插悓鎰忓崗璁椂锛屾樉绀虹湡瀹炴巿鏉冩寜閽� -->
+        <button 
+          v-else-if="isWechat && agreedToPolicy"
           open-type="getPhoneNumber" 
           @getphonenumber="onGetPhoneNumber"
           class="wechat-login-btn cu-btn block bg-green lg round"
@@ -82,6 +92,8 @@
           code: "",
           uuid: ''
         },
+        // 鍗忚鍖哄煙楂樹寒鎻愮ず鐘舵��
+        highlightAgreement: false,
         // 寰俊涓�閿櫥褰曠浉鍏�
         isWechat: false, // 鏄惁涓哄井淇″皬绋嬪簭鐜
         wechatOpenId: '', // 寰俊OpenID
@@ -206,6 +218,16 @@
           this.wechatUnionId = savedUnionId // 鍙兘涓簄ull
           this.loginByOpenId()
         }
+      },
+      
+      // 鏈悓鎰忓崗璁椂鐐瑰嚮鎵嬫満鍙峰揩鎹风櫥褰曠殑澶勭悊
+      checkAgreementBeforePhone() {
+        this.$modal.msgError("璇峰厛闃呰骞跺悓鎰忕敤鎴峰崗璁拰闅愮鏀跨瓥")
+        // 婊氬姩鍒板崗璁尯鍩燂紙楂樹寒鎻愮ず锛�
+        this.highlightAgreement = true
+        setTimeout(() => {
+          this.highlightAgreement = false
+        }, 2000)
       },
       
       // 澶勭悊鑾峰彇鎵嬫満鍙风殑鍥炶皟
@@ -501,6 +523,22 @@
         }
       }
       
+      .agreement-highlight {
+        animation: highlight-shake 0.5s ease-in-out;
+        background-color: #fff3cd;
+        border-radius: 16rpx;
+        border: 2rpx solid #ffc107;
+      }
+      
+      @keyframes highlight-shake {
+        0% { transform: translateX(0); }
+        20% { transform: translateX(-8rpx); }
+        40% { transform: translateX(8rpx); }
+        60% { transform: translateX(-8rpx); }
+        80% { transform: translateX(8rpx); }
+        100% { transform: translateX(0); }
+      }
+      
       .agreement-checkbox {
         margin: 50rpx 0 30rpx 0;
         padding: 20rpx;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
index 43d7369..a702997 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -13,6 +13,7 @@
 import com.ruoyi.system.service.*;
 import com.ruoyi.system.service.ILegacySystemSyncService;
 import com.ruoyi.system.service.ITaskDispatchSyncService;
+import com.ruoyi.system.mapper.SysTaskStatusHistoryMapper;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -33,6 +34,7 @@
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.system.domain.SysTask;
 import com.ruoyi.system.domain.SysTaskLog;
+import com.ruoyi.system.domain.SysTaskStatusHistory;
 import com.ruoyi.system.domain.VehicleInfo;
 import com.ruoyi.system.domain.vo.TaskQueryVO;
 import com.ruoyi.system.domain.vo.TaskCreateVO;
@@ -81,6 +83,9 @@
     
     @Autowired
     private ITaskStatusPushService taskStatusPushService;
+
+    @Autowired
+    private SysTaskStatusHistoryMapper sysTaskStatusHistoryMapper;
 
     /**
      * 鏌ヨ浠诲姟绠$悊鍒楄〃锛堝悗鍙扮鐞嗙锛�
@@ -753,4 +758,22 @@
             return error("鍚屾寮傚父: " + e.getMessage());
         }
     }
+
+    /**
+     * 鏌ヨ浠诲姟鐘舵�佸彉鏇村巻鍙�
+     */
+    @GetMapping("/{taskId}/statusHistory")
+    public AjaxResult getTaskStatusHistory(@PathVariable Long taskId) {
+        try {
+            SysTask task = sysTaskService.selectSysTaskByTaskId(taskId);
+            if (task == null) {
+                return error("浠诲姟涓嶅瓨鍦�");
+            }
+            List<SysTaskStatusHistory> list = sysTaskStatusHistoryMapper.selectByTaskId(taskId);
+            return success(list);
+        } catch (Exception e) {
+            logger.error("鏌ヨ浠诲姟鐘舵�佸巻鍙插紓甯革紝taskId: {}", taskId, e);
+            return error("鏌ヨ澶辫触: " + e.getMessage());
+        }
+    }
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java
index 1fb1f46..a231557 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java
@@ -441,6 +441,9 @@
         
         // 鐘舵�佹祦杞鍒�
         switch (currentStatus) {
+            case NOT_CONFIRMED:
+            case NOT_DEPARTED:
+            case PARTIALLY_CONFIRMED:
             case PENDING:
                 // 寰呭鐞� -> 鍑哄彂涓�佸凡鍙栨秷
                 return newStatus == TaskStatus.DEPARTING || newStatus == TaskStatus.CANCELLED;
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java
new file mode 100644
index 0000000..641654f
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java
@@ -0,0 +1,155 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+
+/**
+ * 浠诲姟鐘舵�佸彉鏇村巻鍙茶褰� sys_task_status_history
+ *
+ * @author ruoyi
+ */
+public class SysTaskStatusHistory {
+    private static final long serialVersionUID = 1L;
+
+    /** 涓婚敭ID */
+    private Long id;
+
+    /** 浠诲姟ID */
+    @Excel(name = "浠诲姟ID")
+    private Long taskId;
+
+    /** 浠诲姟缂栧彿锛堝啑浣欙級 */
+    @Excel(name = "浠诲姟缂栧彿")
+    private String taskCode;
+
+    /** 鍙樻洿鍓嶇姸鎬佺爜锛圢ULL琛ㄧず鍒濆鍒涘缓锛� */
+    @Excel(name = "鍙樻洿鍓嶇姸鎬�")
+    private String fromStatus;
+
+    /** 鍙樻洿鍓嶇姸鎬佸悕绉� */
+    @Excel(name = "鍙樻洿鍓嶇姸鎬佸悕绉�")
+    private String fromStatusName;
+
+    /** 鍙樻洿鍚庣姸鎬佺爜 */
+    @Excel(name = "鍙樻洿鍚庣姸鎬�")
+    private String toStatus;
+
+    /** 鍙樻洿鍚庣姸鎬佸悕绉� */
+    @Excel(name = "鍙樻洿鍚庣姸鎬佸悕绉�")
+    private String toStatusName;
+
+    /** 鍙樻洿鍘熷洜/澶囨敞 */
+    @Excel(name = "鍙樻洿鍘熷洜")
+    private String changeReason;
+
+    /**
+     * 瑙﹀彂鏉ユ簮
+     * APP-绉诲姩绔紝ADMIN-绠$悊鍚庡彴锛孲YSTEM-绯荤粺鑷姩锛孡EGACY-鏃х郴缁熷悓姝�
+     */
+    @Excel(name = "瑙﹀彂鏉ユ簮")
+    private String changeSource;
+
+    /** 鎿嶄綔浜篒D */
+    @Excel(name = "鎿嶄綔浜篒D")
+    private Long operatorId;
+
+    /** 鎿嶄綔浜哄鍚� */
+    @Excel(name = "鎿嶄綔浜�")
+    private String operatorName;
+
+    /** 鍙樻洿鏃堕棿 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "鍙樻洿鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date changeTime;
+
+    /** 鎿嶄綔鏃剁殑缁忓害锛圙PS瀹氫綅锛� */
+    private Double longitude;
+
+    /** 鎿嶄綔鏃剁殑绾害锛圙PS瀹氫綅锛� */
+    private Double latitude;
+
+    /** 鎿嶄綔鏃剁殑浣嶇疆鍦板潃 */
+    @Excel(name = "鎿嶄綔浣嶇疆")
+    private String locationAddress;
+
+    /** 鎿嶄綔IP鍦板潃 */
+    private String ipAddress;
+
+    /** 澶囨敞 */
+    private String remark;
+
+    // ===== 瑙﹀彂鏉ユ簮甯搁噺 =====
+    public static final String SOURCE_APP    = "APP";
+    public static final String SOURCE_ADMIN  = "ADMIN";
+    public static final String SOURCE_SYSTEM = "SYSTEM";
+    public static final String SOURCE_LEGACY = "LEGACY";
+
+    // ===== getter / setter =====
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+
+    public Long getTaskId() { return taskId; }
+    public void setTaskId(Long taskId) { this.taskId = taskId; }
+
+    public String getTaskCode() { return taskCode; }
+    public void setTaskCode(String taskCode) { this.taskCode = taskCode; }
+
+    public String getFromStatus() { return fromStatus; }
+    public void setFromStatus(String fromStatus) { this.fromStatus = fromStatus; }
+
+    public String getFromStatusName() { return fromStatusName; }
+    public void setFromStatusName(String fromStatusName) { this.fromStatusName = fromStatusName; }
+
+    public String getToStatus() { return toStatus; }
+    public void setToStatus(String toStatus) { this.toStatus = toStatus; }
+
+    public String getToStatusName() { return toStatusName; }
+    public void setToStatusName(String toStatusName) { this.toStatusName = toStatusName; }
+
+    public String getChangeReason() { return changeReason; }
+    public void setChangeReason(String changeReason) { this.changeReason = changeReason; }
+
+    public String getChangeSource() { return changeSource; }
+    public void setChangeSource(String changeSource) { this.changeSource = changeSource; }
+
+    public Long getOperatorId() { return operatorId; }
+    public void setOperatorId(Long operatorId) { this.operatorId = operatorId; }
+
+    public String getOperatorName() { return operatorName; }
+    public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
+
+    public Date getChangeTime() { return changeTime; }
+    public void setChangeTime(Date changeTime) { this.changeTime = changeTime; }
+
+    public Double getLongitude() { return longitude; }
+    public void setLongitude(Double longitude) { this.longitude = longitude; }
+
+    public Double getLatitude() { return latitude; }
+    public void setLatitude(Double latitude) { this.latitude = latitude; }
+
+    public String getLocationAddress() { return locationAddress; }
+    public void setLocationAddress(String locationAddress) { this.locationAddress = locationAddress; }
+
+    public String getIpAddress() { return ipAddress; }
+    public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
+
+    public String getRemark() { return remark; }
+    public void setRemark(String remark) { this.remark = remark; }
+
+    @Override
+    public String toString() {
+        return "SysTaskStatusHistory{" +
+                "id=" + id +
+                ", taskId=" + taskId +
+                ", taskCode='" + taskCode + '\'' +
+                ", fromStatus='" + fromStatus + '\'' +
+                ", toStatus='" + toStatus + '\'' +
+                ", changeReason='" + changeReason + '\'' +
+                ", changeSource='" + changeSource + '\'' +
+                ", operatorName='" + operatorName + '\'' +
+                ", changeTime=" + changeTime +
+                '}';
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java
new file mode 100644
index 0000000..cdf54eb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java
@@ -0,0 +1,36 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.ruoyi.system.domain.SysTaskStatusHistory;
+
+/**
+ * 浠诲姟鐘舵�佸彉鏇村巻鍙茶褰� Mapper 鎺ュ彛
+ *
+ * @author ruoyi
+ */
+public interface SysTaskStatusHistoryMapper {
+
+    /**
+     * 鎻掑叆涓�鏉$姸鎬佸彉鏇村巻鍙茶褰�
+     *
+     * @param history 鍘嗗彶璁板綍
+     * @return 褰卞搷琛屾暟
+     */
+    int insert(SysTaskStatusHistory history);
+
+    /**
+     * 鏍规嵁浠诲姟ID鏌ヨ鐘舵�佸彉鏇村巻鍙诧紙鎸夊彉鏇存椂闂村崌搴忥級
+     *
+     * @param taskId 浠诲姟ID
+     * @return 鍘嗗彶鍒楄〃
+     */
+    List<SysTaskStatusHistory> selectByTaskId(Long taskId);
+
+    /**
+     * 鏍规嵁浠诲姟缂栧彿鏌ヨ鐘舵�佸彉鏇村巻鍙�
+     *
+     * @param taskCode 浠诲姟缂栧彿
+     * @return 鍘嗗彶鍒楄〃
+     */
+    List<SysTaskStatusHistory> selectByTaskCode(String taskCode);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
index 9c69173..c7c7048 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -27,6 +27,7 @@
 import com.ruoyi.system.domain.SysTaskVehicle;
 import com.ruoyi.system.domain.SysTaskAttachment;
 import com.ruoyi.system.domain.SysTaskLog;
+import com.ruoyi.system.domain.SysTaskStatusHistory;
 import com.ruoyi.system.domain.SysTaskEmergency;
 import com.ruoyi.system.domain.SysTaskWelfare;
 import com.ruoyi.system.domain.SysTaskAssignee;
@@ -56,6 +57,9 @@
     
     @Autowired
     private SysTaskLogMapper sysTaskLogMapper;
+
+    @Autowired
+    private SysTaskStatusHistoryMapper sysTaskStatusHistoryMapper;
     
     @Autowired
     private SysTaskEmergencyMapper sysTaskEmergencyMapper;
@@ -951,6 +955,15 @@
             recordTaskLog(task.getTaskId(), "FORCE_COMPLETE", "寮哄埗瀹屾垚浠诲姟", 
                          oldStatus, task.getTaskStatus(), 
                          SecurityUtils.getUserId(), SecurityUtils.getUsername());
+            // 鍐欏叆鐘舵�佸彉鏇村巻鍙茶褰�
+            recordStatusHistory(oldTask, oldStatus,
+                    oldTaskStatus != null ? oldTaskStatus.getInfo() : oldStatus,
+                    task.getTaskStatus(),
+                    TaskStatus.getByCode(task.getTaskStatus()) != null ? TaskStatus.getByCode(task.getTaskStatus()).getInfo() : task.getTaskStatus(),
+                    task.getRemark(),
+                    SysTaskStatusHistory.SOURCE_APP,
+                    SecurityUtils.getUserId(), SecurityUtils.getUsername(),
+                    null);
             
             // 鍙戝竷浠诲姟鐘舵�佸彉鏇翠簨浠�
             TaskStatus newTaskStatus = TaskStatus.getByCode(task.getTaskStatus());
@@ -1023,6 +1036,12 @@
                          "鐘舵�侊細" + newStatus.getInfo() + "锛屽娉細" + remark, 
                          SecurityUtils.getUserId(), SecurityUtils.getUsername(),
                          locationLog);
+            // 鍐欏叆鐘舵�佸彉鏇村巻鍙茶褰�
+            recordStatusHistory(oldTask, oldTaskStatus.getCode(), oldTaskStatus.getInfo(),
+                    newStatus.getCode(), newStatus.getInfo(), remark,
+                    SysTaskStatusHistory.SOURCE_APP,
+                    SecurityUtils.getUserId(), SecurityUtils.getUsername(),
+                    locationLog);
         }
         
         // 鍙戝竷浠诲姟鐘舵�佸彉鏇翠簨浠�
@@ -1486,6 +1505,51 @@
     }
 
     /**
+     * 璁板綍浠诲姟鐘舵�佸彉鏇村巻鍙�
+     *
+     * @param task            浠诲姟瀵硅薄锛堝彇 task_id / task_code锛�
+     * @param fromStatus      鍙樻洿鍓嶇姸鎬佺爜
+     * @param fromStatusName  鍙樻洿鍓嶇姸鎬佸悕绉�
+     * @param toStatus        鍙樻洿鍚庣姸鎬佺爜
+     * @param toStatusName    鍙樻洿鍚庣姸鎬佸悕绉�
+     * @param changeReason    鍙樻洿鍘熷洜/澶囨敞
+     * @param changeSource    瑙﹀彂鏉ユ簮锛圓PP / ADMIN / SYSTEM / LEGACY锛�
+     * @param operatorId      鎿嶄綔浜� ID
+     * @param operatorName    鎿嶄綔浜哄鍚�
+     * @param locationLog     GPS 浣嶇疆淇℃伅锛堝彲涓� null锛�
+     */
+    private void recordStatusHistory(SysTask task,
+                                     String fromStatus, String fromStatusName,
+                                     String toStatus, String toStatusName,
+                                     String changeReason, String changeSource,
+                                     Long operatorId, String operatorName,
+                                     SysTaskLog locationLog) {
+        try {
+            SysTaskStatusHistory history = new SysTaskStatusHistory();
+            history.setTaskId(task.getTaskId());
+            history.setTaskCode(task.getTaskCode());
+            history.setFromStatus(fromStatus);
+            history.setFromStatusName(fromStatusName);
+            history.setToStatus(toStatus);
+            history.setToStatusName(toStatusName);
+            history.setChangeReason(changeReason);
+            history.setChangeSource(changeSource != null ? changeSource : SysTaskStatusHistory.SOURCE_APP);
+            history.setOperatorId(operatorId);
+            history.setOperatorName(operatorName);
+            history.setChangeTime(DateUtils.getNowDate());
+            history.setIpAddress("127.0.0.1");
+            if (locationLog != null) {
+                history.setLongitude(locationLog.getLongitude());
+                history.setLatitude(locationLog.getLatitude());
+                history.setLocationAddress(locationLog.getLocationAddress());
+            }
+            sysTaskStatusHistoryMapper.insert(history);
+        } catch (Exception e) {
+            log.error("璁板綍浠诲姟鐘舵�佸彉鏇村巻鍙插け璐�, taskId={}", task.getTaskId(), e);
+        }
+    }
+
+    /**
      * 鏋勫缓浠诲姟鎻忚堪
      * 
      * @param task 浠诲姟瀵硅薄
@@ -1635,6 +1699,13 @@
         }
     }
 
+    private AjaxResult getCheckCanSuccess(){
+        List<Map<String, Object>> conflicts = new ArrayList<>();
+        Map<String, Object> result = new HashMap<>();
+        result.put("valid", conflicts.isEmpty());
+        result.put("conflicts", conflicts);
+        return AjaxResult.success(result);
+    }
     /**
      * 妫�鏌ヤ换鍔℃槸鍚﹀彲浠ュ嚭鍙�
      * 妫�鏌ワ細
@@ -1646,13 +1717,18 @@
      */
     @Override
     public AjaxResult checkTaskCanDepart(Long taskId) {
+        return getCheckCanSuccess();
+    }
+
+    public AjaxResult checkTaskCanDepartOld(Long taskId) {
+        List<Map<String, Object>> conflicts = new ArrayList<>();
+        Map<String, Object> result = new HashMap<>();
+
         // 鑾峰彇浠诲姟璇︽儏
         SysTask task = this.getTaskDetail(taskId);
         if (task == null) {
             return AjaxResult.error("浠诲姟涓嶅瓨鍦�");
         }
-        
-        List<Map<String, Object>> conflicts = new ArrayList<>();
         
         // 1. 妫�鏌ヨ溅杈嗘槸鍚︽湁鏈畬鎴愮殑浠诲姟
         List<SysTaskVehicle> taskVehicles = task.getAssignedVehicles();
@@ -1728,8 +1804,7 @@
             }
         }
         
-        // 杩斿洖缁撴灉
-        Map<String, Object> result = new HashMap<>();
+
         result.put("valid", conflicts.isEmpty());
         result.put("conflicts", conflicts);
         
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml
new file mode 100644
index 0000000..26b56d6
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysTaskStatusHistoryMapper">
+
+    <resultMap id="BaseResultMap" type="com.ruoyi.system.domain.SysTaskStatusHistory">
+        <id     property="id"              column="id"/>
+        <result property="taskId"          column="task_id"/>
+        <result property="taskCode"        column="task_code"/>
+        <result property="fromStatus"      column="from_status"/>
+        <result property="fromStatusName"  column="from_status_name"/>
+        <result property="toStatus"        column="to_status"/>
+        <result property="toStatusName"    column="to_status_name"/>
+        <result property="changeReason"    column="change_reason"/>
+        <result property="changeSource"    column="change_source"/>
+        <result property="operatorId"      column="operator_id"/>
+        <result property="operatorName"    column="operator_name"/>
+        <result property="changeTime"      column="change_time"/>
+        <result property="longitude"       column="longitude"/>
+        <result property="latitude"        column="latitude"/>
+        <result property="locationAddress" column="location_address"/>
+        <result property="ipAddress"       column="ip_address"/>
+        <result property="remark"          column="remark"/>
+    </resultMap>
+
+    <!-- 鎻掑叆鐘舵�佸彉鏇村巻鍙� -->
+    <insert id="insert" parameterType="com.ruoyi.system.domain.SysTaskStatusHistory" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO sys_task_status_history (
+            task_id, task_code,
+            from_status, from_status_name,
+            to_status, to_status_name,
+            change_reason, change_source,
+            operator_id, operator_name,
+            change_time,
+            longitude, latitude, location_address,
+            ip_address, remark
+        ) VALUES (
+            #{taskId}, #{taskCode},
+            #{fromStatus}, #{fromStatusName},
+            #{toStatus}, #{toStatusName},
+            #{changeReason}, #{changeSource},
+            #{operatorId}, #{operatorName},
+            #{changeTime},
+            #{longitude}, #{latitude}, #{locationAddress},
+            #{ipAddress}, #{remark}
+        )
+    </insert>
+
+    <!-- 鎸変换鍔D鏌ヨ鍘嗗彶锛屾椂闂村崌搴� -->
+    <select id="selectByTaskId" parameterType="Long" resultMap="BaseResultMap">
+        SELECT *
+        FROM sys_task_status_history
+        WHERE task_id = #{taskId}
+        ORDER BY change_time ASC
+    </select>
+
+    <!-- 鎸変换鍔$紪鍙锋煡璇㈠巻鍙� -->
+    <select id="selectByTaskCode" parameterType="String" resultMap="BaseResultMap">
+        SELECT *
+        FROM sys_task_status_history
+        WHERE task_code = #{taskCode}
+        ORDER BY change_time ASC
+    </select>
+
+</mapper>
diff --git a/ruoyi-ui/src/api/task.js b/ruoyi-ui/src/api/task.js
index 7990787..77b459e 100644
--- a/ruoyi-ui/src/api/task.js
+++ b/ruoyi-ui/src/api/task.js
@@ -331,3 +331,11 @@
     method: 'get'
   })
 }
+
+// 鏌ヨ浠诲姟鐘舵�佸彉鏇村巻鍙�
+export function getTaskStatusHistory(taskId) {
+  return request({
+    url: '/task/' + taskId + '/statusHistory',
+    method: 'get'
+  })
+}
diff --git a/ruoyi-ui/src/views/task/general/detail.vue b/ruoyi-ui/src/views/task/general/detail.vue
index 2c0ab51..3cf49f3 100644
--- a/ruoyi-ui/src/views/task/general/detail.vue
+++ b/ruoyi-ui/src/views/task/general/detail.vue
@@ -605,6 +605,51 @@
       </div>
     </el-card>
 
+    <!-- 鐘舵�佸彉鏇村巻鍙� -->
+    <el-card class="box-card" style="margin-top: 20px;">
+      <div slot="header" class="clearfix">
+        <span>鐘舵�佸彉鏇村巻鍙�</span>
+      </div>
+
+      <el-timeline v-if="statusHistoryList && statusHistoryList.length > 0">
+        <el-timeline-item
+          v-for="item in statusHistoryList"
+          :key="item.id"
+          :timestamp="parseTime(item.changeTime)"
+          :color="getStatusColor(item.toStatus)"
+          placement="top"
+        >
+          <el-card shadow="never" class="status-history-card">
+            <div class="status-history-header">
+              <span class="status-arrow">
+                <el-tag v-if="item.fromStatus" size="small" :type="getTagType(item.fromStatus)">{{ item.fromStatusName || item.fromStatus }}</el-tag>
+                <span v-else style="color: #C0C4CC; font-size: 13px;">鍒濆鍒涘缓</span>
+                <i class="el-icon-arrow-right" style="margin: 0 8px; color: #909399;"></i>
+                <el-tag size="small" :type="getTagType(item.toStatus)">{{ item.toStatusName || item.toStatus }}</el-tag>
+              </span>
+              <el-tag size="mini" :type="getSourceTagType(item.changeSource)" style="margin-left: 12px;">
+                {{ getSourceLabel(item.changeSource) }}
+              </el-tag>
+            </div>
+            <div style="margin-top: 8px; color: #606266; font-size: 13px;">
+              <span><i class="el-icon-user" style="margin-right: 4px;"></i>{{ item.operatorName || '--' }}</span>
+              <span v-if="item.changeReason" style="margin-left: 16px;">
+                <i class="el-icon-chat-dot-round" style="margin-right: 4px;"></i>{{ item.changeReason }}
+              </span>
+              <span v-if="item.locationAddress" style="margin-left: 16px;">
+                <i class="el-icon-location-outline" style="margin-right: 4px;"></i>{{ item.locationAddress }}
+              </span>
+            </div>
+          </el-card>
+        </el-timeline-item>
+      </el-timeline>
+
+      <div v-else style="text-align: center; padding: 40px 0; color: #909399;">
+        <i class="el-icon-time" style="font-size: 48px; display: block; margin-bottom: 12px;"></i>
+        <span>鏆傛棤鐘舵�佸彉鏇磋褰�</span>
+      </div>
+    </el-card>
+
     <!-- 鎿嶄綔鏃ュ織 -->
     <el-card class="box-card" style="margin-top: 20px;">
       <div slot="header" class="clearfix">
@@ -798,7 +843,7 @@
 </template>
 
 <script>
-import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask, getPaymentInfo, syncServiceOrder, syncDispatchOrder, syncTaskStatus, syncFromLegacySystem, checkTaskInvoice } from "@/api/task";
+import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask, getPaymentInfo, syncServiceOrder, syncDispatchOrder, syncTaskStatus, syncFromLegacySystem, checkTaskInvoice, getTaskStatusHistory } from "@/api/task";
 import { listUser } from "@/api/system/user";
 import { getToken } from "@/utils/auth";
 
@@ -850,6 +895,8 @@
       additionalFeeList: [],
       // 鏀粯淇℃伅
       paymentInfo: null,
+      // 鐘舵�佸彉鏇村巻鍙�
+      statusHistoryList: [],
       // 涓婁紶鐩稿叧
       uploadUrl: process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + (new URLSearchParams(window.location.search).get('taskId') || ''),
       uploadHeaders: {
@@ -902,6 +949,7 @@
     this.getTaskDetail();
     this.getUserList();
     this.getAdditionalFeeList();
+    this.loadStatusHistory();
     // 鍒濆鍖栦笂浼燯RL
     this.uploadUrl = process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId;
     // 妫�鏌ュ彂绁ㄧ敵璇风姸鎬�
@@ -919,6 +967,56 @@
     }
   },
   methods: {
+    /** 鍔犺浇鐘舵�佸彉鏇村巻鍙� */
+    loadStatusHistory() {
+      getTaskStatusHistory(this.$route.params.taskId).then(response => {
+        this.statusHistoryList = response.data || [];
+      }).catch(() => {
+        this.statusHistoryList = [];
+      });
+    },
+    /** 鑾峰彇鐘舵�佸搴旂殑 Tag 绫诲瀷 */
+    getTagType(status) {
+      const map = {
+        PENDING:   'info',
+        DEPARTING: 'warning',
+        IN_PROGRESS: '',
+        COMPLETED: 'success',
+        CANCELLED: 'danger'
+      };
+      return map[status] || 'info';
+    },
+    /** 鑾峰彇鐘舵�佸搴旂殑鏃堕棿杞撮鑹� */
+    getStatusColor(status) {
+      const map = {
+        PENDING:     '#909399',
+        DEPARTING:   '#E6A23C',
+        IN_PROGRESS: '#409EFF',
+        COMPLETED:   '#67C23A',
+        CANCELLED:   '#F56C6C'
+      };
+      return map[status] || '#909399';
+    },
+    /** 鑾峰彇瑙﹀彂鏉ユ簮鏍囩绫诲瀷 */
+    getSourceTagType(source) {
+      const map = {
+        APP:    'primary',
+        ADMIN:  'warning',
+        SYSTEM: 'info',
+        LEGACY: ''
+      };
+      return map[source] || 'info';
+    },
+    /** 鑾峰彇瑙﹀彂鏉ユ簮鏂囧瓧 */
+    getSourceLabel(source) {
+      const map = {
+        APP:    'APP绔�',
+        ADMIN:  '鍚庡彴',
+        SYSTEM: '绯荤粺',
+        LEGACY: '鏃х郴缁�'
+      };
+      return map[source] || source || '--';
+    },
     /** 鑾峰彇浠诲姟璇︽儏 */
     getTaskDetail() {
       getTask(this.$route.params.taskId).then(response => {
@@ -1325,4 +1423,17 @@
 .box-card {
   margin-bottom: 20px;
 }
+.status-history-card {
+  border: none;
+  background: #fafafa;
+}
+.status-history-header {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+}
+.status-arrow {
+  display: flex;
+  align-items: center;
+}
 </style>
diff --git a/sql/sys_task_status_history.sql b/sql/sys_task_status_history.sql
new file mode 100644
index 0000000..b0aa85b
--- /dev/null
+++ b/sql/sys_task_status_history.sql
@@ -0,0 +1,32 @@
+-- =============================================
+-- 浠诲姟鐘舵�佸彉鏇村巻鍙茶褰曡〃
+-- 涓撻棬璁板綍浠诲姟鐘舵�佺殑姣忎竴娆℃祦杞紝渚夸簬瀹¤涓庤拷婧�
+-- =============================================
+
+DROP TABLE IF EXISTS `sys_task_status_history`;
+CREATE TABLE `sys_task_status_history` (
+    `id`              BIGINT        NOT NULL AUTO_INCREMENT              COMMENT '涓婚敭ID',
+    `task_id`         BIGINT        NOT NULL                             COMMENT '浠诲姟ID',
+    `task_code`       VARCHAR(64)   DEFAULT NULL                        COMMENT '浠诲姟缂栧彿锛堝啑浣欙紝鏂逛究鏌ヨ锛�',
+    `from_status`     VARCHAR(32)   DEFAULT NULL                        COMMENT '鍙樻洿鍓嶇姸鎬佺爜锛圢ULL琛ㄧず鍒濆鍒涘缓锛�',
+    `from_status_name` VARCHAR(64)  DEFAULT NULL                        COMMENT '鍙樻洿鍓嶇姸鎬佸悕绉�',
+    `to_status`       VARCHAR(32)   NOT NULL                            COMMENT '鍙樻洿鍚庣姸鎬佺爜',
+    `to_status_name`  VARCHAR(64)   DEFAULT NULL                        COMMENT '鍙樻洿鍚庣姸鎬佸悕绉�',
+    `change_reason`   VARCHAR(500)  DEFAULT NULL                        COMMENT '鍙樻洿鍘熷洜/澶囨敞',
+    `change_source`   VARCHAR(32)   DEFAULT 'APP'                       COMMENT '瑙﹀彂鏉ユ簮锛欰PP-绉诲姩绔紝ADMIN-绠$悊鍚庡彴锛孲YSTEM-绯荤粺鑷姩锛孡EGACY-鏃х郴缁熷悓姝�',
+    `operator_id`     BIGINT        DEFAULT NULL                        COMMENT '鎿嶄綔浜篒D',
+    `operator_name`   VARCHAR(64)   DEFAULT NULL                        COMMENT '鎿嶄綔浜哄鍚�',
+    `change_time`     DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP  COMMENT '鍙樻洿鏃堕棿',
+    `longitude`       DOUBLE        DEFAULT NULL                        COMMENT '鎿嶄綔鏃剁殑缁忓害锛圙PS瀹氫綅锛�',
+    `latitude`        DOUBLE        DEFAULT NULL                        COMMENT '鎿嶄綔鏃剁殑绾害锛圙PS瀹氫綅锛�',
+    `location_address` VARCHAR(255) DEFAULT NULL                        COMMENT '鎿嶄綔鏃剁殑浣嶇疆鍦板潃',
+    `ip_address`      VARCHAR(128)  DEFAULT NULL                        COMMENT '鎿嶄綔IP鍦板潃',
+    `remark`          VARCHAR(500)  DEFAULT NULL                        COMMENT '澶囨敞',
+    PRIMARY KEY (`id`),
+    INDEX `idx_task_id`    (`task_id`),
+    INDEX `idx_task_code`  (`task_code`),
+    INDEX `idx_to_status`  (`to_status`),
+    INDEX `idx_change_time`(`change_time`),
+    INDEX `idx_operator_id`(`operator_id`),
+    CONSTRAINT `fk_status_history_task` FOREIGN KEY (`task_id`) REFERENCES `sys_task`(`task_id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浠诲姟鐘舵�佸彉鏇村巻鍙茶褰曡〃';

--
Gitblit v1.9.1