wlzboy
2025-12-25 77b8624957ea9afafc81af72e52212c15b5f091e
feat: 优化新建 保存同步
23个文件已修改
4个文件已添加
1834 ■■■■■ 已修改文件
app/main.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/index.vue 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/create.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/index.vue 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/ServiceOrderEntity.java 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskCreateVO.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/ServiceOrderMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IServiceOrderService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ServiceOrderServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmergencyTaskServiceImpl.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysWelfareTaskServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskDispatchSyncUtilService.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskEmergencySyncServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java 101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/ServiceOrderMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/add_cancel_reason_fields.sql 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/insert_dict_task_cancel_reason.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
取消原因功能实现说明.md 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/main.js
@@ -11,6 +11,7 @@
App.mpType = 'app'
const app = new Vue({
  store,
  ...App
})
app/pages/index.vue
@@ -232,6 +232,26 @@
        </view>
      </view>
    </scroll-view>
    <!-- å–消原因选择对话框 -->
    <uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
      <view class="cancel-dialog">
        <view class="dialog-title">请选择取消原因</view>
        <picker mode="selector" :range="cancelReasonList" range-key="label" @change="selectCancelReason">
          <view class="reason-picker">
            <view class="picker-label">取消原因</view>
            <view class="picker-value">
              {{ selectedCancelReasonLabel }}
            </view>
            <uni-icons type="arrowright" size="16"></uni-icons>
          </view>
        </picker>
        <view class="dialog-buttons">
          <button class="cancel-btn" @click="closeCancelDialog">取消</button>
          <button class="confirm-btn" @click="confirmCancelTask">确定</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
@@ -241,6 +261,7 @@
import { getUserProfile } from "@/api/system/user";
import { getUserBoundVehicle } from "@/api/vehicle";
import { getUnreadCount } from "@/api/message";
import { getDicts } from "@/api/dict";
import { formatDateTime } from "@/utils/common";
import subscribeManager from "@/utils/subscribe";
import { checkTaskCanDepart } from "@/utils/taskValidator";
@@ -268,6 +289,12 @@
      currentPage: 1,
      pageSize: 10,
      hasMore: true,
      // å–消原因相关
      cancelReasonList: [], // å–消原因列表
      showCancelDialog: false, // æ˜¾ç¤ºå–消原因对话框
      selectedCancelReason: '', // é€‰ä¸­çš„取消原因
      currentCancelTask: null // å½“前要取消的任务
    };
  },
  computed: {
@@ -288,6 +315,15 @@
          "IN_PROGRESS",
        ].includes(task.taskStatus);
      });
    },
    // èŽ·å–é€‰ä¸­çš„å–æ¶ˆåŽŸå› æ ‡ç­¾ï¼ˆç”¨äºŽå¼¹çª—æ˜¾ç¤ºï¼‰
    selectedCancelReasonLabel() {
      if (!this.selectedCancelReason || !this.cancelReasonList.length) {
        return '请选择'
      }
      const reason = this.cancelReasonList.find(r => r.value === this.selectedCancelReason)
      return reason ? reason.label : '请选择'
    },
  },
  onLoad() {
@@ -310,6 +346,8 @@
    this.loadRunningTasks();
    // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
    this.loadUnreadMessageCount();
    // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
    this.loadCancelReasonDict();
  },
  onShow() {
    // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦å·²ç™»å½•
@@ -707,13 +745,9 @@
          break;
        case "cancel":
          // å–消 -> äºŒæ¬¡ç¡®è®¤åŽçŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
          this.$modal
            .confirm("确定要取消此任务吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "CANCELLED", "任务已取消");
            })
            .catch(() => {});
          // å–消 -> æ˜¾ç¤ºå–消原因选择对话框
          this.currentCancelTask = task;
          this.showCancelReasonDialog();
          break;
        case "arrive":
@@ -760,6 +794,56 @@
      this.getLocationAndUpdateStatus(taskId, status, remark);
    },
    
    // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
    loadCancelReasonDict() {
      getDicts('task_cancel_reason').then(response => {
        if (response.code === 200 && response.data) {
          this.cancelReasonList = response.data.map(item => ({
            value: item.dictValue,
            label: item.dictLabel
          }))
        }
      }).catch(error => {
        console.error('加载取消原因字典失败:', error)
      })
    },
    // æ˜¾ç¤ºå–消原因对话框
    showCancelReasonDialog() {
      this.selectedCancelReason = ''
      this.$refs.cancelPopup.open()
    },
    // ç¡®è®¤å–消任务
    confirmCancelTask() {
      if (!this.selectedCancelReason) {
        this.$modal.showToast('请选择取消原因')
        return
      }
      this.$refs.cancelPopup.close()
      // è°ƒç”¨æ›´æ–°çŠ¶æ€æ–¹æ³•ï¼Œä¼ é€’å–æ¶ˆåŽŸå› 
      this.updateTaskStatusWithCancelReason(this.currentCancelTask.taskId, 'CANCELLED', '任务已取消', this.selectedCancelReason)
    },
    // å–消对话框关闭
    closeCancelDialog() {
      this.$refs.cancelPopup.close()
      this.selectedCancelReason = ''
      this.currentCancelTask = null
    },
    // é€‰æ‹©å–消原因
    selectCancelReason(e) {
      this.selectedCancelReason = this.cancelReasonList[e.detail.value].value
    },
    // å¸¦å–消原因的状态更新
    updateTaskStatusWithCancelReason(taskId, status, remark, cancelReason) {
      this.getLocationAndUpdateStatus(taskId, status, remark, cancelReason)
    },
    // æ£€æŸ¥çŸ¥æƒ…同意书附件并更新状态
    async checkConsentAttachmentAndThen(taskId, status, remark) {
      try {
@@ -798,7 +882,7 @@
    },
    // èŽ·å–ä½ç½®ä¿¡æ¯å¹¶æ›´æ–°çŠ¶æ€
    getLocationAndUpdateStatus(taskId, status, remark) {
    getLocationAndUpdateStatus(taskId, status, remark, cancelReason) {
      const that = this;
      // ä½¿ç”¨uni.getLocation获取GPS位置
@@ -825,6 +909,11 @@
            speed: res.speed,
            heading: res.direction || res.heading,
          };
          // å¦‚果有取消原因,添加到请求数据中
          if (cancelReason) {
            statusData.cancelReason = cancelReason
          }
          changeTaskStatus(taskId, statusData)
            .then((response) => {
@@ -846,6 +935,11 @@
                taskStatus: status,
                remark: remark,
              };
              // å¦‚果有取消原因,添加到请求数据中
              if (cancelReason) {
                statusData.cancelReason = cancelReason
              }
              changeTaskStatus(taskId, statusData)
                .then((response) => {
@@ -1383,4 +1477,67 @@
    }
  }
}
// å–消原因对话框样式
.cancel-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;
  }
  .reason-picker {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx 30rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    margin-bottom: 30rpx;
    .picker-label {
      font-size: 28rpx;
      color: #666;
    }
    .picker-value {
      flex: 1;
      text-align: right;
      margin: 0 20rpx;
      font-size: 28rpx;
      color: #333;
    }
  }
  .dialog-buttons {
    display: flex;
    gap: 20rpx;
    button {
      flex: 1;
      height: 80rpx;
      line-height: 80rpx;
      border-radius: 10rpx;
      font-size: 28rpx;
      border: none;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
</style>
app/pages/task/create.vue
@@ -58,15 +58,15 @@
          page: '/pagesTask/create-normal'
        },
        {
          type: 'welfare',
          name: '福祉车',
          icon: 'car',
          color: '#F37B1D',
          description: '老年人、残疾人等特殊群体用车服务',
          taskType: 'WELFARE',
          page: '/pagesTask/create-welfare'
        }
        // {
        //   type: 'welfare',
        //   name: '福祉车',
        //   icon: 'car',
        //   color: '#F37B1D',
        //   description: '老年人、残疾人等特殊群体用车服务',
        //   taskType: 'WELFARE',
        //   page: '/pagesTask/create-welfare'
        // }
      ]
    }
  },
app/pages/task/index.vue
@@ -5,12 +5,12 @@
      <view class="task-header">
        <view class="header-title">任务列表</view>
        <view class="header-actions">
          <button class="search-toggle-btn" @click="toggleSearch">
          <!-- <button class="search-toggle-btn" @click="toggleSearch">
            <uni-icons
              :type="showSearch ? 'close' : 'search'"
              size="20"
            ></uni-icons>
          </button>
          </button> -->
          <button class="refresh-btn" @click="refreshList">
            <uni-icons type="refresh" size="20"></uni-icons>
          </button>
@@ -270,12 +270,33 @@
        </view>
      </scroll-view>
    </view>
    <!-- å–消原因选择对话框 -->
    <uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
      <view class="cancel-dialog">
        <view class="dialog-title">请选择取消原因</view>
        <picker mode="selector" :range="cancelReasonList" range-key="label" @change="selectCancelReason">
          <view class="reason-picker">
            <view class="picker-label">取消原因</view>
            <view class="picker-value">
              {{ selectedCancelReasonLabel }}
            </view>
            <uni-icons type="arrowright" size="16"></uni-icons>
          </view>
        </picker>
        <view class="dialog-buttons">
          <button class="cancel-btn" @click="closeCancelDialog">取消</button>
          <button class="confirm-btn" @click="confirmCancelTask">确定</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
<script>
import uniDatetimePicker from "@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue";
import { listTask, changeTaskStatus, checkTaskConsentAttachment } from "@/api/task";
import { getDicts } from "@/api/dict";
import { mapState } from "vuex";
import { formatDateTime } from "@/utils/common";
import { checkTaskCanDepart } from "@/utils/taskValidator";
@@ -294,8 +315,8 @@
        vehicle: "",
        taskNo: "",
      },
      statusOptions: ["全部状态", "待处理", "处理中", "已完成"],
      statusValues: ["", "pending", "processing", "completed"],
      statusOptions: ["全部状态", "待处理", "处理中", "已完成", "已取消"],
      statusValues: ["", "pending", "processing", "completed", "cancelled"],
      selectedStatus: "",
      selectedStatusText: "",
      startDate: "",
@@ -312,6 +333,12 @@
      pageSize: 10,
      total: 0,
      hasMore: true,
      // å–消原因相关
      cancelReasonList: [], // å–消原因列表
      showCancelDialog: false, // æ˜¾ç¤ºå–消原因对话框
      selectedCancelReason: '', // é€‰ä¸­çš„取消原因
      currentCancelTask: null // å½“前要取消的任务
    };
  },
  computed: {
@@ -323,12 +350,24 @@
      // å®žé™…的筛选将在请求服务器时完成
      return this.taskList;
    },
    // èŽ·å–é€‰ä¸­çš„å–æ¶ˆåŽŸå› æ ‡ç­¾ï¼ˆç”¨äºŽå¼¹çª—æ˜¾ç¤ºï¼‰
    selectedCancelReasonLabel() {
      if (!this.selectedCancelReason || !this.cancelReasonList.length) {
        return '请选择'
      }
      const reason = this.cancelReasonList.find(r => r.value === this.selectedCancelReason)
      return reason ? reason.label : '请选择'
    },
  },
  onLoad() {
    this.loadTaskList();
    // ç›‘听任务列表刷新事件
    uni.$on("refreshTaskList", this.handleRefreshEvent);
    // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
    this.loadCancelReasonDict();
  },
  onShow() {
    // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°åˆ—表(从其他页面返回时)
@@ -382,7 +421,8 @@
          "IN_PROGRESS",
        ].join(",");
      } else if (this.currentFilter === "completed") {
        queryParams.taskStatus = "COMPLETED";
        // å·²å®ŒæˆåŒ…含:已完成和已取消
        queryParams.taskStatusList = "COMPLETED,CANCELLED";
      } else {
        // å¦‚果没有使用顶部tab筛选,则使用查询条件中的状态筛选
        if (this.selectedStatus) {
@@ -395,11 +435,13 @@
              "IN_PROGRESS",
            ].join(","),
            completed: "COMPLETED",
            cancelled: "CANCELLED",
          };
          if (statusMap[this.selectedStatus]) {
            if (
              this.selectedStatus === "pending" ||
              this.selectedStatus === "completed"
              this.selectedStatus === "completed" ||
              this.selectedStatus === "cancelled"
            ) {
              queryParams.taskStatus = statusMap[this.selectedStatus];
            } else {
@@ -497,7 +539,8 @@
          "IN_PROGRESS",
        ].join(",");
      } else if (this.currentFilter === "completed") {
        queryParams.taskStatus = "COMPLETED";
        // å·²å®ŒæˆåŒ…含:已完成和已取消
        queryParams.taskStatusList = "COMPLETED,CANCELLED";
      } else {
        // å¦‚果没有使用顶部tab筛选,则使用查询条件中的状态筛选
        if (this.selectedStatus) {
@@ -510,11 +553,13 @@
              "IN_PROGRESS",
            ].join(","),
            completed: "COMPLETED",
            cancelled: "CANCELLED",
          };
          if (statusMap[this.selectedStatus]) {
            if (
              this.selectedStatus === "pending" ||
              this.selectedStatus === "completed"
              this.selectedStatus === "completed" ||
              this.selectedStatus === "cancelled"
            ) {
              queryParams.taskStatus = statusMap[this.selectedStatus];
            } else {
@@ -654,6 +699,8 @@
      this.endDate = "";
      this.searchForm.vehicle = "";
      this.searchForm.taskNo = "";
      // é‡ç½®åŽé‡æ–°åŠ è½½æ•°æ®
      this.loadTaskList();
    },
    // åˆ·æ–°åˆ—表
@@ -671,6 +718,9 @@
    // ç­›é€‰
    changeFilter(filter) {
      this.currentFilter = filter;
      // é‡ç½®åˆ†é¡µ
      this.currentPage = 1;
      this.hasMore = true;
      // é‡æ–°åŠ è½½æ•°æ®
      this.loadTaskList();
    },
@@ -784,13 +834,9 @@
          break;
        case "cancel":
          // å–消 -> äºŒæ¬¡ç¡®è®¤åŽçŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
          this.$modal
            .confirm("确定要取消此任务吗?")
            .then(() => {
              this.updateTaskStatus(task.taskId, "CANCELLED", "任务已取消");
            })
            .catch(() => {});
          // å–消 -> æ˜¾ç¤ºå–消原因选择对话框
          this.currentCancelTask = task;
          this.showCancelReasonDialog();
          break;
        case "arrive":
@@ -837,6 +883,56 @@
      this.getLocationAndUpdateStatus(taskId, status, remark);
    },
    
    // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
    loadCancelReasonDict() {
      getDicts('task_cancel_reason').then(response => {
        if (response.code === 200 && response.data) {
          this.cancelReasonList = response.data.map(item => ({
            value: item.dictValue,
            label: item.dictLabel
          }))
        }
      }).catch(error => {
        console.error('加载取消原因字典失败:', error)
      })
    },
    // æ˜¾ç¤ºå–消原因对话框
    showCancelReasonDialog() {
      this.selectedCancelReason = ''
      this.$refs.cancelPopup.open()
    },
    // ç¡®è®¤å–消任务
    confirmCancelTask() {
      if (!this.selectedCancelReason) {
        this.$modal.showToast('请选择取消原因')
        return
      }
      this.$refs.cancelPopup.close()
      // è°ƒç”¨æ›´æ–°çŠ¶æ€æ–¹æ³•ï¼Œä¼ é€’å–æ¶ˆåŽŸå› 
      this.updateTaskStatusWithCancelReason(this.currentCancelTask.taskId, 'CANCELLED', '任务已取消', this.selectedCancelReason)
    },
    // å–消对话框关闭
    closeCancelDialog() {
      this.$refs.cancelPopup.close()
      this.selectedCancelReason = ''
      this.currentCancelTask = null
    },
    // é€‰æ‹©å–消原因
    selectCancelReason(e) {
      this.selectedCancelReason = this.cancelReasonList[e.detail.value].value
    },
    // å¸¦å–消原因的状态更新
    updateTaskStatusWithCancelReason(taskId, status, remark, cancelReason) {
      this.getLocationAndUpdateStatus(taskId, status, remark, cancelReason)
    },
    // æ£€æŸ¥çŸ¥æƒ…同意书附件并更新状态
    async checkConsentAttachmentAndThen(taskId, status, remark) {
      try {
@@ -877,7 +973,7 @@
    },
    // èŽ·å–ä½ç½®ä¿¡æ¯å¹¶æ›´æ–°çŠ¶æ€
    getLocationAndUpdateStatus(taskId, status, remark) {
    getLocationAndUpdateStatus(taskId, status, remark, cancelReason) {
      const that = this;
      // ä½¿ç”¨uni.getLocation获取GPS位置
@@ -904,6 +1000,11 @@
            speed: res.speed,
            heading: res.direction || res.heading,
          };
          // å¦‚果有取消原因,添加到请求数据中
          if (cancelReason) {
            statusData.cancelReason = cancelReason
          }
          changeTaskStatus(taskId, statusData)
            .then((response) => {
@@ -926,6 +1027,11 @@
                taskStatus: status,
                remark: remark,
              };
              // å¦‚果有取消原因,添加到请求数据中
              if (cancelReason) {
                statusData.cancelReason = cancelReason
              }
              changeTaskStatus(taskId, statusData)
                .then((response) => {
@@ -1410,4 +1516,67 @@
    vertical-align: middle;
  }
}
// å–消原因对话框样式
.cancel-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;
  }
  .reason-picker {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx 30rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    margin-bottom: 30rpx;
    .picker-label {
      font-size: 28rpx;
      color: #666;
    }
    .picker-value {
      flex: 1;
      text-align: right;
      margin: 0 20rpx;
      font-size: 28rpx;
      color: #333;
    }
  }
  .dialog-buttons {
    display: flex;
    gap: 20rpx;
    button {
      flex: 1;
      height: 80rpx;
      line-height: 80rpx;
      border-radius: 10rpx;
      font-size: 28rpx;
      border: none;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
</style>
app/pagesTask/detail.vue
@@ -249,6 +249,23 @@
        </view>
      </view>
      
      <!-- å–消信息(仅在任务已取消且有取消原因时显示) -->
      <view class="detail-section" v-if="taskDetail.taskStatus === 'CANCELLED' && taskDetail.emergencyInfo && taskDetail.emergencyInfo.cancelReason">
        <view class="section-title">取消信息</view>
        <view class="info-item">
          <view class="label">取消原因</view>
          <view class="value">{{ getCancelReasonLabel(taskDetail.emergencyInfo.cancelReason) }}</view>
        </view>
        <view class="info-item" v-if="taskDetail.emergencyInfo.cancelBy">
          <view class="label">取消人</view>
          <view class="value">{{ taskDetail.emergencyInfo.cancelBy }}</view>
        </view>
        <view class="info-item" v-if="taskDetail.emergencyInfo.cancelTime">
          <view class="label">取消时间</view>
          <view class="value">{{ formatTime(taskDetail.emergencyInfo.cancelTime) }}</view>
        </view>
      </view>
      <!-- æ”¯ä»˜è®°å½•明细 -->
      <view class="detail-section" v-if="paymentInfo && paymentInfo.paidPayments && paymentInfo.paidPayments.length > 0">
        <view class="section-title">支付记录</view>
@@ -355,6 +372,26 @@
      <uni-icons type="spinner-cycle" size="40" color="#007AFF"></uni-icons>
      <text>加载中...</text>
    </view>
    <!-- å–消原因选择对话框 -->
    <uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
      <view class="cancel-dialog">
        <view class="dialog-title">请选择取消原因</view>
        <picker mode="selector" :range="cancelReasonList" range-key="label" @change="selectCancelReason">
          <view class="reason-picker">
            <view class="picker-label">取消原因</view>
            <view class="picker-value">
              {{ selectedCancelReasonLabel }}
            </view>
            <uni-icons type="arrowright" size="16"></uni-icons>
          </view>
        </picker>
        <view class="dialog-buttons">
          <button class="cancel-btn" @click="closeCancelDialog">取消</button>
          <button class="confirm-btn" @click="confirmCancelTask">确定</button>
        </view>
      </view>
    </uni-popup>
    
    <!-- æ“ä½œæŒ‰é’®åŒºåŸŸ -->
    <view class="action-buttons" v-if="taskDetail">
@@ -467,6 +504,7 @@
  import { getTask, changeTaskStatus, setAssigneeReady, checkTaskConsentAttachment } from '@/api/task'
  import { checkVehicleActiveTasks } from '@/api/task'
  import { getPaymentInfo } from '@/api/payment'
  import { getDicts } from '@/api/dict'
  import { formatDateTime } from '@/utils/common'
  import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator'
  import AttachmentUpload from './components/AttachmentUpload.vue'
@@ -480,7 +518,10 @@
      return {
        taskDetail: null,
        taskId: null,
        paymentInfo: null // æ”¯ä»˜ä¿¡æ¯
        paymentInfo: null, // æ”¯ä»˜ä¿¡æ¯
        cancelReasonList: [], // å–消原因列表
        showCancelDialog: false, // æ˜¾ç¤ºå–消原因对话框
        selectedCancelReason: '' // é€‰ä¸­çš„取消原因
      }
    },
    computed: {
@@ -503,7 +544,17 @@
          }
          return [baseClass, roleClasses[userType] || '']
        }
      },      // æ˜¾ç¤ºä»»åŠ¡ç±»åž‹
      },
      // èŽ·å–é€‰ä¸­çš„å–æ¶ˆåŽŸå› æ ‡ç­¾ï¼ˆç”¨äºŽå¼¹çª—æ˜¾ç¤ºï¼‰
      selectedCancelReasonLabel() {
        if (!this.selectedCancelReason || !this.cancelReasonList.length) {
          return '请选择'
        }
        const reason = this.cancelReasonList.find(r => r.value === this.selectedCancelReason)
        return reason ? reason.label : '请选择'
      },
      // æ˜¾ç¤ºä»»åŠ¡ç±»åž‹
      displayTaskType() {
        if (!this.taskDetail || !this.taskDetail.taskType) {
          return '未设置'
@@ -581,6 +632,7 @@
    onLoad(options) {
      this.taskId = options.id
      this.loadTaskDetail()
      this.loadCancelReasonDict() // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
    },
    onShow() {
      // æ¯æ¬¡é¡µé¢æ˜¾ç¤ºæ—¶é‡æ–°åŠ è½½æ•°æ®ï¼Œç¡®ä¿ä»Žç¼–è¾‘é¡µé¢è¿”å›žåŽèƒ½çœ‹åˆ°æœ€æ–°æ•°æ®
@@ -810,10 +862,8 @@
            break;
            
          case 'cancel':
            // å–消 -> äºŒæ¬¡ç¡®è®¤åŽçŠ¶æ€å˜ä¸ºå·²å–æ¶ˆ
            this.$modal.confirm('确定要取消此任务吗?').then(() => {
              this.updateTaskStatus('CANCELLED', '任务已取消')
            }).catch(() => {});
            // å–消 -> æ˜¾ç¤ºå–消原因选择对话框
            this.showCancelReasonDialog();
            break;
            
          case 'arrive':
@@ -995,7 +1045,7 @@
      },
      
      // èŽ·å–ä½ç½®ä¿¡æ¯å¹¶æ›´æ–°çŠ¶æ€
      getLocationAndUpdateStatus(status, remark) {
      getLocationAndUpdateStatus(status, remark, cancelReason) {
        const that = this
        
        // ä½¿ç”¨uni.getLocation获取GPS位置
@@ -1022,6 +1072,11 @@
              heading: res.direction || res.heading
            }
            
            // å¦‚果有取消原因,添加到请求数据中
            if (cancelReason) {
              statusData.cancelReason = cancelReason
            }
            changeTaskStatus(that.taskId, statusData).then(response => {
              that.$modal.showToast('状态更新成功')
              // é‡æ–°åŠ è½½ä»»åŠ¡è¯¦æƒ…
@@ -1039,6 +1094,11 @@
              const statusData = {
                taskStatus: status,
                remark: remark
              }
              // å¦‚果有取消原因,添加到请求数据中
              if (cancelReason) {
                statusData.cancelReason = cancelReason
              }
              
              changeTaskStatus(that.taskId, statusData).then(response => {
@@ -1470,6 +1530,65 @@
        return 'payment-' + (key !== null && key !== undefined ? key : index);
      },
      
      // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
      loadCancelReasonDict() {
        // ä»ŽåŽç«¯èŽ·å–å–æ¶ˆåŽŸå› å­—å…¸
        getDicts('task_cancel_reason').then(response => {
          if (response.code === 200 && response.data) {
            this.cancelReasonList = response.data.map(item => ({
              value: item.dictValue,
              label: item.dictLabel
            }))
          }
        }).catch(error => {
          console.error('加载取消原因字典失败:', error)
        })
      },
      // æ˜¾ç¤ºå–消原因对话框
      showCancelReasonDialog() {
        this.selectedCancelReason = ''
        this.$refs.cancelPopup.open()
      },
      // ç¡®è®¤å–消任务
      confirmCancelTask() {
        if (!this.selectedCancelReason) {
          this.$modal.showToast('请选择取消原因')
          return
        }
        this.$refs.cancelPopup.close()
        // è°ƒç”¨æ›´æ–°çŠ¶æ€æ–¹æ³•ï¼Œä¼ é€’å–æ¶ˆåŽŸå› 
        this.updateTaskStatusWithCancelReason('CANCELLED', '任务已取消', this.selectedCancelReason)
      },
      // å–消对话框关闭
      closeCancelDialog() {
        this.$refs.cancelPopup.close()
        this.selectedCancelReason = ''
      },
      // é€‰æ‹©å–消原因
      selectCancelReason(e) {
        this.selectedCancelReason = e.detail.value
      },
      // å¸¦å–消原因的状态更新
      updateTaskStatusWithCancelReason(status, remark, cancelReason) {
        this.getLocationAndUpdateStatus(status, remark, cancelReason)
      },
      // æ ¹æ®å–消原因value获取label
      getCancelReasonLabel(value) {
        if (!value || !this.cancelReasonList.length) {
          return value || '未知'
        }
        const reason = this.cancelReasonList.find(r => r.value === value)
        return reason ? reason.label : value
      },
    }
  }
</script>
@@ -1867,5 +1986,67 @@
      margin-left: 10rpx;
      vertical-align: middle;
    }
    // å–消原因对话框样式
    .cancel-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;
      }
      .reason-picker {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 20rpx 30rpx;
        background-color: #f5f5f5;
        border-radius: 10rpx;
        margin-bottom: 30rpx;
        .picker-label {
          font-size: 28rpx;
          color: #666;
        }
        .picker-value {
          flex: 1;
          text-align: right;
          font-size: 28rpx;
          color: #333;
          margin: 0 10rpx;
        }
      }
      .dialog-buttons {
        display: flex;
        gap: 20rpx;
        button {
          flex: 1;
          height: 80rpx;
          border-radius: 10rpx;
          font-size: 30rpx;
          border: none;
          &.cancel-btn {
            background-color: #f0f0f0;
            color: #666;
          }
          &.confirm-btn {
            background-color: #007AFF;
            color: white;
          }
        }
      }
    }
  }
</style>
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -277,6 +277,11 @@
            return error("无效的任务状态");
        }
        
        // å¦‚果是取消状态,保存取消原因
        if (newStatus == TaskStatus.CANCELLED && StringUtils.isNotEmpty(request.getCancelReason())) {
            sysTaskService.saveCancelInfo(taskId, request.getCancelReason());
        }
        // å¦‚果包含GPS位置信息,使用带位置的方法
        if (request.getLatitude() != null && request.getLongitude() != null) {
           String address= mapService.reverseGeocoding(request.getLongitude(), request.getLatitude());
@@ -430,6 +435,9 @@
        private Double altitude;
        private Double speed;
        private Double heading;
        // å–消相关字段
        private String cancelReason;  // å–消原因(关联数据字典task_cancel_reason)
        public String getTaskStatus() {
            return taskStatus;
@@ -526,5 +534,13 @@
        public void setHeading(Double heading) {
            this.heading = heading;
        }
        public String getCancelReason() {
            return cancelReason;
        }
        public void setCancelReason(String cancelReason) {
            this.cancelReason = cancelReason;
        }
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java
@@ -69,6 +69,21 @@
    }
    /**
     * èŽ·å–ç”¨æˆ·æ˜µç§°
     **/
    public static String getNickName()
    {
        try
        {
            return getLoginUser().getUser().getNickName();
        }
        catch (Exception e)
        {
            throw new ServiceException("获取用户昵称异常", HttpStatus.UNAUTHORIZED);
        }
    }
    /**
     * èŽ·å–ç”¨æˆ·
     **/
    public static LoginUser getLoginUser()
ruoyi-system/src/main/java/com/ruoyi/system/domain/ServiceOrderEntity.java
New file
@@ -0,0 +1,319 @@
package com.ruoyi.system.domain;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.ibatis.type.Alias;
import java.util.Date;
/**
 * ServiceOrder对象 service_order
 *
 * @author ruoyi
 * @date 2025-12-24
 */
@Alias("ServiceOrderEntity")
@DataSource(DataSourceType.SQLSERVER)
public class ServiceOrderEntity extends BaseEntity
{
    private static final long serialVersionUID = 1L;
    /** ServiceOrdID */
    private Long serviceOrdId;
    /** ServiceOrdPtServices */
    private String serviceOrdPtServices;
    /** ServiceOrdPtInServices */
    private String serviceOrdPtInServices;
    /** ServiceOrdClass */
    private String serviceOrdClass;
    /** ServiceOrdAreaType */
    private Long serviceOrdAreaType;
    /** ServiceOrdType */
    private Long serviceOrdType;
    /** ServiceOrdApptDate */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date serviceOrdApptDate;
    /** ServiceOrdCoTies */
    private String serviceOrdCoTies;
    /** ServiceOrdPtSex */
    private String serviceOrdPtSex;
    /** ServiceOrdPtNat */
    private String serviceOrdPtNat;
    /** ServiceOrdPtIDCard */
    private String serviceOrdPtIdcard;
    /**
     * æŠ¥ä»·é‡‘额
     */
    private Long serviceOrdTraTxnPrice;
    /** ServiceOrdPtOutHospID_New (对应ServiceOrdPtOutHospID字段) */
    private Long serviceOrdPtOutHospIdNew;
    /** ServiceOrdPtOutHospID */
    private String serviceOrdPtOutHospId;
    /** ServiceOrdPtInHospID_New (对应ServiceOrdPtInHospID字段) */
    private Long serviceOrdPtInHospIdNew;
    /** ServiceOrdPtInHospID */
    private String serviceOrdPtInHospId;
    /** ServiceOrdPtCondition */
    private String serviceOrdPtCondition;
    /** ServiceOrdTaskRemarks */
    private String serviceOrdTaskRemarks;
    /** ServiceOrdTraStreet */
    private String serviceOrdTraStreet;
    /** ServiceOrdTraStreetCoo */
    private String serviceOrdTraStreetCoo;
    /** ServiceOrdTraEnd */
    private String serviceOrdTraEnd;
    /** ServiceOrdTraEndCoo */
    private String serviceOrdTraEndCoo;
    /** ServiceOrd_CC_ID */
    private Long serviceOrdCcId;
    public Long getServiceOrdId()
    {
        return serviceOrdId;
    }
    public void setServiceOrdId(Long serviceOrdId)
    {
        this.serviceOrdId = serviceOrdId;
    }
    public String getServiceOrdPtServices()
    {
        return serviceOrdPtServices;
    }
    public void setServiceOrdPtServices(String serviceOrdPtServices)
    {
        this.serviceOrdPtServices = serviceOrdPtServices;
    }
    public String getServiceOrdPtInServices()
    {
        return serviceOrdPtInServices;
    }
    public void setServiceOrdPtInServices(String serviceOrdPtInServices)
    {
        this.serviceOrdPtInServices = serviceOrdPtInServices;
    }
    public Long getServiceOrdTraTxnPrice() {
        return serviceOrdTraTxnPrice;
    }
    public void setServiceOrdTraTxnPrice(Long serviceOrdTraTxnPrice) {
        this.serviceOrdTraTxnPrice = serviceOrdTraTxnPrice;
    }
    public String getServiceOrdClass()
    {
        return serviceOrdClass;
    }
    public void setServiceOrdClass(String serviceOrdClass)
    {
        this.serviceOrdClass = serviceOrdClass;
    }
    public Long getServiceOrdAreaType()
    {
        return serviceOrdAreaType;
    }
    public void setServiceOrdAreaType(Long serviceOrdAreaType)
    {
        this.serviceOrdAreaType = serviceOrdAreaType;
    }
    public Long getServiceOrdType()
    {
        return serviceOrdType;
    }
    public void setServiceOrdType(Long serviceOrdType)
    {
        this.serviceOrdType = serviceOrdType;
    }
    public Date getServiceOrdApptDate()
    {
        return serviceOrdApptDate;
    }
    public void setServiceOrdApptDate(Date serviceOrdApptDate)
    {
        this.serviceOrdApptDate = serviceOrdApptDate;
    }
    public String getServiceOrdCoTies()
    {
        return serviceOrdCoTies;
    }
    public void setServiceOrdCoTies(String serviceOrdCoTies)
    {
        this.serviceOrdCoTies = serviceOrdCoTies;
    }
    public String getServiceOrdPtSex()
    {
        return serviceOrdPtSex;
    }
    public void setServiceOrdPtSex(String serviceOrdPtSex)
    {
        this.serviceOrdPtSex = serviceOrdPtSex;
    }
    public String getServiceOrdPtNat()
    {
        return serviceOrdPtNat;
    }
    public void setServiceOrdPtNat(String serviceOrdPtNat)
    {
        this.serviceOrdPtNat = serviceOrdPtNat;
    }
    public String getServiceOrdPtIdcard()
    {
        return serviceOrdPtIdcard;
    }
    public void setServiceOrdPtIdcard(String serviceOrdPtIdcard)
    {
        this.serviceOrdPtIdcard = serviceOrdPtIdcard;
    }
    public Long getServiceOrdPtOutHospIdNew()
    {
        return serviceOrdPtOutHospIdNew;
    }
    public void setServiceOrdPtOutHospIdNew(Long serviceOrdPtOutHospIdNew)
    {
        this.serviceOrdPtOutHospIdNew = serviceOrdPtOutHospIdNew;
    }
    public String getServiceOrdPtOutHospId()
    {
        return serviceOrdPtOutHospId;
    }
    public void setServiceOrdPtOutHospId(String serviceOrdPtOutHospId)
    {
        this.serviceOrdPtOutHospId = serviceOrdPtOutHospId;
    }
    public Long getServiceOrdPtInHospIdNew()
    {
        return serviceOrdPtInHospIdNew;
    }
    public void setServiceOrdPtInHospIdNew(Long serviceOrdPtInHospIdNew)
    {
        this.serviceOrdPtInHospIdNew = serviceOrdPtInHospIdNew;
    }
    public String getServiceOrdPtInHospId()
    {
        return serviceOrdPtInHospId;
    }
    public void setServiceOrdPtInHospId(String serviceOrdPtInHospId)
    {
        this.serviceOrdPtInHospId = serviceOrdPtInHospId;
    }
    public String getServiceOrdPtCondition()
    {
        return serviceOrdPtCondition;
    }
    public void setServiceOrdPtCondition(String serviceOrdPtCondition)
    {
        this.serviceOrdPtCondition = serviceOrdPtCondition;
    }
    public String getServiceOrdTaskRemarks()
    {
        return serviceOrdTaskRemarks;
    }
    public void setServiceOrdTaskRemarks(String serviceOrdTaskRemarks)
    {
        this.serviceOrdTaskRemarks = serviceOrdTaskRemarks;
    }
    public String getServiceOrdTraStreet()
    {
        return serviceOrdTraStreet;
    }
    public void setServiceOrdTraStreet(String serviceOrdTraStreet)
    {
        this.serviceOrdTraStreet = serviceOrdTraStreet;
    }
    public String getServiceOrdTraStreetCoo()
    {
        return serviceOrdTraStreetCoo;
    }
    public void setServiceOrdTraStreetCoo(String serviceOrdTraStreetCoo)
    {
        this.serviceOrdTraStreetCoo = serviceOrdTraStreetCoo;
    }
    public String getServiceOrdTraEnd()
    {
        return serviceOrdTraEnd;
    }
    public void setServiceOrdTraEnd(String serviceOrdTraEnd)
    {
        this.serviceOrdTraEnd = serviceOrdTraEnd;
    }
    public String getServiceOrdTraEndCoo()
    {
        return serviceOrdTraEndCoo;
    }
    public void setServiceOrdTraEndCoo(String serviceOrdTraEndCoo)
    {
        this.serviceOrdTraEndCoo = serviceOrdTraEndCoo;
    }
    public Long getServiceOrdCcId()
    {
        return serviceOrdCcId;
    }
    public void setServiceOrdCcId(Long serviceOrdCcId)
    {
        this.serviceOrdCcId = serviceOrdCcId;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
@@ -156,6 +156,15 @@
    
    /** æ˜¯å¦å¹¿å·žæ€»éƒ¨æŽ¨é€ï¼ˆ0否 1是) */
    private String fromHq2Is;
    /** å–消原因(关联数据字典task_cancel_reason) */
    private String cancelReason;
    /** å–消人 */
    private String cancelBy;
    /** å–消时间 */
    private java.util.Date cancelTime;
    public String getServiceCode(){
        if(this.legacyServiceOrdClass!=null && this.legacyServiceNsTime!=null && this.legacyServiceOrdNo!=null) {
@@ -562,6 +571,30 @@
        this.fromHq2Is = fromHq2Is;
    }
    public String getCancelReason() {
        return cancelReason;
    }
    public void setCancelReason(String cancelReason) {
        this.cancelReason = cancelReason;
    }
    public String getCancelBy() {
        return cancelBy;
    }
    public void setCancelBy(String cancelBy) {
        this.cancelBy = cancelBy;
    }
    public java.util.Date getCancelTime() {
        return cancelTime;
    }
    public void setCancelTime(java.util.Date cancelTime) {
        this.cancelTime = cancelTime;
    }
    @Override
    public String toString() {
        return "SysTaskEmergency{" +
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskCreateVO.java
@@ -62,9 +62,6 @@
    /** ç›®çš„地纬度 */
    private BigDecimal destinationLatitude;
    /** é¢„计公里数 */
    private BigDecimal estimatedDistance;
    /** è½¬è¿æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date transferTime;
@@ -96,8 +93,8 @@
    /** ç»“束地址 */
    private String endAddress;
    /** ç¦ç¥‰è½¦å…¬é‡Œæ•° */
    private BigDecimal distance;
    /** å…¬é‡Œæ•° */
    private BigDecimal transferDistance;
    /** å•据类型ID(对应SQL Server的dictionary表vID) */
    private String documentTypeId;
@@ -460,14 +457,6 @@
        this.destinationLatitude = destinationLatitude;
    }
    public BigDecimal getEstimatedDistance() {
        return estimatedDistance;
    }
    public void setEstimatedDistance(BigDecimal estimatedDistance) {
        this.estimatedDistance = estimatedDistance;
    }
    public Date getTransferTime() {
        return transferTime;
    }
@@ -549,13 +538,8 @@
        this.vehicleIds = vehicleIds;
    }
    public BigDecimal getDistance() {
        return distance;
    }
    public void setDistance(BigDecimal distance) {
        this.distance = distance;
    }
    public String getDocumentTypeId() {
        return documentTypeId;
ruoyi-system/src/main/java/com/ruoyi/system/mapper/ServiceOrderMapper.java
@@ -1,6 +1,8 @@
package com.ruoyi.system.mapper;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.ServiceOrder;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.enums.DataSourceType;
@@ -70,4 +72,32 @@
     * @return ç»“æžœ
     */
    public int deleteServiceOrderByIds(Long[] serviceOrdIds);
    /**
     * æ›´æ–°æœåŠ¡è®¢å•å–æ¶ˆä¿¡æ¯
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @param cancelReason å–消原因ID
     * @param cancelReasonText å–消原因文本
     * @return å½±å“è¡Œæ•°
     */
    public int updateServiceOrderCancelInfo(@Param("serviceOrdId") Long serviceOrdId,
                                           @Param("cancelReason") Integer cancelReason,
                                           @Param("cancelReasonText") String cancelReasonText);
    /**
     * æ ¹æ®æœåŠ¡è®¢å•ID查询订单状态
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @return è®¢å•状态码
     */
    public Integer selectServiceOrderStateById(Long serviceOrdId);
    /**
     * æ ¹æ®æœåŠ¡è®¢å•ID查询取消信息
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @return å–消信息
     */
    public Map<String, Object> selectServiceCancelInfoById(Long serviceOrdId);
ruoyi-system/src/main/java/com/ruoyi/system/service/IServiceOrderService.java
@@ -1,6 +1,9 @@
package com.ruoyi.system.service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.ruoyi.system.domain.ServiceOrder;
/**
@@ -62,4 +65,24 @@
     * @return ç»“æžœ
     */
    public int deleteServiceOrderById(Long serviceOrdId);
    /**
     * æ›´æ–°æœåŠ¡è®¢å•å–æ¶ˆä¿¡æ¯
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @param cancelReason å–消原因ID
     * @param cancelReasonText å–消原因文本
     * @return å½±å“è¡Œæ•°
     */
    public int updateServiceOrderCancelInfo(Long serviceOrdId, Integer cancelReason, String cancelReasonText);
    /**
     * æ ¹æ®æœåŠ¡è®¢å•ID查询订单状态
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @return è®¢å•状态码
     */
    public Integer selectServiceOrderStateById(Long serviceOrdId);
    public Map<String, Object> selectCancelInfoById(Long serviceOrdId);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -136,6 +136,23 @@
    public int changeTaskStatusWithLocation(Long taskId, TaskStatus newStatus, String remark, SysTaskLog locationLog);
    /**
     * ä¿å­˜ä»»åŠ¡å–æ¶ˆä¿¡æ¯ï¼ˆä»…é™è½¬è¿ä»»åŠ¡ï¼‰
     *
     * @param taskId ä»»åŠ¡ID
     * @param cancelReason å–消原因(数据字典task_cancel_reason的value)
     * @return ç»“æžœ
     */
    public int saveCancelInfo(Long taskId, String cancelReason);
    /**
     * ä¿å­˜ä»»åŠ¡å–æ¶ˆä¿¡æ¯ï¼ˆå«å–æ¶ˆäººï¼‰
     * @param taskId
     * @param cancelReason
     * @param cancelBy
     * @return
     */
    public int saveCancel(Long taskId, String cancelReason,String cancelBy,Date cancelTime);
    /**
     * ä¸Šä¼ ä»»åС附件
     * 
     * @param taskId ä»»åŠ¡ID
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -508,7 +508,7 @@
            String ServiceOrdTraDistance=MapValueUtils.getStringValue(order, "ServiceOrdTraDistance");
            if(ServiceOrdTraDistance!=null){
                ServiceOrdTraDistance=ServiceOrdTraDistance.replaceAll("[^0-9]", "");
                createTaskVo.setDistance(new BigDecimal(ServiceOrdTraDistance));
                createTaskVo.setTransferDistance(new BigDecimal(ServiceOrdTraDistance));
            }
            
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ServiceOrderServiceImpl.java
@@ -1,6 +1,8 @@
package com.ruoyi.system.service.impl;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.enums.DataSourceType;
@@ -95,4 +97,33 @@
    public int deleteServiceOrderById(Long serviceOrdId) {
        return serviceOrderMapper.deleteServiceOrderById(serviceOrdId);
    }
    /**
     * æ›´æ–°æœåŠ¡è®¢å•å–æ¶ˆä¿¡æ¯
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @param cancelReason å–消原因ID
     * @param cancelReasonText å–消原因文本
     * @return å½±å“è¡Œæ•°
     */
    @Override
    public int updateServiceOrderCancelInfo(Long serviceOrdId, Integer cancelReason, String cancelReasonText) {
        return serviceOrderMapper.updateServiceOrderCancelInfo(serviceOrdId, cancelReason, cancelReasonText);
    }
    /**
     * æ ¹æ®æœåŠ¡è®¢å•ID查询订单状态
     *
     * @param serviceOrdId æœåŠ¡è®¢å•ID
     * @return è®¢å•状态码
     */
    @Override
    public Integer selectServiceOrderStateById(Long serviceOrdId) {
        return serviceOrderMapper.selectServiceOrderStateById(serviceOrdId);
    }
    @Override
    public Map<String, Object> selectCancelInfoById(Long serviceOrdId) {
        return serviceOrderMapper.selectServiceCancelInfoById(serviceOrdId);
    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmergencyTaskServiceImpl.java
@@ -113,7 +113,7 @@
            }
        }
        emergencyInfo.setTransferDistance(createVO.getDistance());
        emergencyInfo.setTransferDistance(createVO.getTransferDistance());
        emergencyInfo.setTransferPrice(createVO.getPrice());
        emergencyInfo.setDocumentTypeId(createVO.getDocumentTypeId());
        emergencyInfo.setTaskTypeId(createVO.getTaskTypeId());
@@ -269,8 +269,8 @@
            }
        }
        if (updateVO.getDistance() != null) {
            oldEmergency.setTransferDistance(updateVO.getDistance());
        if (updateVO.getTransferDistance() != null) {
            oldEmergency.setTransferDistance(updateVO.getTransferDistance());
        }
        if (updateVO.getPrice() != null) {
            oldEmergency.setTransferPrice(updateVO.getPrice());
@@ -434,8 +434,8 @@
        }
        
        // æ›´æ–°è´¹ç”¨ä¿¡æ¯
        if (createVO.getDistance() != null && BigDecimalUtil.izBigZero(createVO.getDistance())) {
            existingInfo.setTransferDistance(createVO.getDistance());
        if (createVO.getTransferDistance() != null && BigDecimalUtil.izBigZero(createVO.getTransferDistance())) {
            existingInfo.setTransferDistance(createVO.getTransferDistance());
        }
        if (createVO.getPrice() != null) {
            existingInfo.setTransferPrice(createVO.getPrice());
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -252,24 +252,29 @@
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int insertSysTask(TaskCreateVO createVO) {
    // èŽ·å–å½“å‰ç”¨æˆ·åå’Œç”¨æˆ·ID
        String username = SecurityUtils.getUsername();
        Long userId = SecurityUtils.getUserId();
    // æ ¡éªŒç”¨æˆ·ID是否为空或为0
        if(userId==null || userId==0){
            log.error("insertSysTask ç”¨æˆ·ID为空 userName:{}",username);
            return 0;
        }
        SysTask task = new SysTask();
    // åˆ›å»ºæ–°çš„任务对象
        task.setTaskCode(generateTaskCode());
        task.setTaskType(createVO.getTaskType());
        task.setTaskStatus(TaskStatus.PENDING.getCode());
        task.setTaskDescription(createVO.getTaskDescription());
        task.setPlannedStartTime(createVO.getPlannedStartTime());
        task.setPlannedEndTime(createVO.getPlannedEndTime());
        task.setAssigneeId(createVO.getAssigneeId());
    // è®¾ç½®ä»»åŠ¡åŸºæœ¬ä¿¡æ¯
        task.setTaskType(createVO.getTaskType()); // ç”Ÿæˆä»»åŠ¡ç¼–ç 
        task.setTaskStatus(TaskStatus.PENDING.getCode()); // è®¾ç½®ä»»åŠ¡ç±»åž‹
        task.setTaskDescription(createVO.getTaskDescription()); // è®¾ç½®ä»»åŠ¡çŠ¶æ€ä¸ºå¾…å¤„ç†
        task.setPlannedStartTime(createVO.getPlannedStartTime()); // è®¾ç½®ä»»åŠ¡æè¿°
        task.setPlannedEndTime(createVO.getPlannedEndTime()); // è®¾ç½®è®¡åˆ’开始时间
        task.setAssigneeId(createVO.getAssigneeId()); // è®¾ç½®è®¡åˆ’结束时间
 // è®¾ç½®æŒ‡æ´¾äººID
        task.setCreatorId(userId);
    // è®¾ç½®åˆ›å»ºäººä¿¡æ¯
        // ä¼˜å…ˆä½¿ç”¨å‰ç«¯ä¼ å…¥çš„部门ID,如果没有则使用当前用户的部门ID
        task.setDeptId(createVO.getDeptId() != null ? createVO.getDeptId() : SecurityUtils.getDeptId());
        task.setCreateBy(username);
@@ -388,8 +393,8 @@
        if (createVO.getTransferTime() != null) {
            task.setPlannedStartTime(createVO.getTransferTime());
        }
        if (createVO.getDistance() != null) {
            task.setEstimatedDistance(createVO.getDistance());
        if (createVO.getTransferDistance() != null) {
            task.setEstimatedDistance(createVO.getTransferDistance());
        }
        if (createVO.getPlannedStartTime() != null) {
            task.setPlannedStartTime(createVO.getPlannedStartTime());
@@ -400,9 +405,7 @@
        if (createVO.getEndAddress() != null) {
            task.setDestinationAddress(createVO.getEndAddress());
        }
        if (createVO.getDistance() != null) {
            task.setEstimatedDistance(createVO.getDistance());
        }
        // è‡ªåŠ¨å¡«å……ç¼ºå¤±çš„GPS坐标
        autoFillMissingGpsCoordinates(task);
        
@@ -503,11 +506,8 @@
        
        // è®¾ç½®é¢„计距离
        if (updateVO.getEstimatedDistance() != null) {
            task.setEstimatedDistance(updateVO.getEstimatedDistance());
        } else if (updateVO.getDistance() != null) {
            // å…¼å®¹æ€¥æ•‘转运字段
            task.setEstimatedDistance(updateVO.getDistance());
        if (updateVO.getTransferDistance() != null) {
            task.setEstimatedDistance(updateVO.getTransferDistance());
        }
        
        // å¦‚果更新了部门ID
@@ -812,7 +812,7 @@
            taskEmergency.setUpdateTime(DateUtils.getNowDate());
            Boolean hasEmergencyInfo = updateVO.getHospitalOut() != null || updateVO.getHospitalIn() != null || updateVO.getPatient() != null
                    || updateVO.getPrice() != null || updateVO.getDistance() != null;
                    || updateVO.getPrice() != null || updateVO.getTransferDistance() != null;
//            Boolean isHeadPush=isTaskHeaderPush(task.getCreatorId(),task.getDeptId());
//            updateVO.setFromHQ2_is(isHeadPush?"1":"0");
@@ -1013,6 +1013,71 @@
        }
        
        return result;
    }
    /**
     * ä¿å­˜ä»»åŠ¡å–æ¶ˆä¿¡æ¯ï¼ˆä»…é™è½¬è¿ä»»åŠ¡ï¼‰
     *
     * @param taskId ä»»åŠ¡ID
     * @param cancelReason å–消原因(数据字典task_cancel_reason的value)
     * @return ç»“æžœ
     */
    @Override
    public int saveCancelInfo(Long taskId, String cancelReason) {
        // èŽ·å–ä»»åŠ¡ä¿¡æ¯
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        // åªæœ‰è½¬è¿ä»»åŠ¡æ‰ä¿å­˜å–æ¶ˆä¿¡æ¯
        if (!"EMERGENCY_TRANSFER".equals(task.getTaskType())) {
            return 0;
        }
        // èŽ·å–è½¬è¿ä»»åŠ¡æ‰©å±•ä¿¡æ¯
        SysTaskEmergency emergency = sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId);
        if (emergency == null) {
            return 0;
        }
        // è®¾ç½®å–消信息
        emergency.setCancelReason(cancelReason);
        emergency.setCancelBy(SecurityUtils.getNickName());
        emergency.setCancelTime(DateUtils.getNowDate());
        emergency.setUpdateBy(SecurityUtils.getUsername());
        emergency.setUpdateTime(DateUtils.getNowDate());
        // æ›´æ–°æ•°æ®åº“
        return sysTaskEmergencyMapper.updateSysTaskEmergency(emergency);
    }
    @Override
    public int saveCancel(Long taskId, String cancelReason, String cancelBy,Date cancelTime) {
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        // åªæœ‰è½¬è¿ä»»åŠ¡æ‰ä¿å­˜å–æ¶ˆä¿¡æ¯
        if (!"EMERGENCY_TRANSFER".equals(task.getTaskType())) {
            return 0;
        }
        // èŽ·å–è½¬è¿ä»»åŠ¡æ‰©å±•ä¿¡æ¯
        SysTaskEmergency emergency = sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId);
        if (emergency == null) {
            return 0;
        }
        // è®¾ç½®å–消信息
        emergency.setCancelReason(cancelReason);
        emergency.setCancelBy(cancelBy);
        emergency.setCancelTime(cancelTime);
        emergency.setUpdateTime(DateUtils.getNowDate());
        // æ›´æ–°æ•°æ®åº“
        return sysTaskEmergencyMapper.updateSysTaskEmergency(emergency);
    }
    /**
@@ -1420,8 +1485,8 @@
        if (createVO.getDestinationLatitude() != null) {
            task.setDestinationLatitude(createVO.getDestinationLatitude());
        }
        if (createVO.getEstimatedDistance() != null) {
            task.setEstimatedDistance(createVO.getEstimatedDistance());
        if (createVO.getTransferDistance() != null) {
            task.setEstimatedDistance(createVO.getTransferDistance());
        }
    }
@@ -1436,8 +1501,8 @@
        if (createVO.getTransferTime() != null) {
            task.setPlannedStartTime(createVO.getTransferTime());
        }
        if (createVO.getDistance() != null) {
            task.setEstimatedDistance(createVO.getDistance());
        if (createVO.getTransferDistance() != null) {
            task.setEstimatedDistance(createVO.getTransferDistance());
        }
        
        // è®¾ç½®ç¦ç¥‰è½¦ç‰¹å®šä¿¡æ¯
@@ -1450,9 +1515,7 @@
        if (createVO.getEndAddress() != null) {
            task.setDestinationAddress(createVO.getEndAddress());
        }
        if (createVO.getDistance() != null) {
            task.setEstimatedDistance(createVO.getDistance());
        }
    }
    /**
@@ -1654,5 +1717,7 @@
    public AjaxResult cancelAssigneeReady(Long taskId, Long userId) {
        return sysTaskAssigneeService.cancelAssigneeReady(taskId, userId);
    }
   
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysWelfareTaskServiceImpl.java
@@ -44,7 +44,7 @@
        
        // è®¾ç½®è·ç¦»å’Œè´¹ç”¨
        // ä¼˜å…ˆä½¿ç”¨ç¦ç¥‰è½¦ä¸“用的distance字段,如果没有则使用通用的estimatedDistance
        BigDecimal serviceDistance = createVO.getDistance() != null ? createVO.getDistance() : createVO.getEstimatedDistance();
        BigDecimal serviceDistance = createVO.getTransferDistance() != null ? createVO.getTransferDistance() : BigDecimal.ZERO;
        welfareInfo.setServiceDistance(serviceDistance);
        welfareInfo.setServicePrice(createVO.getPrice());
        
@@ -102,10 +102,8 @@
        }
        
        // æ›´æ–°è·ç¦»å’Œè´¹ç”¨
        if (createVO.getDistance() != null) {
            existingInfo.setServiceDistance(createVO.getDistance());
        } else if (createVO.getEstimatedDistance() != null) {
            existingInfo.setServiceDistance(createVO.getEstimatedDistance());
        if (createVO.getTransferDistance() != null) {
            existingInfo.setServiceDistance(createVO.getTransferDistance());
        }
        if (createVO.getPrice() != null) {
            existingInfo.setServicePrice(createVO.getPrice());
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskDispatchSyncUtilService.java
@@ -42,7 +42,7 @@
        // èŽ·å–ç®¡ç†å‘˜ID(创建人ID对应的OA_UserID)
        String adminID = taskSyncUtilService.getAdminID(task);
        String serviceOrdClass = "BF";
        // èŽ·å–è°ƒåº¦å•ç±»åž‹ï¼ˆä»Žä»»åŠ¡æ‰€å±žéƒ¨é—¨çš„è°ƒåº¦å•ç¼–ç èŽ·å–ï¼‰
        String dispatchOrdClass = "SA"; // é»˜è®¤å€¼
        if (task.getDeptId() != null) {
@@ -50,6 +50,7 @@
                SysDept dept = sysDeptMapper.selectDeptById(task.getDeptId());
                if (dept != null && StringUtils.isNotEmpty(dept.getDispatchOrderClass())) {
                    dispatchOrdClass = dept.getDispatchOrderClass();
                    serviceOrdClass = dept.getServiceOrderClass();
//                    log.info("获取任务所属部门的调度单编码成功,部门ID: {}, è°ƒåº¦å•编码: {}", task.getDeptId(), dispatchOrdClass);
                }
            } catch (Exception e) {
@@ -57,7 +58,23 @@
            }
        }
        params.put("AdminID", adminID);
        params.put("ServiceOrdClass", serviceOrdClass);
        String serviceOrdAreaType = "1"; // é»˜è®¤å€¼
        if (StringUtils.isNotEmpty(emergency.getDocumentTypeId())) {
            serviceOrdAreaType = emergency.getDocumentTypeId();
//            log.info("获取单据类型ID成功,任务ID: {}, å•据类型ID: {}", task.getTaskId(), serviceOrdAreaType);
        } else {
            log.warn("任务未配置单据类型ID,任务ID: {},使用默认值", task.getTaskId());
        }
        params.put("ServiceOrdAreaType", serviceOrdAreaType); // ä»Žä»»åŠ¡çš„document_type_id获取
        String serviceOrdType = "1"; // é»˜è®¤å€¼
        if (StringUtils.isNotEmpty(emergency.getTaskTypeId())) {
            serviceOrdType = emergency.getTaskTypeId();
//            log.info("获取任务类型ID成功,任务ID: {}, ä»»åŠ¡ç±»åž‹ID: {}", taskId, serviceOrdType);
        } else {
            log.warn("任务未配置任务类型ID,任务ID: {},使用默认值", task.getTaskId());
        }
        params.put("ServiceOrdType", serviceOrdType); // æœåŠ¡å•æ‰§è¡Œç±»åž‹ï¼ˆä»Žä»»åŠ¡çš„task_type_id获取)
        // åŸºæœ¬ä¿¡æ¯
        params.put("DispatchOrdClass", dispatchOrdClass);
        params.put("ServiceOrdID", emergency.getLegacyServiceOrdId().toString());
@@ -104,7 +121,8 @@
                transferPrice = "0";
            }
        }
        params.put("DispatchOrdPerfomance", transferPrice);
        //成交价
        params.put("ServiceOrdTraTxnPrice", transferPrice);
        params.put("StretcherMoney", "0"); // æŠ¬æ‹…æž¶è´¹
        params.put("AddMoneyType", ""); // é™„加项目
        params.put("AddMoney", "0"); // é™„加项目费用
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskEmergencySyncServiceImpl.java
@@ -86,7 +86,7 @@
            // è§£æžå“åº”
            Long serviceOrdId = taskSyncUtilService.parseResponse(response);
            if (LongUtil.isNotEmpty(serviceOrderId)) {
            if (LongUtil.isNotEmpty(serviceOrdId)) {
                // åŒæ­¥æˆåŠŸï¼Œæ›´æ–°è®°å½•
                emergency.setLegacyServiceOrdId(serviceOrdId);
                emergency.setSyncStatus(2); // åŒæ­¥æˆåŠŸ
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusPushServiceImpl.java
@@ -1,11 +1,14 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.config.LegacySystemConfig;
import com.ruoyi.common.utils.LongUtil;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.service.IDispatchOrdService;
import com.ruoyi.system.service.IServiceOrderService;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysTaskEmergencyService;
import com.ruoyi.system.service.ITaskStatusPushService;
import com.ruoyi.system.utils.TaskStatusPushConverter;
@@ -40,6 +43,12 @@
    
    @Autowired
    private IDispatchOrdService dispatchOrdService;
    @Autowired
    private IServiceOrderService serviceOrderService;
    @Autowired
    private ISysDictDataService sysDictDataService;
    
    /**
     * å°†å•个任务状态推送到旧系统
@@ -115,7 +124,12 @@
//                    currentLegacyStatus, TaskStatusPushConverter.getLegacyStatusDescription(currentLegacyStatus));
                return true; // è¿”回true,因为这不算失败,只是不需要推送
            }
            if(newTaskStatus.equals(TaskStatus.CANCELLED)){
                if(LongUtil.isNotEmpty(emergency.getLegacyServiceOrdId()) ) {
                    log.info("取消转运任务: {}", emergency.getLegacyServiceOrdId());
                    cancelTask(emergency.getLegacyServiceOrdId(), emergency.getCancelReason(), emergency.getCancelBy());
                }
            }
            // æŽ¨é€çŠ¶æ€åˆ°æ—§ç³»ç»Ÿ
            boolean result = updateLegacyTaskStatus(emergency.getLegacyDispatchOrdId(), targetStatusCode);
            
@@ -204,7 +218,52 @@
            return 0;
        }
    }
    private void cancelTask(Long serviceOrderId, String cancelReason, String cancelBy){
        // å–消任务,更新SQL Server中的ServiceOrder表
        try {
            if (serviceOrderId == null || serviceOrderId <= 0) {
                log.warn("ServiceOrderID为空,无法取消任务");
                return;
            }
            // è§£æžå–消原因ID
            Integer cancelReasonId = null;
            String cancelReasonText = "";
            if (cancelReason != null && !cancelReason.isEmpty()) {
                try {
                    cancelReasonId = Integer.parseInt(cancelReason);
                    // ä»Žæ•°æ®å­—典查询取消原因文本
                    String dictLabel = sysDictDataService.selectDictLabel("task_cancel_reason", cancelReason);
                    if (dictLabel != null && !dictLabel.isEmpty()) {
                        cancelReasonText = "[取消操作:" + (cancelBy != null ? cancelBy : "系统") + "] " + dictLabel;
                    } else {
                        cancelReasonText = "[取消操作:" + (cancelBy != null ? cancelBy : "系统") + "]";
                    }
                } catch (NumberFormatException e) {
                    log.warn("取消原因ID格式错误: {}", cancelReason);
                    cancelReasonText = "[取消操作:" + (cancelBy != null ? cancelBy : "系统") + "] " + cancelReason;
                }
            } else {
                cancelReasonText = "[取消操作:" + (cancelBy != null ? cancelBy : "系统") + "]";
            }
            // è°ƒç”¨Service更新ServiceOrder表
            int rows = serviceOrderService.updateServiceOrderCancelInfo(serviceOrderId, cancelReasonId, cancelReasonText);
            if (rows > 0) {
                log.info("成功更新旧系统ServiceOrder取消信息,ServiceOrdID: {}, å–消原因ID: {}, å–消原因文本: {}",
                    serviceOrderId, cancelReasonId, cancelReasonText);
            } else {
                log.warn("更新旧系统ServiceOrder取消信息失败,未找到对应订单,ServiceOrdID: {}", serviceOrderId);
            }
        } catch (Exception e) {
            log.error("更新旧系统ServiceOrder取消信息异常,ServiceOrdID: {}", serviceOrderId, e);
        }
    }
    /**
     * æ›´æ–°æ—§ç³»ç»Ÿè°ƒåº¦å•状态(直接操作SQL Server数据库)
     * 
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskStatusSyncServiceImpl.java
@@ -1,14 +1,14 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.config.LegacySystemConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.LongUtil;
import com.ruoyi.system.domain.DispatchOrd;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.service.IDispatchOrdService;
import com.ruoyi.system.service.ISysTaskEmergencyService;
import com.ruoyi.system.service.ITaskStatusSyncService;
import com.ruoyi.system.service.*;
import com.ruoyi.system.utils.TaskStatusConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +35,9 @@
    
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private ISysTaskService taskService;
    
    @Autowired
    private ISysTaskEmergencyService sysTaskEmergencyService;
@@ -97,7 +100,9 @@
            return false;
        }
    }
    @Autowired
    private IServiceOrderService serviceOrderService;
    /**
     * æ‰¹é‡åŒæ­¥å·²åŒæ­¥è°ƒåº¦å•的任务状态(从旧系统到新系统)
     * ä½¿ç”¨åˆ†é¡µæŸ¥è¯¢ï¼Œæ‰¹é‡æŸ¥è¯¢SQL Server数据库,减少网络请求次数
@@ -128,69 +133,58 @@
//                log.info("开始同步状态第 {} é¡µï¼Œä»»åŠ¡æ•°é‡: {}", (offset / pageSize) + 1, syncedTasks.size());
                
                // 2. æå–调度单ID列表
                List<Long> dispatchOrdIDs = new ArrayList<>();
                Map<Long, SysTaskEmergency> dispatchIdToTaskMap = new HashMap<>();
                for (SysTaskEmergency emergency : syncedTasks) {
                    Long dispatchOrdId = emergency.getLegacyDispatchOrdId();
                    if (dispatchOrdId != null && dispatchOrdId > 0) {
                        dispatchOrdIDs.add(dispatchOrdId);
                        dispatchIdToTaskMap.put(dispatchOrdId, emergency);
                    if (LongUtil.isNotEmpty(emergency.getTaskId())) {
                        if(!dispatchIdToTaskMap.containsKey(dispatchOrdId)) {
                            dispatchIdToTaskMap.put(emergency.getTaskId(), emergency);
                        }
                    }
                    Long serviceOrdId = emergency.getLegacyServiceOrdId();
                    if(LongUtil.isNotEmpty(serviceOrdId)){
                        if(!dispatchIdToTaskMap.containsKey(emergency.getTaskId())) {
                            dispatchIdToTaskMap.put(emergency.getTaskId(), emergency);
                        }
                    }
                }
                
                if (dispatchOrdIDs.isEmpty()) {
                if (dispatchIdToTaskMap.size()<=0) {
//                    log.warn("本页没有有效的调度单ID");
                    offset += pageSize;
                    continue;
                }
                
                // 3. æ‰¹é‡æŸ¥è¯¢æ—§ç³»ç»Ÿè°ƒåº¦å•状态(直接查询SQL Server数据库)
                List<DispatchOrd> dispatchOrds = dispatchOrdService.selectDispatchOrdStatesByIDs(dispatchOrdIDs);
                if (dispatchOrds == null || dispatchOrds.isEmpty()) {
                    log.warn("未查询到旧系统调度单状态");
                    offset += pageSize;
                    continue;
                }
                // 4. æž„建调度单ID到状态的映射
                Map<Long, Integer> dispatchIdToStateMap = new HashMap<>();
                for (DispatchOrd dispatchOrd : dispatchOrds) {
                    try {
                        Long dispatchOrdId = Long.parseLong(dispatchOrd.getDispatchOrdID());
                        Integer dispatchOrdState = Integer.parseInt(dispatchOrd.getDispatchOrdState());
                        if (dispatchOrdState != null) {
                            dispatchIdToStateMap.put(dispatchOrdId, dispatchOrdState);
                dispatchIdToTaskMap.forEach((taskId, emergency) -> {
                    Long dispatchOrdId = emergency.getLegacyDispatchOrdId();
                    Long serviceOrdId = emergency.getLegacyServiceOrdId();
                    if(LongUtil.isNotEmpty(dispatchOrdId)){
                       Integer state =dispatchOrdService.selectDispatchOrdStateByID(dispatchOrdId);
                        syncTaskStatusWithLegacyState(taskId, state);
                    }
                    else if(LongUtil.isNotEmpty(serviceOrdId)){
                        try {
                            Integer serviceOrdState = serviceOrderService.selectServiceOrderStateById(serviceOrdId);
                            if (serviceOrdState == 4) {
                                //取消
                                Map<String, Object> cancelInfo = serviceOrderService.selectCancelInfoById(serviceOrdId);
                                String cancelReasonText = (String) cancelInfo.get("ServiceOrdCancelReasonTXT");
                                String cancelReason = (String) cancelInfo.get("ServiceOrdCancelReason");
                                taskService.saveCancel(taskId, cancelReason, cancelReasonText, DateUtils.getNowDate());
                            }
                        }catch (Exception ex){
                            log.error("查询旧系统状态失败,任务ID: {}, ServiceOrdID: {}", taskId, serviceOrdId);
                        }
                    } catch (NumberFormatException e) {
                        log.error("解析调度单ID失败: {}", dispatchOrd.getDispatchOrdID(), e);
//                        syncTaskStatusWithLegacyState(taskId, state);
                    }
                }
                // 5. éåŽ†ä»»åŠ¡ï¼ŒåŒæ­¥çŠ¶æ€
                int pageSuccessCount = 0;
                for (Map.Entry<Long, SysTaskEmergency> entry : dispatchIdToTaskMap.entrySet()) {
                    Long dispatchOrdId = entry.getKey();
                    SysTaskEmergency emergency = entry.getValue();
                    // èŽ·å–æ—§ç³»ç»ŸçŠ¶æ€
                    Integer legacyStatus = dispatchIdToStateMap.get(dispatchOrdId);
                    if (legacyStatus == null) {
//                        log.warn("未找到调度单状态,DispatchOrdID: {}", dispatchOrdId);
                        continue;
                    }
                    // åŒæ­¥å•个任务状态
                    boolean result = syncTaskStatusWithLegacyState(emergency.getTaskId(), legacyStatus);
                    if (result) {
                        pageSuccessCount++;
                    }
                }
                totalSuccessCount += pageSuccessCount;
//                log.info("状态第 {} é¡µåŒæ­¥å®Œæˆï¼Œæ€»æ•°: {}, æˆåŠŸ: {}",
//                    (offset / pageSize) + 1, syncedTasks.size(), pageSuccessCount);
                });
//
                // å¦‚果本页数据少于每页大小,说明已经是最后一页
                if (syncedTasks.size() < pageSize) {
@@ -279,6 +273,7 @@
                default:
                    break;
            }
            
            sysTaskMapper.updateSysTask(task);
            
ruoyi-system/src/main/resources/mapper/system/ServiceOrderMapper.xml
@@ -33,7 +33,11 @@
        <result property="serviceOrdCCTime"    column="ServiceOrd_CC_Time"    />
        <result property="serviceOrdVisit"    column="ServiceOrdVisit"    />
    </resultMap>
    <resultMap type="java.util.HashMap" id="ServiceCancelResult">
        <result property="ServiceOrdState" column="ServiceOrdState" />
        <result property="ServiceOrdCancelReason" column="ServiceOrdCancelReason" />
        <result property="ServiceOrdCancelReasonTXT" column="ServiceOrdCancelReasonTXT" />
    </resultMap>
    <sql id="selectServiceOrderVo">
        select ServiceOrdID, ServiceOrdUserID, ServiceOrdClass, ServiceOrdType, ServiceOrdState, ServiceOrdStartDate, ServiceOrdApptDate, ServiceOrdCoName, ServiceOrdCoPhone, ServiceOrdPtName, ServiceOrdPtAge, ServiceOrdPtSex, ServiceOrdPtKG, ServiceOrdPtNat, ServiceOrdPtIDCard, ServiceOrdTraProvince, ServiceOrdTraCity, ServiceOrdTraStreet, ServiceOrdTraEnd, ServiceOrdTraDistance, ServiceOrdTraUnitPrice, ServiceOrdTraTxnPrice, ServiceOrdTraPaidType, ServiceOrdTraPaidPrice, ServiceOrdUnitRemarks, ServiceOrd_CC_Time, ServiceOrdVisit from ServiceOrder
    </sql>
@@ -174,4 +178,25 @@
            #{serviceOrdId}
        </foreach>
    </delete>
    <!-- æ›´æ–°æœåŠ¡è®¢å•å–æ¶ˆä¿¡æ¯ -->
    <update id="updateServiceOrderCancelInfo">
        update ServiceOrder
        set ServiceOrdState = 4,
            ServiceOrdCancelReason = #{cancelReason},
            ServiceOrdCancelReasonTXT = #{cancelReasonText}
        where ServiceOrdID = #{serviceOrdId}
    </update>
    <!-- æ ¹æ®æœåŠ¡è®¢å•ID查询订单状态 -->
    <select id="selectServiceOrderStateById" parameterType="Long" resultType="Integer">
        select ServiceOrdState
        from ServiceOrder
        where ServiceOrdID = #{serviceOrdId}
    </select>
    <select id="selectServiceCancelInfoById" parameterType="Long" resultMap="ServiceCancelResult">
        select ServiceOrdState, ServiceOrdCancelReason, ServiceOrdCancelReasonTXT
        from ServiceOrder
        where ServiceOrdID = #{serviceOrdId}
    </select>
</mapper> 
ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml
@@ -53,6 +53,9 @@
        <result property="legacyServiceOrdClass"   column="legacy_service_ord_class" />
        <result property="serviceOrdVip"           column="service_ord_vip"         />
        <result property="fromHq2Is"               column="from_hq2_is"             />
        <result property="cancelReason"            column="cancel_reason"           />
        <result property="cancelBy"                column="cancel_by"               />
        <result property="cancelTime"              column="cancel_time"             />
        <result property="createTime"              column="create_time"             />
        <result property="updateTime"              column="update_time"             />
        <result property="createBy"                column="create_by"               />
@@ -68,6 +71,7 @@
               hospital_in_latitude, transfer_distance, transfer_price, passenger_contact, 
               passenger_phone, disease_ids, document_type_id, task_type_id, legacy_service_ord_id, legacy_dispatch_ord_id, 
               sync_status, sync_time, sync_error_msg, dispatch_sync_status, dispatch_sync_time, dispatch_sync_error_msg, need_resync, legacy_service_ord_no, legacy_dispatch_ord_no, legacy_service_ns_time, legacy_dispatch_ns_time, legacy_dispatch_ord_class, legacy_service_ord_class, service_ord_vip, from_hq2_is,
               cancel_reason, cancel_by, cancel_time,
               create_time, update_time, create_by, update_by
        from sys_task_emergency
    </sql>
@@ -132,6 +136,9 @@
            <if test="legacyServiceOrdClass != null">legacy_service_ord_class,</if>
            <if test="serviceOrdVip != null">service_ord_vip,</if>
            <if test="fromHq2Is != null">from_hq2_is,</if>
            <if test="cancelReason != null">cancel_reason,</if>
            <if test="cancelBy != null">cancel_by,</if>
            <if test="cancelTime != null">cancel_time,</if>
            <if test="createTime != null">create_time,</if>
            <if test="updateTime != null">update_time,</if>
            <if test="createBy != null">create_by,</if>
@@ -185,6 +192,9 @@
            <if test="legacyServiceOrdClass != null">#{legacyServiceOrdClass},</if>
            <if test="serviceOrdVip != null">#{serviceOrdVip},</if>
            <if test="fromHq2Is != null">#{fromHq2Is},</if>
            <if test="cancelReason != null">#{cancelReason},</if>
            <if test="cancelBy != null">#{cancelBy},</if>
            <if test="cancelTime != null">#{cancelTime},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="updateTime != null">#{updateTime},</if>
            <if test="createBy != null">#{createBy},</if>
@@ -241,6 +251,9 @@
            <if test="legacyServiceOrdClass != null">legacy_service_ord_class = #{legacyServiceOrdClass},</if>
            <if test="serviceOrdVip != null">service_ord_vip = #{serviceOrdVip},</if>
            <if test="fromHq2Is != null">from_hq2_is = #{fromHq2Is},</if>
            <if test="cancelReason != null">cancel_reason = #{cancelReason},</if>
            <if test="cancelBy != null">cancel_by = #{cancelBy},</if>
            <if test="cancelTime != null">cancel_time = #{cancelTime},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
        </trim>
sql/add_cancel_reason_fields.sql
New file
@@ -0,0 +1,24 @@
-- ----------------------------
-- ä¸ºsys_task_emergency表添加取消相关字段
-- ç”¨äºŽè®°å½•任务取消的原因、取消人和取消时间
-- æ‰§è¡Œæ—¶é—´ï¼š2024-12-24
-- ----------------------------
-- æ·»åŠ å–æ¶ˆåŽŸå› å­—æ®µï¼ˆå…³è”æ•°æ®å­—å…¸task_cancel_reason)
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_reason VARCHAR(50) COMMENT '取消原因(关联数据字典task_cancel_reason)' AFTER from_hq2_is;
-- æ·»åŠ å–æ¶ˆäººå­—æ®µ
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_by VARCHAR(64) COMMENT '取消人' AFTER cancel_reason;
-- æ·»åŠ å–æ¶ˆæ—¶é—´å­—æ®µ
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_time DATETIME COMMENT '取消时间' AFTER cancel_by;
-- åˆ›å»ºç´¢å¼•以提升查询性能
CREATE INDEX idx_cancel_reason ON sys_task_emergency(cancel_reason);
CREATE INDEX idx_cancel_time ON sys_task_emergency(cancel_time);
-- æŸ¥çœ‹è¡¨ç»“æž„
DESC sys_task_emergency;
sql/insert_dict_task_cancel_reason.sql
New file
@@ -0,0 +1,42 @@
-- ----------------------------
-- æ’入任务取消原因数据字典
-- å­—典类型:task_cancel_reason
-- æ‰§è¡Œæ—¶é—´ï¼š2024-12-24
-- ----------------------------
-- 1. æ’入字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('任务取消原因', 'task_cancel_reason', '0', 'admin', NOW(), '转运任务取消原因字典');
-- 2. æ’入字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) VALUES
(1, '价格不接受', '1', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '客户对价格不满意'),
(2, '时间紧急', '2', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '时间要求紧急'),
(3, '出车速度慢', '3', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '出车速度不够快'),
(4, '选择其他车', '4', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '客户选择其他车辆'),
(5, '病人没有生命体征', '5', 'task_cancel_reason', '', 'danger', 'N', '0', 'admin', NOW(), '病人已无生命体征'),
(6, '医生护士均不足', '6', 'task_cancel_reason', '', 'warning', 'N', '0', 'admin', NOW(), '医护人员不足'),
(7, '病人情况有变', '7', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '病人情况发生变化'),
(8, '其他', '8', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '其他原因'),
(9, '第三方取消', '9', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '第三方取消任务'),
(10, '测试订单', '10', 'task_cancel_reason', '', 'info', 'N', '0', 'admin', NOW(), '测试订单'),
(11, '传染性疾病', '11', 'task_cancel_reason', '', 'danger', 'N', '0', 'admin', NOW(), '病人患有传染性疾病'),
(12, '家属挂机/拒接/不接电话', '12', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '家属无法联系'),
(13, '护士不足', '13', 'task_cancel_reason', '', 'warning', 'N', '0', 'admin', NOW(), '护士人员不足'),
(14, '医生不足', '14', 'task_cancel_reason', '', 'warning', 'N', '0', 'admin', NOW(), '医生人员不足'),
(15, '设备不足(呼吸机)', '15', 'task_cancel_reason', '', 'warning', 'N', '0', 'admin', NOW(), '缺少必要设备'),
(16, '家属没联系好床位', '16', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '目标医院床位未确认'),
(17, '移交分支机构执行', '17', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '任务移交给分支机构'),
(18, '移交办事处(湛江)', '18', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '任务移交湛江办事处'),
(19, '移交办事处(茂名)', '19', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '任务移交茂名办事处'),
(20, '家属不肯透露信息', '20', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '家属拒绝提供必要信息'),
(21, '所在医院/目的地医院派车', '21', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '医院自己安排车辆'),
(22, '自驾车接送患者', '22', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '家属自己开车接送'),
(23, '选择其他机构车辆', '23', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '选择其他救护机构'),
(24, '外联通知取消', '24', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '外联合作方通知取消'),
(25, '外联无反馈', '25', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '外联无响应'),
(26, '家属无原因直接告知取消', '26', 'task_cancel_reason', '', 'default', 'N', '0', 'admin', NOW(), '家属直接要求取消');
-- æŸ¥è¯¢éªŒè¯
SELECT * FROM sys_dict_type WHERE dict_type = 'task_cancel_reason';
SELECT * FROM sys_dict_data WHERE dict_type = 'task_cancel_reason' ORDER BY dict_sort;
È¡ÏûÔ­Òò¹¦ÄÜʵÏÖ˵Ã÷.md
New file
@@ -0,0 +1,309 @@
# è½¬è¿ä»»åŠ¡å–æ¶ˆåŽŸå› åŠŸèƒ½å®žçŽ°è¯´æ˜Ž
## ä¸€ã€åŠŸèƒ½æ¦‚è¿°
为转运任务增加取消原因记录功能,当用户取消任务时,需要选择取消原因并记录取消人和取消时间。
## äºŒã€æ•°æ®åº“变更
### 2.1 sys_task_emergency表增加三个字段
执行SQL脚本:`sql/add_cancel_reason_fields.sql`
```sql
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_reason VARCHAR(50) COMMENT '取消原因(关联数据字典task_cancel_reason)';
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_by VARCHAR(64) COMMENT '取消人';
ALTER TABLE sys_task_emergency
ADD COLUMN cancel_time DATETIME COMMENT '取消时间';
```
### 2.2 æ•°æ®å­—典配置
执行SQL脚本:`sql/insert_dict_task_cancel_reason.sql`
添加数据字典类型 `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. å®¶å±žæ— åŽŸå› ç›´æŽ¥å‘ŠçŸ¥å–æ¶ˆ
## ä¸‰ã€åŽç«¯ä¿®æ”¹
### 3.1 å®žä½“类修改
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java`
添加三个字段属性:
- `private String cancelReason;` - å–消原因
- `private String cancelBy;` - å–消人
- `private java.util.Date cancelTime;` - å–消时间
### 3.2 Mapper XML修改
**文件**: `ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml`
在`resultMap`、`selectSysTaskEmergencyVo`、`insertSysTaskEmergency`、`updateSysTaskEmergency`中添加三个字段的映射和处理。
### 3.3 æŽ§åˆ¶å™¨ä¿®æ”¹
**文件**: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java`
#### 3.3.1 ChangeStatusRequest增加字段
```java
private String cancelReason;  // å–消原因(关联数据字典task_cancel_reason)
```
#### 3.3.2 ä¿®æ”¹appChangeTaskStatus方法
在状态变更方法中添加取消原因处理逻辑:
```java
// å¦‚果是取消状态,保存取消原因
if (newStatus == TaskStatus.CANCELLED && StringUtils.isNotEmpty(request.getCancelReason())) {
    sysTaskService.saveCancelInfo(taskId, request.getCancelReason());
}
```
### 3.4 æœåŠ¡å±‚ä¿®æ”¹
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java`
添加接口方法:
```java
/**
 * ä¿å­˜ä»»åŠ¡å–æ¶ˆä¿¡æ¯ï¼ˆä»…é™è½¬è¿ä»»åŠ¡ï¼‰
 *
 * @param taskId ä»»åŠ¡ID
 * @param cancelReason å–消原因(数据字典task_cancel_reason的value)
 * @return ç»“æžœ
 */
public int saveCancelInfo(Long taskId, String cancelReason);
```
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
实现saveCancelInfo方法,保存取消原因、取消人和取消时间:
```java
@Override
@Transactional
public int saveCancelInfo(Long taskId, String cancelReason) {
    // èŽ·å–ä»»åŠ¡ä¿¡æ¯
    SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
    if (task == null) {
        throw new RuntimeException("任务不存在");
    }
    // åªæœ‰è½¬è¿ä»»åŠ¡æ‰ä¿å­˜å–æ¶ˆä¿¡æ¯
    if (!"EMERGENCY_TRANSFER".equals(task.getTaskType())) {
        return 0;
    }
    // èŽ·å–è½¬è¿ä»»åŠ¡æ‰©å±•ä¿¡æ¯
    SysTaskEmergency emergency = sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId);
    if (emergency == null) {
        return 0;
    }
    // è®¾ç½®å–消信息
    emergency.setCancelReason(cancelReason);
    emergency.setCancelBy(SecurityUtils.getUsername());
    emergency.setCancelTime(DateUtils.getNowDate());
    emergency.setUpdateBy(SecurityUtils.getUsername());
    emergency.setUpdateTime(DateUtils.getNowDate());
    // æ›´æ–°æ•°æ®åº“
    return sysTaskEmergencyMapper.updateSysTaskEmergency(emergency);
}
```
## å››ã€å‰ç«¯ä¿®æ”¹
### 4.1 ä»»åŠ¡è¯¦æƒ…é¡µä¿®æ”¹
**文件**: `app/pagesTask/detail.vue`
#### 4.1.1 data中添加字段
```javascript
data() {
  return {
    taskDetail: null,
    taskId: null,
    paymentInfo: null,
    cancelReasonList: [],      // å–消原因列表
    showCancelDialog: false,   // æ˜¾ç¤ºå–消原因对话框
    selectedCancelReason: ''   // é€‰ä¸­çš„取消原因
  }
}
```
#### 4.1.2 onLoad中加载字典
```javascript
onLoad(options) {
  this.taskId = options.id
  this.loadTaskDetail()
  this.loadCancelReasonDict() // åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
}
```
#### 4.1.3 ä¿®æ”¹å–消按钮处理逻辑
```javascript
case 'cancel':
  // å–消 -> æ˜¾ç¤ºå–消原因选择对话框
  this.showCancelReasonDialog();
  break;
```
#### 4.1.4 æ·»åŠ æ–¹æ³•
- `loadCancelReasonDict()` - ä»ŽåŽç«¯åŠ è½½å–æ¶ˆåŽŸå› å­—å…¸
- `showCancelReasonDialog()` - æ˜¾ç¤ºå–消原因选择弹窗
- `confirmCancelTask()` - ç¡®è®¤å–消任务
- `closeCancelDialog()` - å…³é—­å–消弹窗
- `selectCancelReason(e)` - é€‰æ‹©å–消原因
- `updateTaskStatusWithCancelReason()` - å¸¦å–消原因的状态更新
- `getCancelReasonLabel()` - æ ¹æ®value获取label显示
#### 4.1.5 ä¿®æ”¹getLocationAndUpdateStatus方法
添加cancelReason参数,在状态更新请求中传递取消原因:
```javascript
// å¦‚果有取消原因,添加到请求数据中
if (cancelReason) {
  statusData.cancelReason = cancelReason
}
```
#### 4.1.6 æ·»åŠ UI组件
**取消原因选择弹窗**:
```vue
<uni-popup ref="cancelPopup" type="center" :is-mask-click="false">
  <view class="cancel-dialog">
    <view class="dialog-title">请选择取消原因</view>
    <picker mode="selector" :range="cancelReasonList" range-key="label" @change="selectCancelReason">
      <view class="reason-picker">
        <view class="picker-label">取消原因</view>
        <view class="picker-value">
          {{ selectedCancelReason ? cancelReasonList.find(r => r.value === selectedCancelReason)?.label : '请选择' }}
        </view>
        <uni-icons type="arrowright" size="16"></uni-icons>
      </view>
    </picker>
    <view class="dialog-buttons">
      <button class="cancel-btn" @click="closeCancelDialog">取消</button>
      <button class="confirm-btn" @click="confirmCancelTask">确定</button>
    </view>
  </view>
</uni-popup>
```
**取消信息显示区域**(仅在任务已取消且有取消原因时显示):
```vue
<view class="detail-section" v-if="taskDetail.taskStatus === 'CANCELLED' && taskDetail.emergencyInfo && taskDetail.emergencyInfo.cancelReason">
  <view class="section-title">取消信息</view>
  <view class="info-item">
    <view class="label">取消原因</view>
    <view class="value">{{ getCancelReasonLabel(taskDetail.emergencyInfo.cancelReason) }}</view>
  </view>
  <view class="info-item" v-if="taskDetail.emergencyInfo.cancelBy">
    <view class="label">取消人</view>
    <view class="value">{{ taskDetail.emergencyInfo.cancelBy }}</view>
  </view>
  <view class="info-item" v-if="taskDetail.emergencyInfo.cancelTime">
    <view class="label">取消时间</view>
    <view class="value">{{ formatTime(taskDetail.emergencyInfo.cancelTime) }}</view>
  </view>
</view>
```
#### 4.1.7 æ·»åŠ æ ·å¼
为取消原因弹窗添加样式,包括对话框、选择器和按钮样式。
## äº”、使用流程
### 5.1 å–消任务流程
1. ç”¨æˆ·åœ¨ä»»åŠ¡è¯¦æƒ…é¡µç‚¹å‡»"取消"按钮
2. ç³»ç»Ÿå¼¹å‡ºå–消原因选择对话框
3. ç”¨æˆ·ä»Ž26个预设选项中选择取消原因
4. ç”¨æˆ·ç‚¹å‡»"确定"按钮
5. ç³»ç»Ÿè°ƒç”¨çŠ¶æ€æ›´æ–°æŽ¥å£ï¼Œä¼ é€’å–æ¶ˆåŽŸå› 
6. åŽç«¯ä¿å­˜å–消原因、取消人(当前用户)和取消时间(当前时间)
7. ä»»åŠ¡çŠ¶æ€å˜æ›´ä¸º"已取消"
### 5.2 æŸ¥çœ‹å–消信息
在任务详情页,如果任务状态为"已取消"且记录了取消原因,会在费用信息下方显示"取消信息"区块,包括:
- å–消原因(显示中文标签)
- å–消人
- å–消时间
## å…­ã€æ³¨æ„äº‹é¡¹
1. **仅限转运任务**:取消原因功能仅对转运任务(EMERGENCY_TRANSFER)生效
2. **强制选择**:取消任务时必须选择取消原因,否则无法提交
3. **自动记录**:取消人和取消时间由系统自动记录,无需用户输入
4. **不可修改**:取消信息一旦保存不可修改
5. **数据字典依赖**:需要先执行数据字典SQL脚本,否则前端无法加载取消原因列表
## ä¸ƒã€æ–‡ä»¶æ¸…单
### SQL文件
- `sql/add_cancel_reason_fields.sql` - æ·»åŠ æ•°æ®åº“å­—æ®µ
- `sql/insert_dict_task_cancel_reason.sql` - æ’入数据字典
### åŽç«¯æ–‡ä»¶
- `ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java`
- `ruoyi-system/src/main/resources/mapper/system/SysTaskEmergencyMapper.xml`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java`
### å‰ç«¯æ–‡ä»¶
- `app/pagesTask/detail.vue`
## å…«ã€æµ‹è¯•建议
1. æµ‹è¯•取消原因弹窗是否正常显示
2. æµ‹è¯•选择不同取消原因后是否正确保存
3. æµ‹è¯•未选择取消原因时是否阻止提交
4. æµ‹è¯•取消信息是否正确显示
5. æµ‹è¯•非转运任务点击取消是否正常(不应显示取消原因弹窗)
6. æµ‹è¯•已取消的任务是否正确显示取消信息