wlzboy
2026-03-24 6676a35122fd9c97d1b1679c211bc8a9b97f08f2
feat: 增加日志记录历史消息
6个文件已修改
4个文件已添加
559 ■■■■■ 已修改文件
app/pages/login.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTask.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/task.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/detail.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/sys_task_status_history.sql 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 // 可能为null
          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;
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());
        }
    }
}
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;
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskStatusHistory.java
New file
@@ -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;
    /** 变更前状态码(NULL表示初始创建) */
    @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-管理后台,SYSTEM-系统自动,LEGACY-旧系统同步
     */
    @Excel(name = "触发来源")
    private String changeSource;
    /** 操作人ID */
    @Excel(name = "操作人ID")
    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;
    /** 操作时的经度(GPS定位) */
    private Double longitude;
    /** 操作时的纬度(GPS定位) */
    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 +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskStatusHistoryMapper.java
New file
@@ -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);
}
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());
@@ -1021,6 +1034,12 @@
            recordTaskLog(taskId, "STATUS_CHANGE", "状态变更", 
                         "状态:" + oldTaskStatus.getInfo(), 
                         "状态:" + 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    触发来源(APP / 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);
        
ruoyi-system/src/main/resources/mapper/system/SysTaskStatusHistoryMapper.xml
New file
@@ -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>
    <!-- 按任务ID查询历史,时间升序 -->
    <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>
ruoyi-ui/src/api/task.js
@@ -331,3 +331,11 @@
    method: 'get'
  })
}
// 查询任务状态变更历史
export function getTaskStatusHistory(taskId) {
  return request({
    url: '/task/' + taskId + '/statusHistory',
    method: 'get'
  })
}
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();
    // 初始化上传URL
    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>
sql/sys_task_status_history.sql
New file
@@ -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 '变更前状态码(NULL表示初始创建)',
    `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 '触发来源:APP-移动端,ADMIN-管理后台,SYSTEM-系统自动,LEGACY-旧系统同步',
    `operator_id`     BIGINT        DEFAULT NULL                        COMMENT '操作人ID',
    `operator_name`   VARCHAR(64)   DEFAULT NULL                        COMMENT '操作人姓名',
    `change_time`     DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP  COMMENT '变更时间',
    `longitude`       DOUBLE        DEFAULT NULL                        COMMENT '操作时的经度(GPS定位)',
    `latitude`        DOUBLE        DEFAULT NULL                        COMMENT '操作时的纬度(GPS定位)',
    `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='任务状态变更历史记录表';