wlzboy
2025-12-26 4fdde57a837b47b0a04aa17a7627c21b7425eda2
feat: 优化取消时,调度单中显示原因
12个文件已修改
1个文件已添加
919 ■■■■■ 已修改文件
app/pagesTask/create-emergency.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 427 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/listener/DispatchOrdRunningListener.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskSyncUtilService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
取消原因同步到DispatchOrd功能说明.md 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue
@@ -916,7 +916,10 @@
      if (!this.validateForm()) {
        return
      }
      this.doSubmitTask();
      
    },
    checkTaskDp(){
      // èŽ·å–å½“å‰æ—¥æœŸï¼ˆæ ¼å¼YYYY-MM-DD)
      const today = new Date()
      const createDate = today.getFullYear() + '-' + 
@@ -946,7 +949,6 @@
        this.$modal.showToast('检查失败,请重试')
      })
    },
    // æ‰§è¡Œå®žé™…的提交操作
    doSubmitTask() {
      this.$modal.confirm('确定要保存任务吗?').then(() => {
app/pagesTask/detail.vue
@@ -5,6 +5,9 @@
        <uni-icons type="arrowleft" size="20"></uni-icons>
      </view>
      <view class="title">任务详情</view>
      <view class="edit-btn" @click="handleEdit" v-if="taskDetail && !isTaskFinished">
        <uni-icons type="compose" size="20" color="#007AFF"></uni-icons>
      </view>
    </view>
    
    <scroll-view class="detail-content" scroll-y="true" v-if="taskDetail">
@@ -64,6 +67,17 @@
                  {{ isAssigneeReady(assignee) ? '已就绪' : '未就绪' }}
                </view>
              </view>
            </view>
            <!-- å½“前登录人是该执行人且未就绪时显示就绪按钮 -->
            <view
              v-if="showAssigneeReadyFeature() && isAssigneeSelf(assignee) && !isAssigneeReady(assignee) && taskDetail.taskStatus === 'PENDING'"
              class="assignee-ready-btn"
              :data-user-id="assignee.userId || assignee.oaUserId"
              :data-user-name="assignee.userName"
              :data-index="index"
              @click="handleReadyClick"
            >
              ç‚¹å‡»å°±ç»ª
            </view>
          </view>
        </view>
@@ -373,6 +387,51 @@
      <text>加载中...</text>
    </view>
    
    <!-- å¼ºåˆ¶å®Œæˆå¯¹è¯æ¡† -->
    <uni-popup ref="forceCompletePopup" type="center" :is-mask-click="false">
      <view class="force-complete-dialog">
        <view class="dialog-title">请输入时间</view>
        <view class="time-picker-item">
          <view class="time-label">转运开始时间</view>
          <picker
            mode="date"
            :value="getDateFromDateTime(forceCompleteForm.actualStartTime)"
            @change="selectStartDate"
          >
            <view class="picker-value">{{ getDateFromDateTime(forceCompleteForm.actualStartTime) || '请选择日期' }}</view>
          </picker>
          <picker
            mode="time"
            :value="getTimeFromDateTime(forceCompleteForm.actualStartTime)"
            @change="selectStartTime"
          >
            <view class="picker-value">{{ getTimeFromDateTime(forceCompleteForm.actualStartTime) || '请选择时间' }}</view>
          </picker>
        </view>
        <view class="time-picker-item">
          <view class="time-label">转运结束时间</view>
          <picker
            mode="date"
            :value="getDateFromDateTime(forceCompleteForm.actualEndTime)"
            @change="selectEndDate"
          >
            <view class="picker-value">{{ getDateFromDateTime(forceCompleteForm.actualEndTime) || '请选择日期' }}</view>
          </picker>
          <picker
            mode="time"
            :value="getTimeFromDateTime(forceCompleteForm.actualEndTime)"
            @change="selectEndTime"
          >
            <view class="picker-value">{{ getTimeFromDateTime(forceCompleteForm.actualEndTime) || '请选择时间' }}</view>
          </picker>
        </view>
        <view class="dialog-buttons">
          <button class="cancel-btn" @click="closeForceCompleteDialog">取消</button>
          <button class="confirm-btn" @click="confirmForceComplete">确定</button>
        </view>
      </view>
    </uni-popup>
    <!-- å–消原因选择对话框 -->
    <uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
      <view class="cancel-dialog">
@@ -395,22 +454,9 @@
    
    <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
    <view class="action-buttons" v-if="taskDetail">
      <!-- å¾…处理状态: æ˜¾ç¤ºç¼–辑、出发、取消 -->
      <!-- å¾…处理状态: æ˜¾ç¤ºå‡ºå‘、取消、强制完成 -->
      <template v-if="taskDetail.taskStatus === 'PENDING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          ä¿®æ”¹
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button
            v-if="showAssigneeReadyFeature() && isMultipleAssignees() && !isCurrentUserReady()"
            class="action-btn primary"
            @click="handleReadyAction()"
          >
            å°±ç»ª
          </button>
          <button 
            class="action-btn primary" 
            @click="handleDepartAction()"
@@ -423,17 +469,17 @@
          >
            å–消
          </button>
          <button
            class="action-btn force-complete"
            @click="showForceCompleteTimeDialog()"
          >
            å¼ºåˆ¶å®Œæˆ
          </button>
        </template>
      </template>
      
      <!-- å‡ºå‘中状态: æ˜¾ç¤ºç¼–辑、已到达、强制结束 -->
      <!-- å‡ºå‘中状态: æ˜¾ç¤ºå·²åˆ°è¾¾ã€å¼ºåˆ¶ç»“束 -->
      <template v-else-if="taskDetail.taskStatus === 'DEPARTING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          ä¿®æ”¹
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -450,14 +496,8 @@
        </template>
      </template>
      
      <!-- å·²åˆ°è¾¾çŠ¶æ€: æ˜¾ç¤ºç¼–辑、已返程 -->
      <!-- å·²åˆ°è¾¾çŠ¶æ€: æ˜¾ç¤ºå·²è¿”程 -->
      <template v-else-if="taskDetail.taskStatus === 'ARRIVED'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          ä¿®æ”¹
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -468,14 +508,8 @@
        </template>
      </template>
      
      <!-- è¿”程中状态: æ˜¾ç¤ºç¼–辑、已完成 -->
      <!-- è¿”程中状态: æ˜¾ç¤ºå·²å®Œæˆ -->
      <template v-else-if="taskDetail.taskStatus === 'RETURNING'">
        <button
          class="action-btn edit"
          @click="handleEdit"
        >
          ä¿®æ”¹
        </button>
        <template v-if="isCurrentUserAssignee()">
          <button 
            class="action-btn primary" 
@@ -521,7 +555,12 @@
        paymentInfo: null, // æ”¯ä»˜ä¿¡æ¯
        cancelReasonList: [], // å–消原因列表
        showCancelDialog: false, // æ˜¾ç¤ºå–消原因对话框
        selectedCancelReason: '' // é€‰ä¸­çš„取消原因
        selectedCancelReason: '', // é€‰ä¸­çš„取消原因
        showForceCompleteDialog: false, // æ˜¾ç¤ºå¼ºåˆ¶å®Œæˆå¯¹è¯æ¡†
        forceCompleteForm: {
          actualStartTime: '',
          actualEndTime: ''
        }
      }
    },
    computed: {
@@ -1393,17 +1432,52 @@
        return assignee && (assignee.userId === userId || assignee.oaUserId === userId)
      },
      // æ‰§è¡Œäººç‚¹å‡»â€œå°±ç»ªâ€
      markAssigneeReady(assignee) {
        if (!assignee || !this.taskDetail) {
          this.$modal.showToast('执行人或任务信息不存在')
      // å¤„理就绪按钮点击(通过data属性获取执行人信息)
      handleReadyClick(e) {
        const dataset = e.currentTarget.dataset
        const index = dataset.index
        console.log('handleReadyClick - dataset:', dataset)
        console.log('handleReadyClick - index:', index)
        if (index === undefined || index === null) {
          this.$modal.showToast('无法获取执行人索引')
          return
        }
        const assignee = this.taskDetail.assignees[index]
        console.log('handleReadyClick - assignee:', assignee)
        if (!assignee) {
          this.$modal.showToast('执行人信息不存在')
          return
        }
        this.markAssigneeReady(assignee)
      },
      // æ‰§è¡Œäººç‚¹å‡»"就绪"
      markAssigneeReady(assignee) {
        console.log('markAssigneeReady è¢«è°ƒç”¨ï¼Œå‚æ•°:', assignee)
        console.log('taskDetail:', this.taskDetail)
        if (!this.taskDetail) {
          this.$modal.showToast('任务信息不存在')
          return
        }
        if (!assignee) {
          this.$modal.showToast('执行人信息不存在')
          return
        }
        const userId = assignee.userId || assignee.oaUserId
        console.log('执行人ID:', userId)
        if (!userId) {
          this.$modal.showToast('无法识别执行人ID')
          return
        }
        this.$modal.showLoading && this.$modal.showLoading('提交中...')
        setAssigneeReady(this.taskId).then(() => {
          this.$modal.hideLoading && this.$modal.hideLoading()
@@ -1581,7 +1655,10 @@
      
      // é€‰æ‹©å–消原因
      selectCancelReason(e) {
        this.selectedCancelReason = e.detail.value
        const index = parseInt(e.detail.value)
        if (this.cancelReasonList && this.cancelReasonList[index]) {
          this.selectedCancelReason = this.cancelReasonList[index].value
        }
      },
      
      // å¸¦å–消原因的状态更新
@@ -1596,6 +1673,167 @@
        }
        const reason = this.cancelReasonList.find(r => r.value === value)
        return reason ? reason.label : value
      },
      // æ˜¾ç¤ºå¼ºåˆ¶å®Œæˆæ—¶é—´å¯¹è¯æ¡†
      showForceCompleteTimeDialog() {
        // æ ¡éªŒä»»åŠ¡æ˜¯å¦æ»¡è¶³å¼ºåˆ¶å®Œæˆæ¡ä»¶
        const validation = this.validateForceComplete()
        if (!validation.valid) {
          this.$modal.showToast(validation.message)
          return
        }
        // é‡ç½®è¡¨å•
        const now = new Date()
        const nowDateStr = this.formatDateForPicker(now)
        const nowTimeStr = this.formatTimeForPicker(now)
        // å¼€å§‹æ—¶é—´ï¼šä¼˜å…ˆä½¿ç”¨é¢„约时间,如果没有则使用当前时间
        let startTimeStr = nowDateStr + ' ' + nowTimeStr
        if (this.taskDetail && this.taskDetail.plannedStartTime) {
          const plannedTime = new Date(this.taskDetail.plannedStartTime)
          const year = plannedTime.getFullYear()
          // æ£€æŸ¥æ˜¯å¦æ˜¯æœ‰æ•ˆæ—¶é—´ï¼ˆæŽ’除1900、1970等无效年份)
          if (year > 2000) {
            const plannedDateStr = this.formatDateForPicker(plannedTime)
            const plannedTimeStr = this.formatTimeForPicker(plannedTime)
            startTimeStr = plannedDateStr + ' ' + plannedTimeStr
          }
        }
        this.forceCompleteForm = {
          actualStartTime: startTimeStr,
          actualEndTime: nowDateStr + ' ' + nowTimeStr
        }
        this.$refs.forceCompletePopup.open()
      },
      // æ ¡éªŒæ˜¯å¦å¯ä»¥å¼ºåˆ¶å®Œæˆ
      validateForceComplete() {
        if (!this.taskDetail) {
          return { valid: false, message: '任务信息不存在' }
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰æ‰§è¡Œäºº
        if (!this.taskDetail.assignees || this.taskDetail.assignees.length === 0) {
          return { valid: false, message: '请先分配执行人' }
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰è½¦è¾†
        if (!this.taskDetail.assignedVehicles || this.taskDetail.assignedVehicles.length === 0) {
          return { valid: false, message: '请先分配执行车辆' }
        }
        return { valid: true }
      },
      // å…³é—­å¼ºåˆ¶å®Œæˆå¯¹è¯æ¡†
      closeForceCompleteDialog() {
        this.$refs.forceCompletePopup.close()
      },
      // ç¡®è®¤å¼ºåˆ¶å®Œæˆ
      confirmForceComplete() {
        // æ ¡éªŒæ—¶é—´
        if (!this.forceCompleteForm.actualStartTime || !this.forceCompleteForm.actualEndTime) {
          this.$modal.showToast('请选择开始和结束时间')
          return
        }
        const startTime = new Date(this.forceCompleteForm.actualStartTime)
        const endTime = new Date(this.forceCompleteForm.actualEndTime)
        if (startTime >= endTime) {
          this.$modal.showToast('结束时间必须大于开始时间')
          return
        }
        this.$refs.forceCompletePopup.close()
        // è°ƒç”¨API更新任务
        this.forceCompleteTask()
      },
      // å¼ºåˆ¶å®Œæˆä»»åŠ¡
      forceCompleteTask() {
        uni.showLoading({
          title: '处理中...'
        })
        const statusData = {
          taskStatus: 'COMPLETED',
          actualStartTime: this.forceCompleteForm.actualStartTime,
          actualEndTime: this.forceCompleteForm.actualEndTime,
          remark: '强制完成任务'
        }
        changeTaskStatus(this.taskId, statusData).then(response => {
          uni.hideLoading()
          this.$modal.showToast('任务已完成')
          // é‡æ–°åŠ è½½ä»»åŠ¡è¯¦æƒ…
          this.loadTaskDetail()
        }).catch(error => {
          uni.hideLoading()
          console.error('强制完成任务失败:', error)
          this.$modal.showToast('操作失败,请重试')
        })
      },
      // é€‰æ‹©å¼€å§‹æ—¥æœŸ
      selectStartDate(e) {
        const date = e.detail.value
        const time = this.getTimeFromDateTime(this.forceCompleteForm.actualStartTime) || '00:00'
        this.forceCompleteForm.actualStartTime = date + ' ' + time
      },
      // é€‰æ‹©å¼€å§‹æ—¶é—´
      selectStartTime(e) {
        const time = e.detail.value
        const date = this.getDateFromDateTime(this.forceCompleteForm.actualStartTime) || this.formatDateForPicker(new Date())
        this.forceCompleteForm.actualStartTime = date + ' ' + time
      },
      // é€‰æ‹©ç»“束日期
      selectEndDate(e) {
        const date = e.detail.value
        const time = this.getTimeFromDateTime(this.forceCompleteForm.actualEndTime) || '00:00'
        this.forceCompleteForm.actualEndTime = date + ' ' + time
      },
      // é€‰æ‹©ç»“束时间
      selectEndTime(e) {
        const time = e.detail.value
        const date = this.getDateFromDateTime(this.forceCompleteForm.actualEndTime) || this.formatDateForPicker(new Date())
        this.forceCompleteForm.actualEndTime = date + ' ' + time
      },
      // ä»Žæ—¥æœŸæ—¶é—´å­—符串中提取日期
      getDateFromDateTime(dateTimeStr) {
        if (!dateTimeStr) return ''
        return dateTimeStr.split(' ')[0] || ''
      },
      // ä»Žæ—¥æœŸæ—¶é—´å­—符串中提取时间
      getTimeFromDateTime(dateTimeStr) {
        if (!dateTimeStr) return ''
        return dateTimeStr.split(' ')[1] || ''
      },
      // æ ¼å¼åŒ–日期为 picker éœ€è¦çš„æ ¼å¼ (YYYY-MM-DD)
      formatDateForPicker(date) {
        const year = date.getFullYear()
        const month = String(date.getMonth() + 1).padStart(2, '0')
        const day = String(date.getDate()).padStart(2, '0')
        return `${year}-${month}-${day}`
      },
      // æ ¼å¼åŒ–时间为 picker éœ€è¦çš„æ ¼å¼ (HH:mm)
      formatTimeForPicker(date) {
        const hour = String(date.getHours()).padStart(2, '0')
        const minute = String(date.getMinutes()).padStart(2, '0')
        return `${hour}:${minute}`
      },
      
    }
@@ -1626,9 +1864,19 @@
      }
      
      .title {
        flex: 1;
        font-size: 36rpx;
        font-weight: bold;
        color: #333;
      }
      .edit-btn {
        width: 60rpx;
        height: 60rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
      }
    }
    
@@ -1726,6 +1974,9 @@
            }
            
            .assignee-role {
              display: flex;
              align-items: center;
              .role-tag {
                display: inline-block;
                padding: 4rpx 12rpx;
@@ -1746,15 +1997,6 @@
                }
              }
              
              .assignee-ready-btn {
                margin-left: 12rpx;
                padding: 8rpx 16rpx;
                font-size: 24rpx;
                border-radius: 6rpx;
                background-color: #34C759;
                color: #fff;
                border: none;
              }
              .ready-badge {
                display: inline-block;
                margin-left: 12rpx;
@@ -1771,6 +2013,17 @@
                }
              }
            }
          }
          .assignee-ready-btn {
            margin-left: 12rpx;
            padding: 8rpx 16rpx;
            font-size: 24rpx;
            border-radius: 6rpx;
            background-color: #34C759;
            color: #fff;
            border: none;
            flex-shrink: 0;
          }
        }
      }
@@ -1964,6 +2217,11 @@
          color: white;
        }
        
        &.force-complete {
          background-color: #5856d6;
          color: white;
        }
        &:first-child {
          margin-left: 0;
        }
@@ -2057,5 +2315,74 @@
        }
      }
    }
    // å¼ºåˆ¶å®Œæˆå¯¹è¯æ¡†æ ·å¼
    .force-complete-dialog {
      width: 600rpx;
      background-color: white;
      border-radius: 20rpx;
      padding: 40rpx;
      .dialog-title {
        font-size: 32rpx;
        font-weight: bold;
        text-align: center;
        margin-bottom: 30rpx;
        color: #333;
      }
      .time-picker-item {
        margin-bottom: 30rpx;
        .time-label {
          font-size: 28rpx;
          color: #333;
          margin-bottom: 15rpx;
          font-weight: 500;
        }
        picker {
          display: inline-block;
          width: 48%;
          &:first-of-type {
            margin-right: 4%;
          }
          .picker-value {
            padding: 20rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            font-size: 26rpx;
            color: #333;
            text-align: center;
          }
        }
      }
      .dialog-buttons {
        display: flex;
        gap: 20rpx;
        margin-top: 40rpx;
        button {
          flex: 1;
          height: 80rpx;
          border-radius: 10rpx;
          font-size: 30rpx;
          border: none;
          &.cancel-btn {
            background-color: #f0f0f0;
            color: #666;
          }
          &.confirm-btn {
            background-color: #5856d6;
            color: white;
          }
        }
      }
    }
  }
</style>
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -1,10 +1,14 @@
package com.ruoyi.web.controller.task;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.service.*;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -306,6 +310,23 @@
            sysTaskService.saveCancelInfo(taskId, request.getCancelReason());
        }
        
        // å¦‚果是强制完成,更新实际开始时间和结束时间
        if (newStatus == TaskStatus.COMPLETED && request.getActualStartTime() != null && request.getActualEndTime() != null) {
            SysTask task = new SysTask();
            task.setTaskId(taskId);
            task.setTaskStatus(newStatus.getCode());
            //将String转成Date
            task.setActualStartTime(DateUtils.parseDate(request.getActualStartTime()));
            task.setActualEndTime(DateUtils.parseDate(request.getActualEndTime()));
            task.setRemark(request.getRemark());
            task.setUpdateBy(SecurityUtils.getUsername());
            task.setUpdateTime(DateUtils.getNowDate());
            int result = sysTaskService.forceCompleteTask(task);
            return toAjax(result);
        }
        // å¦‚果包含GPS位置信息,使用带位置的方法
        if (request.getLatitude() != null && request.getLongitude() != null) {
           String address= mapService.reverseGeocoding(request.getLongitude(), request.getLatitude());
@@ -463,6 +484,12 @@
        // å–消相关字段
        private String cancelReason;  // å–消原因(关联数据字典task_cancel_reason)
        // å¼ºåˆ¶å®Œæˆç›¸å…³å­—段
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
        private String actualStartTime;  // å®žé™…开始时间
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
        private String actualEndTime;    // å®žé™…结束时间
        public String getTaskStatus() {
            return taskStatus;
        }
@@ -566,5 +593,21 @@
        public void setCancelReason(String cancelReason) {
            this.cancelReason = cancelReason;
        }
        public String getActualStartTime() {
            return actualStartTime;
        }
        public void setActualStartTime(String actualStartTime) {
            this.actualStartTime = actualStartTime;
        }
        public String getActualEndTime() {
            return actualEndTime;
        }
        public void setActualEndTime(String actualEndTime) {
            this.actualEndTime = actualEndTime;
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/listener/DispatchOrdRunningListener.java
@@ -6,7 +6,9 @@
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import com.ruoyi.system.mapper.DispatchOrdMapper;
import com.ruoyi.system.mapper.LegacyTransferSyncMapper;
import com.ruoyi.system.mapper.SysDictDataMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysUserMapper;
@@ -46,6 +48,12 @@
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private DispatchOrdMapper dispatchOrdMapper;
    @Autowired
    private SysDictDataMapper sysDictDataMapper;
    
    /**
     * ç›‘听任务状态变更事件
@@ -111,6 +119,11 @@
                log.debug("任务状态不需要同步到旧系统,任务ID: {}, çŠ¶æ€: {}",
                    event.getTaskId(), newTaskStatus.getInfo());
                return;
            }
            // å¦‚果是取消状态,同步取消原因到DispatchOrd表
            if (TaskStatus.CANCELLED.equals(newTaskStatus) && emergency.getCancelReason() != null) {
                syncCancelReasonToDispatchOrd(emergency.getLegacyDispatchOrdId(), emergency.getCancelReason());
            }
            
            // æ’入状态变更记录到DispatchOrd_Running表
@@ -192,4 +205,53 @@
            // ä¸æŠ›å‡ºå¼‚常,避免影响主流程
        }
    }
    /**
     * åŒæ­¥å–消原因到DispatchOrd表
     *
     * @param dispatchOrdId è°ƒåº¦å•ID
     * @param cancelReason å–消原因(字典value)
     */
    private void syncCancelReasonToDispatchOrd(Long dispatchOrdId, String cancelReason) {
        try {
            if (cancelReason == null || cancelReason.isEmpty()) {
                log.debug("取消原因为空,跳过同步,DispatchOrdID: {}", dispatchOrdId);
                return;
            }
            // å°†cancelReason(字典value)转换为Integer
            Integer cancelReasonId = null;
            try {
                cancelReasonId = Integer.valueOf(cancelReason);
            } catch (NumberFormatException e) {
                log.error("取消原因格式错误,无法转换为数字,cancelReason: {}, DispatchOrdID: {}", cancelReason, dispatchOrdId);
                return;
            }
            // ä»Žæ•°æ®å­—典中查询取消原因文本
            String cancelReasonText = sysDictDataMapper.selectDictLabel("task_cancel_reason", cancelReason);
            if (cancelReasonText == null || cancelReasonText.isEmpty()) {
                log.warn("未找到取消原因对应的文本,cancelReason: {}, DispatchOrdID: {}", cancelReason, dispatchOrdId);
                cancelReasonText = cancelReason; // ä½¿ç”¨åŽŸå€¼ä½œä¸ºé¢„å¤‡
            }
            log.info("开始同步取消原因到DispatchOrd,DispatchOrdID: {}, å–消原因ID: {}, å–消原因文本: {}",
                dispatchOrdId, cancelReasonId, cancelReasonText);
            // è°ƒç”¨Mapper更新DispatchOrd表
            int rows = dispatchOrdMapper.updateDispatchOrdCancelReason(dispatchOrdId, cancelReasonId, cancelReasonText);
            if (rows > 0) {
                log.info("成功同步取消原因到DispatchOrd,DispatchOrdID: {}, å–消原因: {} ({})",
                    dispatchOrdId, cancelReasonText, cancelReasonId);
            } else {
                log.warn("同步取消原因失败,未找到对应的调度单,DispatchOrdID: {}", dispatchOrdId);
            }
        } catch (Exception e) {
            log.error("同步取消原因到DispatchOrd异常,DispatchOrdID: {}, å–消原因: {}",
                dispatchOrdId, cancelReason, e);
            // ä¸æŠ›å‡ºå¼‚常,避免影响主流程
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java
@@ -85,4 +85,16 @@
     * @return å½±å“è¡Œæ•°
     */
    public int updateDispatchOrdState(@Param("dispatchOrdID") Long dispatchOrdID, @Param("dispatchOrdState") Integer dispatchOrdState);
    /**
     * æ›´æ–°è°ƒåº¦å•取消原因
     *
     * @param dispatchOrdID è°ƒåº¦å•ID
     * @param cancelReasonId å–消原因ID
     * @param cancelReasonText å–消原因文本
     * @return å½±å“è¡Œæ•°
     */
    public int updateDispatchOrdCancelReason(@Param("dispatchOrdID") Long dispatchOrdID,
                                             @Param("cancelReasonId") Integer cancelReasonId,
                                             @Param("cancelReasonText") String cancelReasonText);
ruoyi-system/src/main/java/com/ruoyi/system/service/IDispatchOrdService.java
@@ -86,4 +86,6 @@
     * @return å½±å“è¡Œæ•°
     */
    public int updateDispatchOrdState(Long dispatchOrdID, Integer dispatchOrdState);
    public void cancelDispatchOrd(Long dispatchOrdID,Integer cancelReason,String cancelCReasonTxt);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -125,6 +125,14 @@
    public int changeTaskStatus(Long taskId, TaskStatus newStatus, String remark);
    /**
     * å¼ºåˆ¶å®Œæˆä»»åŠ¡ï¼ˆæŒ‡å®šå®žé™…å¼€å§‹æ—¶é—´å’Œç»“æŸæ—¶é—´ï¼‰
     *
     * @param task ä»»åŠ¡ä¿¡æ¯ï¼ˆåŒ…å«taskId、taskStatus、actualStartTime、actualEndTime、remark)
     * @return ç»“æžœ
     */
    public int forceCompleteTask(SysTask task);
    /**
     * å˜æ›´ä»»åŠ¡çŠ¶æ€ï¼ˆå«GPS位置信息)
     * 
     * @param taskId ä»»åŠ¡ID
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DispatchOrdServiceImpl.java
@@ -122,5 +122,10 @@
        return dispatchOrdMapper.updateDispatchOrdState(dispatchOrdID, dispatchOrdState);
    }
        
    @Override
    public void cancelDispatchOrd(Long dispatchOrdID, Integer cancelReason, String cancelCReasonTxt) {
        dispatchOrdMapper.updateDispatchOrdCancelReason(dispatchOrdID, cancelReason, cancelCReasonTxt);
    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -931,6 +931,67 @@
    }
    /**
     * å¼ºåˆ¶å®Œæˆä»»åŠ¡ï¼ˆæŒ‡å®šå®žé™…å¼€å§‹æ—¶é—´å’Œç»“æŸæ—¶é—´ï¼‰
     *
     * @param task ä»»åŠ¡ä¿¡æ¯
     * @return ç»“æžœ
     */
    @Override
    public int forceCompleteTask(SysTask task) {
        if (task == null || task.getTaskId() == null) {
            throw new RuntimeException("任务信息不能为空");
        }
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(task.getTaskId());
        if (oldTask == null) {
            throw new RuntimeException("任务不存在");
        }
        // æ ¡éªŒå¼€å§‹æ—¶é—´å’Œç»“束时间
        if (task.getActualStartTime() == null || task.getActualEndTime() == null) {
            throw new RuntimeException("实际开始时间和结束时间不能为空");
        }
        if (task.getActualStartTime().after(task.getActualEndTime())) {
            throw new RuntimeException("结束时间必须大于开始时间");
        }
        // è®°å½•旧状态
        String oldStatus = oldTask.getTaskStatus();
        TaskStatus oldTaskStatus = TaskStatus.getByCode(oldStatus);
        // æ›´æ–°ä»»åŠ¡
        int result = sysTaskMapper.updateTaskStatus(task);
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(task.getTaskId(), "FORCE_COMPLETE", "强制完成任务",
                         oldStatus, task.getTaskStatus(),
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
            // å‘布任务状态变更事件
            TaskStatus newTaskStatus = TaskStatus.getByCode(task.getTaskStatus());
            eventPublisher.publishEvent(new TaskStatusChangedEvent(
                this,
                task.getTaskId(),
                oldTask.getTaskCode(),
                oldStatus,
                task.getTaskStatus(),
                oldTaskStatus != null ? oldTaskStatus.getInfo() : "未知",
                newTaskStatus != null ? newTaskStatus.getInfo() : "未知",
                null, // assigneeIds
                SecurityUtils.getUserId(),
                SecurityUtils.getUserId(),
                null, // longitude
                null, // latitude
                null  // address
            ));
        }
        return result;
    }
    /**
     * å˜æ›´ä»»åŠ¡çŠ¶æ€ï¼ˆå«GPS位置信息)
     * 
     * @param taskId ä»»åŠ¡ID
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java
@@ -129,6 +129,10 @@
                    log.info("取消转运任务: {}", emergency.getLegacyServiceOrdId());
                    cancelTask(emergency.getLegacyServiceOrdId(), emergency.getCancelReason(), emergency.getCancelBy());
                }
                if(LongUtil.isNotEmpty(emergency.getLegacyDispatchOrdId())){
                    log.info("取消调度单: {}", emergency.getLegacyDispatchOrdId());
                    cancelDispatch(emergency.getLegacyDispatchOrdId(), emergency.getCancelReason(), emergency.getCancelBy());
                }
            }
            // æŽ¨é€çŠ¶æ€åˆ°æ—§ç³»ç»Ÿ
            boolean result = updateLegacyTaskStatus(emergency.getLegacyDispatchOrdId(), targetStatusCode);
@@ -149,6 +153,10 @@
            log.error("【新推旧】推送任务状态异常,任务ID: {}", taskId, e);
            return false;
        }
    }
    private void cancelDispatch(Long legacyDispatchOrdId, String cancelReason, String cancelReasonText) {
        dispatchOrdService.cancelDispatchOrd(legacyDispatchOrdId, Integer.parseInt(cancelReason), cancelReasonText);
    }
    
    /**
@@ -223,7 +231,7 @@
    private void cancelTask(Long serviceOrderId, String cancelReason, String cancelBy){
        // å–消任务,更新SQL Server中的ServiceOrder表
        try {
            if (serviceOrderId == null || serviceOrderId <= 0) {
            if (LongUtil.isEmpty(serviceOrderId)) {
                log.warn("ServiceOrderID为空,无法取消任务");
                return;
            }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskSyncUtilService.java
@@ -228,12 +228,12 @@
        params.put("CommissionScenarioID", "0"); // ä¼å¾®ç»©æ•ˆæ–¹æ¡ˆ
        params.put("ServiceOrdOperationRemarks", "新系统同步创建"); // æ“ä½œå¤‡æ³¨
        params.put("ServiceOrdEstimatedOrderDate", ""); // é¢„计派单时间
        params.put("ServiceOrdSource", "10"); // è®¢å•来源(10=新系统)
        params.put("ServiceOrdSource", "0"); // è®¢å•来源(10=新系统)
        params.put("OrderLevel", "0"); // æŸ¥çœ‹ç­‰çº§
        params.put("ServiceOrdDepartureType", "1"); // é¢„约类型
        params.put("ConditionLevel", "0"); // ç—…重级别
        params.put("DirectionType", "0"); // è½¬è¿å޻向
        params.put("ServiceOrd_m", ""); // æ¥æºå…¥å£
        params.put("ServiceOrd_m", "1"); // æ¥æºå…¥å£
        params.put("FromHQ2_is", "0"); // å¹¿å·žæ€»éƒ¨æŽ¨é€ä»»åŠ¡æ ‡è®°
        params.put("OrderPrice_Auto", "0"); // è®¢å•自动报价参考值
ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml
@@ -107,5 +107,13 @@
        set DispatchOrdState = #{dispatchOrdState}
        where DispatchOrdID = #{dispatchOrdID}
    </update>
    <!-- æ›´æ–°è°ƒåº¦å•取消原因 -->
    <update id="updateDispatchOrdCancelReason">
        update DispatchOrd
        set DispatchOrdCancelReason = #{cancelReasonId},
            DispatchOrdCancelReasonTXT = #{cancelReasonText}
        where DispatchOrdID = #{dispatchOrdID}
    </update>
</mapper> 
È¡ÏûÔ­Òòͬ²½µ½DispatchOrd¹¦ÄÜ˵Ã÷.md
New file
@@ -0,0 +1,273 @@
# å–消原因同步到DispatchOrd功能说明
## åŠŸèƒ½æ¦‚è¿°
在转运任务取消时,自动将取消原因同步到旧系统的DispatchOrd表中,记录到`DispatchOrdCancelReason`(取消原因ID)和`DispatchOrdCancelReasonTXT`(取消原因文本)字段。
## å®žçŽ°æ–¹å¼
### 1. æ ¸å¿ƒæµç¨‹
当任务状态变更为"已取消"时:
1. ç›‘听器(`DispatchOrdRunningListener`)检测到取消状态
2. ä»Ž`sys_task_emergency`表获取取消原因(cancelReason字段,存储数据字典value)
3. å°†å–消原因value转换为Integer作为DispatchOrdCancelReason
4. ä»Žæ•°æ®å­—å…¸`task_cancel_reason`查询对应的中文标签作为DispatchOrdCancelReasonTXT
5. æ‰§è¡ŒUPDATE语句更新DispatchOrd表
### 2. æ•°æ®æ˜ å°„关系
| æ–°ç³»ç»Ÿå­—段 | æ–°ç³»ç»Ÿè¡¨ | æ—§ç³»ç»Ÿå­—段 | æ—§ç³»ç»Ÿè¡¨ | è¯´æ˜Ž |
|-----------|---------|-----------|---------|------|
| cancel_reason | sys_task_emergency | DispatchOrdCancelReason | DispatchOrd | å–消原因ID(数字) |
| å­—å…¸label | sys_dict_data | DispatchOrdCancelReasonTXT | DispatchOrd | å–消原因文本(中文) |
### 3. å–消原因字典
数据字典类型:`task_cancel_reason`
包含26个取消原因选项:
1. ä»·æ ¼ä¸æŽ¥å—
2. æ—¶é—´ç´§æ€¥
3. å‡ºè½¦é€Ÿåº¦æ…¢
4. é€‰æ‹©å…¶ä»–车
5. ç—…人没有生命体征
6. åŒ»ç”ŸæŠ¤å£«å‡ä¸è¶³
7. ç—…人情况有变
8. å…¶ä»–
9. ç¬¬ä¸‰æ–¹å–消
10. æµ‹è¯•订单
11. ä¼ æŸ“性疾病
12. å®¶å±žæŒ‚机/拒接/不接电话
13. æŠ¤å£«ä¸è¶³
14. åŒ»ç”Ÿä¸è¶³
15. è®¾å¤‡ä¸è¶³(呼吸机)
16. å®¶å±žæ²¡è”系好床位
17. ç§»äº¤åˆ†æ”¯æœºæž„执行
18. ç§»äº¤åŠžäº‹å¤„(湛江)
19. ç§»äº¤åŠžäº‹å¤„(茂名)
20. å®¶å±žä¸è‚¯é€éœ²ä¿¡æ¯
21. æ‰€åœ¨åŒ»é™¢/目的地医院派车
22. è‡ªé©¾è½¦æŽ¥é€æ‚£è€…
23. é€‰æ‹©å…¶ä»–机构车辆
24. å¤–联通知取消
25. å¤–联无反馈
26. å®¶å±žæ— åŽŸå› ç›´æŽ¥å‘ŠçŸ¥å–æ¶ˆ
## ä»£ç ä¿®æ”¹æ¸…单
### 1. DispatchOrdMapper.java
**文件路径**: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/DispatchOrdMapper.java`
**新增方法**:
```java
/**
 * æ›´æ–°è°ƒåº¦å•取消原因
 *
 * @param dispatchOrdID è°ƒåº¦å•ID
 * @param cancelReasonId å–消原因ID
 * @param cancelReasonText å–消原因文本
 * @return å½±å“è¡Œæ•°
 */
public int updateDispatchOrdCancelReason(@Param("dispatchOrdID") Long dispatchOrdID,
                                         @Param("cancelReasonId") Integer cancelReasonId,
                                         @Param("cancelReasonText") String cancelReasonText);
```
### 2. DispatchOrdMapper.xml
**文件路径**: `ruoyi-system/src/main/resources/mapper/system/DispatchOrdMapper.xml`
**新增SQL**:
```xml
<!-- æ›´æ–°è°ƒåº¦å•取消原因 -->
<update id="updateDispatchOrdCancelReason">
    update DispatchOrd
    set DispatchOrdCancelReason = #{cancelReasonId},
        DispatchOrdCancelReasonTXT = #{cancelReasonText}
    where DispatchOrdID = #{dispatchOrdID}
</update>
```
### 3. DispatchOrdRunningListener.java
**文件路径**: `ruoyi-system/src/main/java/com/ruoyi/system/listener/DispatchOrdRunningListener.java`
**新增依赖注入**:
```java
@Autowired
private DispatchOrdMapper dispatchOrdMapper;
@Autowired
private SysDictDataMapper sysDictDataMapper;
```
**新增import**:
```java
import com.ruoyi.system.mapper.DispatchOrdMapper;
import com.ruoyi.system.mapper.SysDictDataMapper;
```
**修改handleTaskStatusChangedEvent方法**:
在状态转换后增加取消原因同步逻辑(第122-126行):
```java
// å¦‚果是取消状态,同步取消原因到DispatchOrd表
if (TaskStatus.CANCELLED.equals(newTaskStatus) && emergency.getCancelReason() != null) {
    syncCancelReasonToDispatchOrd(emergency.getLegacyDispatchOrdId(), emergency.getCancelReason());
}
```
**新增方法**:
```java
/**
 * åŒæ­¥å–消原因到DispatchOrd表
 *
 * @param dispatchOrdId è°ƒåº¦å•ID
 * @param cancelReason å–消原因(字典value)
 */
private void syncCancelReasonToDispatchOrd(Long dispatchOrdId, String cancelReason) {
    try {
        if (cancelReason == null || cancelReason.isEmpty()) {
            log.debug("取消原因为空,跳过同步,DispatchOrdID: {}", dispatchOrdId);
            return;
        }
        // å°†cancelReason(字典value)转换为Integer
        Integer cancelReasonId = null;
        try {
            cancelReasonId = Integer.valueOf(cancelReason);
        } catch (NumberFormatException e) {
            log.error("取消原因格式错误,无法转换为数字,cancelReason: {}, DispatchOrdID: {}", cancelReason, dispatchOrdId);
            return;
        }
        // ä»Žæ•°æ®å­—典中查询取消原因文本
        String cancelReasonText = sysDictDataMapper.selectDictLabel("task_cancel_reason", cancelReason);
        if (cancelReasonText == null || cancelReasonText.isEmpty()) {
            log.warn("未找到取消原因对应的文本,cancelReason: {}, DispatchOrdID: {}", cancelReason, dispatchOrdId);
            cancelReasonText = cancelReason; // ä½¿ç”¨åŽŸå€¼ä½œä¸ºé¢„å¤‡
        }
        log.info("开始同步取消原因到DispatchOrd,DispatchOrdID: {}, å–消原因ID: {}, å–消原因文本: {}",
            dispatchOrdId, cancelReasonId, cancelReasonText);
        // è°ƒç”¨Mapper更新DispatchOrd表
        int rows = dispatchOrdMapper.updateDispatchOrdCancelReason(dispatchOrdId, cancelReasonId, cancelReasonText);
        if (rows > 0) {
            log.info("成功同步取消原因到DispatchOrd,DispatchOrdID: {}, å–消原因: {} ({})",
                dispatchOrdId, cancelReasonText, cancelReasonId);
        } else {
            log.warn("同步取消原因失败,未找到对应的调度单,DispatchOrdID: {}", dispatchOrdId);
        }
    } catch (Exception e) {
        log.error("同步取消原因到DispatchOrd异常,DispatchOrdID: {}, å–消原因: {}",
            dispatchOrdId, cancelReason, e);
        // ä¸æŠ›å‡ºå¼‚常,避免影响主流程
    }
}
```
## æ‰§è¡ŒSQL示例
当取消原因为"价格不接受"(value=1)时,实际执行的SQL:
```sql
UPDATE DispatchOrd
SET DispatchOrdCancelReason = 1,
    DispatchOrdCancelReasonTXT = '价格不接受'
WHERE DispatchOrdID = 12345
```
## åŒæ­¥æ¡ä»¶
取消原因同步需要满足以下条件:
1. âœ… æ—§ç³»ç»ŸåŒæ­¥å·²å¯ç”¨
2. âœ… ä»»åŠ¡ç±»åž‹ä¸ºæ€¥æ•‘è½¬è¿ä»»åŠ¡ï¼ˆEMERGENCY_TRANSFER)
3. âœ… ä»»åŠ¡å·²åŒæ­¥åˆ°æ—§ç³»ç»Ÿï¼ˆæœ‰legacyDispatchOrdId)
4. âœ… ä»»åŠ¡çŠ¶æ€å˜æ›´ä¸º"已取消"(CANCELLED)
5. âœ… ä»»åŠ¡è®°å½•äº†å–æ¶ˆåŽŸå› ï¼ˆcancelReason不为空)
## æ‰§è¡Œæ—¶æœº
**异步执行**:任务状态变更为"已取消"时,通过事件监听器异步处理,不影响主业务流程。
## æ—¥å¿—说明
### æ­£å¸¸æ—¥å¿—
```
INFO - æ”¶åˆ°ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œå‡†å¤‡åŒæ­¥åˆ°DispatchOrd_Running,任务ID:1001,旧状态:PENDING,新状态:CANCELLED
INFO - å¼€å§‹åŒæ­¥å–消原因到DispatchOrd,DispatchOrdID: 12345, å–消原因ID: 1, å–消原因文本: ä»·æ ¼ä¸æŽ¥å—
INFO - æˆåŠŸåŒæ­¥å–æ¶ˆåŽŸå› åˆ°DispatchOrd,DispatchOrdID: 12345, å–消原因: ä»·æ ¼ä¸æŽ¥å— (1)
```
### è·³è¿‡æ—¥å¿—
```
DEBUG - å–消原因为空,跳过同步,DispatchOrdID: 12345
```
### å¼‚常日志
```
WARN - æœªæ‰¾åˆ°å–消原因对应的文本,cancelReason: 99, DispatchOrdID: 12345
ERROR - å–消原因格式错误,无法转换为数字,cancelReason: abc, DispatchOrdID: 12345
WARN - åŒæ­¥å–消原因失败,未找到对应的调度单,DispatchOrdID: 12345
ERROR - åŒæ­¥å–消原因到DispatchOrd异常,DispatchOrdID: 12345, å–消原因: 1
```
## éªŒè¯æ–¹æ³•
### 1. æ•°æ®åº“验证
查询DispatchOrd表的取消原因字段:
```sql
SELECT
    DispatchOrdID,
    DispatchOrdState,
    DispatchOrdCancelReason,
    DispatchOrdCancelReasonTXT
FROM DispatchOrd
WHERE DispatchOrdID = 12345
```
### 2. æµ‹è¯•步骤
1. åœ¨æ–°ç³»ç»Ÿåˆ›å»ºè½¬è¿ä»»åŠ¡å¹¶åŒæ­¥åˆ°æ—§ç³»ç»Ÿ
2. åœ¨ä»»åŠ¡è¯¦æƒ…é¡µç‚¹å‡»"取消"按钮
3. é€‰æ‹©å–消原因(如"价格不接受")
4. ç¡®è®¤å–消
5. æ£€æŸ¥åº”用日志,确认同步成功
6. æŸ¥è¯¢DispatchOrd表,验证取消原因字段已更新
### 3. é¢„期结果
- DispatchOrdCancelReason = 1
- DispatchOrdCancelReasonTXT = "价格不接受"
## ç‰¹æ€§è¯´æ˜Ž
1. **异步处理**:不阻塞主业务流程,提高系统响应速度
2. **容错机制**:同步失败不影响任务取消操作
3. **完整日志**:记录详细的同步过程和结果
4. **数据转换**:自动将字典value转换为ID和文本
5. **事件驱动**:利用Spring事件机制实现解耦
6. **自动同步**:无需手动触发,状态变更时自动执行
## æ³¨æ„äº‹é¡¹
1. **仅限取消状态**:只有任务状态变更为CANCELLED时才会同步取消原因
2. **依赖调度单同步**:必须先同步到旧系统才能更新取消原因
3. **异常不中断**:同步异常只记录日志,不抛出异常,不影响主流程
4. **数据库字段**:确保DispatchOrd表有DispatchOrdCancelReason和DispatchOrdCancelReasonTXT字段
5. **字典依赖**:依赖数据字典task_cancel_reason正确配置
## ç›¸å…³æ–‡æ¡£
- [取消原因功能实现说明.md](./取消原因功能实现说明.md)
- [转运任务状态变更记录同步功能说明.md](./prd/转运任务状态变更记录同步功能说明.md)
- [旧系统同步-快速开始.md](./prd/旧系统同步-快速开始.md)
## æ›´æ–°æ—¥å¿—
- 2025-12-26: å®žçŽ°å–æ¶ˆåŽŸå› åŒæ­¥åˆ°DispatchOrd功能