wlzboy
2025-12-06 847a7773ef1a8ad418c6934d35b5f205a97c04d0
fix:在任务状态更新时,需要更新日志到旧系统
6个文件已添加
24个文件已修改
3个文件已删除
2457 ■■■■ 已修改文件
app/pages/task/index.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/AttachmentUpload.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/DepartmentSelector.vue 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/HospitalSelector.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue 168 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit-emergency.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/utils/taskValidator.js 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/转运任务状态变更记录同步功能说明.md 397 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/UserSyncDTO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskStatusChangedEvent.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/listener/DispatchOrdRunningListener.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserSyncServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/UserSyncMapper.xml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/test/java/com/ruoyi/system/mapper/SysDeptMapperTest.java 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/test/java/com/ruoyi/system/service/LegacySystemHttpsTest.java 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/test/java/com/ruoyi/system/service/SysDeptServiceTest.java 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/system/user/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/DispatchRunning.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/add_can_view_all_consult_to_sys_user.sql 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/index.vue
@@ -221,6 +221,7 @@
  import { listTask, changeTaskStatus } from '@/api/task'
  import { mapState } from 'vuex'
  import { formatDateTime } from '@/utils/common'
  import { checkTaskCanDepart } from '@/utils/taskValidator'
  
  export default {
    components: {
@@ -461,13 +462,87 @@
      },
      
      // å¤„理任务操作
      handleTaskAction(task, action) {
      async handleTaskAction(task, action) {
        switch (action) {
          case 'depart':
            // å‡ºå‘ -> çŠ¶æ€å˜ä¸ºå‡ºå‘ä¸­
            this.$modal.confirm('确定要出发吗?').then(() => {
              this.updateTaskStatus(task.taskId, 'DEPARTING', '任务已出发')
            }).catch(() => {});
            // æ˜¾ç¤ºåŠ è½½æç¤º
            uni.showLoading({
              title: '检查任务状态...'
            });
            try {
              // è°ƒç”¨å·¥å…·ç±»æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘ï¼ˆåŒ…å«åŸºæœ¬æ ¡éªŒå’Œå†²çªæ£€æŸ¥ï¼‰
              const checkResult = await checkTaskCanDepart(task)
              uni.hideLoading();
              console.log('出发检查结果:', checkResult);
              console.log('valid:', checkResult.valid);
              console.log('conflicts:', checkResult.conflicts);
              if (!checkResult.valid) {
                // æ ¡éªŒå¤±è´¥ï¼Œæ˜¾ç¤ºæç¤ºä¿¡æ¯å¹¶æä¾›è·³è½¬é€‰é¡¹
                const conflicts = checkResult.conflicts || [];
                const conflictInfo = conflicts.length > 0 ? conflicts[0] : null;
                console.log('冲突信息:', conflictInfo);
                // å¦‚果有冲突任务信息,提供跳转按钮
                if (conflictInfo && conflictInfo.taskId) {
                  console.log('显示带跳转按钮的弹窗,任务ID:', conflictInfo.taskId);
                  const conflictTaskId = conflictInfo.taskId;
                  const message = checkResult.message || conflictInfo.message || '存在冲突任务';
                  uni.showModal({
                    title: '提示',
                    content: message,
                    confirmText: '去处理',
                    cancelText: '知道了',
                    success: function(res) {
                      console.log('弹窗点击结果:', res);
                      if (res.confirm) {
                        // ç”¨æˆ·ç‚¹å‡»"现在去处理",跳转到冲突任务详情页
                        console.log('准备跳转到任务详情页:', conflictTaskId);
                        uni.navigateTo({
                          url: `/pagesTask/detail?id=${conflictTaskId}`
                        });
                      }
                    },
                    fail: function(err) {
                      console.error('显示弹窗失败:', err);
                    }
                  });
                } else {
                  // æ²¡æœ‰å†²çªä»»åŠ¡ID,只显示提示
                  console.log('显示普通提示弹窗');
                  uni.showModal({
                    title: '提示',
                    content: checkResult.message || '任务校验失败',
                    showCancel: false,
                    confirmText: '知道了',
                    fail: function(err) {
                      console.error('显示弹窗失败:', err);
                    }
                  });
                }
                return;
              }
              // æ‰€æœ‰æ£€æŸ¥é€šè¿‡ï¼Œå¯ä»¥å‡ºå‘
              this.$modal.confirm('确定要出发吗?').then(() => {
                this.updateTaskStatus(task.taskId, 'DEPARTING', '任务已出发')
              }).catch(() => {});
            } catch (error) {
              uni.hideLoading();
              console.error('检查任务状态失败:', error);
              // æ£€æŸ¥å¤±è´¥æ—¶ï¼Œä»ç„¶å…è®¸å‡ºå‘
              this.$modal.confirm('检查任务状态失败,是否继续出发?').then(() => {
                this.updateTaskStatus(task.taskId, 'DEPARTING', '任务已出发')
              }).catch(() => {});
            }
            break;
            
          case 'cancel':
app/pagesTask/components/AttachmentUpload.vue
@@ -50,7 +50,7 @@
            <view class="form-label">选择图片</view>
            <button class="choose-image-btn" @click="chooseImage">
              <uni-icons type="image" size="20"></uni-icons>
              <text>点击选择</text>
              <text class="btn-text">点击选择</text>
            </button>
          </view>
          <view class="preview-area" v-if="tempImagePath">
@@ -506,7 +506,7 @@
            color: #666;
            font-size: 28rpx;
            
            text {
            .btn-text {
              margin-left: 10rpx;
            }
          }
app/pagesTask/components/DepartmentSelector.vue
New file
@@ -0,0 +1,230 @@
<template>
  <view class="form-item">
    <view class="form-label" :class="{ required: required }">{{ label }}</view>
    <picker
      v-if="!isHome && departmentOptions.length > 0"
      mode="selector"
      :range="departmentOptions"
      range-key="text"
      :value="selectedIndex"
      @change="onDepartmentChange"
    >
      <view class="form-input picker-input">
        {{ displayText }}
        <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
      </view>
    </picker>
    <input
      v-else-if="!isHome"
      class="form-input"
      placeholder="请输入科室"
      :value="value"
      @input="onDepartmentInput"
    />
    <view v-else class="form-input picker-input disabled">
      å…¶å®ƒ
    </view>
  </view>
</template>
<script>
import { getHospitalDepartments } from "@/api/dictionary"
export default {
  name: 'DepartmentSelector',
  props: {
    // æ ‡ç­¾æ–‡æœ¬
    label: {
      type: String,
      default: '科室'
    },
    // æ˜¯å¦å¿…å¡«
    required: {
      type: Boolean,
      default: false
    },
    // å½“前选中的科室名称
    value: {
      type: String,
      default: ''
    },
    // å½“前选中的科室ID
    departmentId: {
      type: [Number, String],
      default: null
    },
    // æ˜¯å¦ä¸º"家中"(如果是家中,显示"其它"且不可选择)
    isHome: {
      type: Boolean,
      default: false
    },
    // å ä½ç¬¦
    placeholder: {
      type: String,
      default: '请选择科室'
    }
  },
  data() {
    return {
      departmentOptions: [], // ç§‘室选项列表
      selectedIndex: -1
    }
  },
  computed: {
    displayText() {
      if (this.selectedIndex >= 0 && this.selectedIndex < this.departmentOptions.length) {
        return this.departmentOptions[this.selectedIndex].text
      }
      return this.value || this.placeholder
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.updateSelectedIndex(newVal)
        } else {
          this.selectedIndex = -1
        }
      }
    },
    departmentId: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.updateSelectedIndexById(newVal)
        }
      }
    },
    departmentOptions: {
      immediate: true,
      handler() {
        if (this.value) {
          this.updateSelectedIndex(this.value)
        } else if (this.departmentId) {
          this.updateSelectedIndexById(this.departmentId)
        }
      }
    }
  },
  mounted() {
    this.loadDepartments()
  },
  methods: {
    // åŠ è½½ç§‘å®¤æ•°æ®
    loadDepartments() {
      getHospitalDepartments().then(response => {
        const list = response.data || []
        this.departmentOptions = list.map(item => ({
          id: item.vID,
          text: item.vtext,
          dictValue: item.vtext
        }))
      }).catch(error => {
        console.error('加载科室数据失败:', error)
        this.departmentOptions = []
      })
    },
    // æ ¹æ®ç§‘室名称更新选中索引
    updateSelectedIndex(departmentName) {
      if (!departmentName || this.departmentOptions.length === 0) {
        this.selectedIndex = -1
        return
      }
      const index = this.departmentOptions.findIndex(d => d.text === departmentName)
      this.selectedIndex = index !== -1 ? index : -1
    },
    // æ ¹æ®ç§‘室ID更新选中索引
    updateSelectedIndexById(departmentId) {
      if (!departmentId || this.departmentOptions.length === 0) {
        this.selectedIndex = -1
        return
      }
      const index = this.departmentOptions.findIndex(d => d.id == departmentId)
      this.selectedIndex = index !== -1 ? index : -1
    },
    // ç§‘室选择变化(picker)
    onDepartmentChange(e) {
      const index = e.detail.value
      // å®‰å…¨æ£€æŸ¥ï¼šç¡®ä¿ç´¢å¼•有效且选项存在
      if (index < 0 || index >= this.departmentOptions.length) {
        console.warn('科室选择索引越界:', index, '数组长度:', this.departmentOptions.length)
        return
      }
      const selected = this.departmentOptions[index]
      // äºŒæ¬¡æ£€æŸ¥ï¼šç¡®ä¿é€‰ä¸­é¡¹å­˜åœ¨ä¸”有text属性
      if (!selected || !selected.text) {
        console.warn('科室选项数据异常:', selected)
        return
      }
      this.selectedIndex = index
      this.$emit('input', selected.text)
      this.$emit('change', {
        department: selected.text,
        departmentId: selected.id
      })
    },
    // ç§‘室输入(手动输入)
    onDepartmentInput(e) {
      const department = e.detail.value
      this.$emit('input', department)
      this.$emit('change', {
        department: department,
        departmentId: null
      })
    }
  }
}
</script>
<style lang="scss" scoped>
.form-item {
  margin-bottom: 40rpx;
  .form-label {
    font-size: 28rpx;
    margin-bottom: 15rpx;
    color: #333;
    &.required::before {
      content: '*';
      color: #ff0000;
      margin-right: 5rpx;
    }
  }
  .form-input {
    width: 100%;
    height: 70rpx;
    padding: 0 20rpx;
    border: 1rpx solid #eee;
    border-radius: 10rpx;
    font-size: 28rpx;
    box-sizing: border-box;
    &.picker-input {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    &.disabled {
      background-color: #f5f5f5;
      color: #999;
    }
  }
}
</style>
app/pagesTask/components/HospitalSelector.vue
@@ -27,42 +27,6 @@
      </view>
    </view>
    
    <view class="form-item" v-if="showDepartment">
      <view class="form-label" :class="{ required: departmentRequired }">科室</view>
      <picker
        v-if="selectedHospitalName !== '家中' && departmentOptions.length > 0"
        mode="selector"
        :range="departmentOptions"
        range-key="text"
        @change="onDepartmentChange"
      >
        <view class="form-input picker-input">
          {{ departmentValue || '请选择科室' }}
          <uni-icons type="arrowright" size="16" color="#999"></uni-icons>
        </view>
      </picker>
      <input
        v-else-if="selectedHospitalName !== '家中'"
        class="form-input"
        placeholder="请输入科室"
        :value="departmentValue"
        @input="onDepartmentInput"
      />
      <view v-else class="form-input picker-input disabled">
        å…¶å®ƒ
      </view>
    </view>
    <view class="form-item" v-if="showBedNumber">
      <view class="form-label">床号</view>
      <input
        class="form-input"
        placeholder="请输入床号"
        :value="bedNumberValue"
        @input="onBedNumberInput"
      />
    </view>
    <view class="form-item">
      <view class="form-label" :class="{ required: required }">{{ addressLabel }}</view>
      <view class="address-input-container" v-if="selectedHospitalName === '家中'">
@@ -136,26 +100,6 @@
        address: ''
      })
    },
    // æ˜¯å¦æ˜¾ç¤ºç§‘室
    showDepartment: {
      type: Boolean,
      default: true
    },
    // ç§‘室是否必填
    departmentRequired: {
      type: Boolean,
      default: false
    },
    // ç§‘室选项列表(用于 picker)
    departmentOptions: {
      type: Array,
      default: () => []
    },
    // æ˜¯å¦æ˜¾ç¤ºåºŠå·
    showBedNumber: {
      type: Boolean,
      default: true
    },
    // éƒ¨é—¨ID(用于区域过滤)
    deptId: {
      type: [Number, String],
@@ -186,12 +130,6 @@
  computed: {
    addressValue() {
      return this.value.address || ''
    },
    departmentValue() {
      return this.value.department || ''
    },
    bedNumberValue() {
      return this.value.bedNumber || ''
    }
  },
  watch: {
@@ -292,16 +230,7 @@
      const hospitalData = {
        id: hospital.hospId,
        name: hospital.hospName,
        department: this.value.department || '',
        departmentId: this.value.departmentId || null,
        bedNumber: this.value.bedNumber || '',
        address: hospital.hospName === '家中' ? '' : this.buildFullAddress(hospital)
      }
      // å¦‚果选择的是"家中",科室设置为"其它"
      if (hospital.hospName === '家中') {
        hospitalData.department = '其它'
        hospitalData.departmentId = null
      }
      
      this.showResults = false
@@ -388,45 +317,13 @@
        ...this.value,
        address: fullAddress
      })
      this.$emit('address-selected', {
        address: fullAddress,
        location: item.location
      })
          
      this.showAddressSuggestions = false
      this.addressSuggestions = []
    },
    // ç§‘室选择变化(picker)
    onDepartmentChange(e) {
      const index = e.detail.value
      const selected = this.departmentOptions[index]
      const updatedValue = {
        ...this.value,
        department: selected.text,
        departmentId: selected.id
      }
      this.$emit('input', updatedValue)
      this.$emit('department-change', {
        department: selected.text,
        departmentId: selected.id
      })
    },
    // ç§‘室输入(手动输入)
    onDepartmentInput(e) {
      const department = e.detail.value
      this.$emit('input', {
        ...this.value,
        department: department
      })
      this.$emit('department-change', department)
    },
    // åºŠå·è¾“å…¥
    onBedNumberInput(e) {
      const bedNumber = e.detail.value
      this.$emit('input', {
        ...this.value,
        bedNumber: bedNumber
      })
      this.$emit('bed-number-change', bedNumber)
    }
  }
}
app/pagesTask/create-emergency.vue
@@ -144,28 +144,60 @@
        label="医院名称"
        address-label="转出地址"
        :required="true"
        :department-required="true"
        :show-department="false"
        v-model="taskForm.hospitalOut"
        :dept-id="selectedOrganizationId"
        :region="selectedRegion"
        :department-options="departmentOptions"
        @change="onHospitalOutChange"
        @address-selected="onHospitalOutAddressSelected"
      />
      <DepartmentSelector
        label="转出科室"
        :required="true"
        v-model="taskForm.hospitalOut.department"
        :department-id="taskForm.hospitalOut.departmentId"
        :is-home="taskForm.hospitalOut.name === '家中'"
        @change="onHospitalOutDepartmentChange"
      />
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalOut.bedNumber"
        />
      </view>
      
      <view class="form-section-title">转入医院信息</view>
      <HospitalSelector
        label="医院名称"
        address-label="转入地址"
        :required="true"
        :department-required="true"
        :show-department="false"
        v-model="taskForm.hospitalIn"
        :dept-id="selectedOrganizationId"
        :region="selectedRegion"
        :department-options="departmentOptions"
        @change="onHospitalInChange"
        @address-selected="onHospitalInAddressSelected"
      />
      <DepartmentSelector
        label="转入科室"
        :required="true"
        v-model="taskForm.hospitalIn.department"
        :department-id="taskForm.hospitalIn.departmentId"
        :is-home="taskForm.hospitalIn.name === '家中'"
        @change="onHospitalInDepartmentChange"
      />
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalIn.bedNumber"
        />
      </view>
      
      <view class="form-item">
        <view class="form-label required">转运公里数</view>
@@ -245,13 +277,14 @@
import { checkVehicleActiveTasks } from "@/api/task"
import { getDicts } from "@/api/dict"
import { getServiceOrdAreaTypes, getServiceOrderTypes, getHospitalDepartments } from "@/api/dictionary"
import { getServiceOrdAreaTypes, getServiceOrderTypes } from "@/api/dictionary"
import { listBranchCompany, getDept } from "@/api/system/dept"
import MapSelector from './components/map-selector.vue'
import OrganizationSelector from './components/OrganizationSelector.vue'
import HospitalSelector from './components/HospitalSelector.vue'
import DiseaseSelector from './components/DiseaseSelector.vue'
import StaffSelector from './components/StaffSelector.vue'
import DepartmentSelector from './components/DepartmentSelector.vue'
export default {
  components: {
@@ -262,7 +295,8 @@
    HospitalSelector,
    DiseaseSelector,
    DepartureSelector,
    StaffSelector
    StaffSelector,
    DepartmentSelector
  },
  data() {
    return {
@@ -320,7 +354,6 @@
      emergencyTaskTypeOptions: [], // ä»»åŠ¡ç±»åž‹é€‰é¡¹ï¼ˆç”¨äºŽpicker显示)
      documentTypes: [], // å•据类型列表
      documentTypeOptions: [], // å•据类型选项(用于picker显示)
      departmentOptions: [], // ç§‘室字典数据
      loading: false,
      // æ™ºèƒ½è¯†åˆ«ç›¸å…³
      rawText: '',
@@ -357,8 +390,6 @@
    this.getAvailableVehicles().then(() => {
      this.getUserBoundVehicleInfo()
    })
    // åŠ è½½ç§‘å®¤å­—å…¸æ•°æ®
    this.loadDepartments()
    // åŠ è½½ä»»åŠ¡ç±»åž‹æ•°æ®
    this.loadEmergencyTaskTypes()
    // åŠ è½½å•æ®ç±»åž‹æ•°æ®
@@ -468,22 +499,6 @@
      }
    },
    
    // åŠ è½½ç§‘å®¤æ•°æ®ï¼ˆä»Ž SQL Server åŠ¨æ€åŠ è½½ï¼‰
    loadDepartments() {
      getHospitalDepartments().then(response => {
        const list = response.data || [];
        this.departmentOptions = list.map(item => ({
          id: item.vID,
          text: item.vtext,
          dictValue: item.vtext  // ä¸ºäº†ä¿æŒå…¼å®¹æ€§ï¼Œä¿ç•™dictValue字段
        }));
        // console.log('科室数据加载成功:', this.departmentOptions);
      }).catch(error => {
        console.error('加载科室数据失败:', error)
        this.departmentOptions = []
      })
    },
    // åŠ è½½ä»»åŠ¡ç±»åž‹æ•°æ®ï¼ˆä»Ž SQL Server)
    loadEmergencyTaskTypes() {
      getServiceOrderTypes().then(response => {
@@ -552,6 +567,12 @@
      console.log('转出医院变化:', hospitalData)
      // ç»„件已经通过 v-model æ›´æ–°äº† taskForm.hospitalOut
      
      // å¦‚果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalOut.department = '其它'
        this.taskForm.hospitalOut.departmentId = null
      }
      // å¦‚果转入地址已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
        // å¦‚果两个都不是"家中",使用医院距离计算
@@ -576,6 +597,12 @@
      console.log('转入医院变化:', hospitalData)
      // ç»„件已经通过 v-model æ›´æ–°äº† taskForm.hospitalIn
      
      // å¦‚果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalIn.department = '其它'
        this.taskForm.hospitalIn.departmentId = null
      }
      // å¦‚果转出地址已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
        // å¦‚果两个都不是"家中",使用医院距离计算
@@ -592,6 +619,28 @@
    onHospitalInAddressSelected(data) {
      if (this.taskForm.hospitalOut.address) {
        this.calculateDistanceByManualAddress()
      }
    },
    // è½¬å‡ºç§‘室变化
    onHospitalOutDepartmentChange(data) {
      if (data && typeof data === 'object') {
        this.taskForm.hospitalOut.department = data.department
        this.taskForm.hospitalOut.departmentId = data.departmentId
      } else {
        this.taskForm.hospitalOut.department = data
        this.taskForm.hospitalOut.departmentId = null
      }
    },
    // è½¬å…¥ç§‘室变化
    onHospitalInDepartmentChange(data) {
      if (data && typeof data === 'object') {
        this.taskForm.hospitalIn.department = data.department
        this.taskForm.hospitalIn.departmentId = data.departmentId
      } else {
        this.taskForm.hospitalIn.department = data
        this.taskForm.hospitalIn.departmentId = null
      }
    },
    
@@ -1002,20 +1051,14 @@
      if (result.phone) this.taskForm.patient.phone = result.phone
      if (result.price) this.taskForm.price = result.price
      
      // åº”用科室信息(匹配 departmentOptions ä¸­çš„æ•°æ®ï¼‰
      // åº”用科室信息(通过 DepartmentSelector ç»„件处理)
      if (result.departmentOut) {
        const deptOut = this.matchDepartment(result.departmentOut)
        if (deptOut) {
          this.taskForm.hospitalOut.department = deptOut.text
          this.taskForm.hospitalOut.departmentId = deptOut.id
        }
        this.taskForm.hospitalOut.department = result.departmentOut
        // ç§‘室ID会在 DepartmentSelector ç»„件中自动匹配
      }
      if (result.departmentIn) {
        const deptIn = this.matchDepartment(result.departmentIn)
        if (deptIn) {
          this.taskForm.hospitalIn.department = deptIn.text
          this.taskForm.hospitalIn.departmentId = deptIn.id
        }
        this.taskForm.hospitalIn.department = result.departmentIn
        // ç§‘室ID会在 DepartmentSelector ç»„件中自动匹配
      }
      // å¤„理医院名称 â†’ ç²¾ç¡®åŒ¹é…åŒ»é™¢å¹¶è¡¥å…¨åœ°å€ä¸ŽID(不限制分公司区域)
@@ -1161,40 +1204,6 @@
      return ''
    },
    
    // åŒ¹é…ç§‘室(优先使用 departmentOptions ä¸­çš„æ•°æ®ï¼‰
    matchDepartment(deptName) {
      if (!deptName || !this.departmentOptions || this.departmentOptions.length === 0) {
        return null
      }
      const normalized = deptName.trim().toUpperCase()
      // 1. ç²¾ç¡®åŒ¹é…ï¼ˆä¸åŒºåˆ†å¤§å°å†™ï¼‰
      let matched = this.departmentOptions.find(d =>
        d.text.toUpperCase() === normalized
      )
      if (matched) return matched
      // 2. åŒ…含匹配(科室名包含识别到的关键词)
      matched = this.departmentOptions.find(d =>
        d.text.toUpperCase().includes(normalized) ||
        normalized.includes(d.text.toUpperCase())
      )
      if (matched) return matched
      // 3. æ¨¡ç³ŠåŒ¹é…ï¼ˆå޻除"科"、"室"等后缀再匹配)
      const cleanedInput = normalized.replace(/[科室部]/g, '')
      matched = this.departmentOptions.find(d => {
        const cleanedDept = d.text.toUpperCase().replace(/[科室部]/g, '')
        return cleanedDept === cleanedInput ||
               cleanedDept.includes(cleanedInput) ||
               cleanedInput.includes(cleanedDept)
      })
      if (matched) return matched
      return null
    },
    // æå–科室信息
    extractDepartment(text, type) {
      // å¸¸è§ç§‘室关键词(作为兜底方案)
@@ -1210,24 +1219,7 @@
        '检验科', '病理科', '药剂科', '营养科'
      ]
      
      // ä¼˜å…ˆå°è¯•从 departmentOptions ä¸­åŒ¹é…
      if (this.departmentOptions && this.departmentOptions.length > 0) {
        // æž„建 departmentOptions çš„匹配模式(按长度倒序)
        const optionTexts = this.departmentOptions.map(d => d.text).sort((a, b) => b.length - a.length)
        const optionPattern = optionTexts.map(t => t.replace(/[()()]/g, '\\$&')).join('|')
        if (optionPattern) {
          const regex = new RegExp(`(${optionPattern})`, 'gi')
          const matches = text.match(regex)
          if (matches && matches.length > 0) {
            // å¦‚果是转出,取第一个科室;如果是转入,取最后一个科室
            return type === 'out' ? matches[0] : matches[matches.length - 1]
          }
        }
      }
      // å…œåº•:使用默认科室列表匹配
      // ä½¿ç”¨é»˜è®¤ç§‘室列表匹配
      const sortedDepts = departments.sort((a, b) => b.length - a.length)
      const deptPattern = sortedDepts.join('|')
      
app/pagesTask/detail.vue
@@ -37,7 +37,7 @@
          <view 
            class="assignee-item" 
            v-for="(assignee, index) in taskDetail.assignees" 
            :key="assignee.userId || index"
            :key="'assignee-' + (assignee.userId || assignee.userName || index)"
          >
            <view class="assignee-index">{{ index + 1 }}</view>
            <view class="assignee-info">
@@ -131,12 +131,12 @@
        </view>
      </view>
      
      <view class="detail-section" v-if="taskDetail.taskDescription">
      <view class="detail-section" v-if="taskDetail.taskDescription && taskDetail.taskType !== 'EMERGENCY_TRANSFER'">
        <view class="section-title">任务描述</view>
        <view class="description">{{ taskDetail.taskDescription }}</view>
      </view>
      
      <view class="detail-section" v-if="taskDetail.remark">
      <view class="detail-section" v-if="taskDetail.remark && taskDetail.taskType !== 'EMERGENCY_TRANSFER'">
        <view class="section-title">备注信息</view>
        <view class="description">{{ taskDetail.remark }}</view>
      </view>
@@ -250,8 +250,8 @@
        <view class="section-title">支付记录</view>
        <view 
          class="payment-record-item" 
          v-for="payment in paymentInfo.paidPayments"
          :key="payment.id"
          v-for="(payment, index) in paymentInfo.paidPayments"
          :key="'payment-' + (payment.id || index)"
        >
          <view class="payment-header">
            <view 
@@ -449,6 +449,7 @@
  import { checkVehicleActiveTasks } from '@/api/task'
  import { getPaymentInfo } from '@/api/payment'
  import { formatDateTime } from '@/utils/common'
  import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator'
  import AttachmentUpload from './components/AttachmentUpload.vue'
  
  export default {
@@ -752,6 +753,15 @@
      
      // å¤„理结算
      handleSettlement() {
        // æ ¡éªŒä»»åŠ¡æ˜¯å¦å¯ä»¥ç»“ç®—
        const validation = validateTaskForSettlement(this.taskDetail)
        if (!validation.valid) {
          this.$modal.confirm(`${validation.message},需要先修改任务后才能结算。是否现在去修改?`).then(() => {
            this.handleEdit()
          }).catch(() => {})
          return
        }
        uni.navigateTo({
          url: '/pagesTask/settlement?taskId=' + this.taskId
        })
@@ -803,68 +813,84 @@
      },
      
      // æ£€æŸ¥è½¦è¾†çŠ¶æ€å¹¶å‡ºå‘
      checkVehicleAndDepart() {
        // æ£€æŸ¥å‡ºå‘时间是否为空或1900年(修复:防止无效时间)
        if (!this.taskDetail.plannedStartTime || this.taskDetail.plannedStartTime.startsWith('1900')) {
          this.$modal.confirm('任务的转运时间未设置或无效,需要先修改任务补充转运时间后才能出发。是否现在去修改?').then(() => {
            this.handleEdit()
          }).catch(() => {})
          return
        }
        // èŽ·å–ä»»åŠ¡è½¦è¾†ID
        const vehicleId = this.getVehicleId();
        if (!vehicleId) {
          this.$modal.showToast('未找到任务车辆信息');
          return;
        }
      async checkVehicleAndDepart() {
        // æ˜¾ç¤ºåŠ è½½æç¤º
        uni.showLoading({
          title: '检查车辆状态...'
          title: '检查任务状态...'
        });
        
        checkVehicleActiveTasks(vehicleId).then(response => {
        try {
          // è°ƒç”¨å·¥å…·ç±»æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘ï¼ˆåŒ…å«åŸºæœ¬æ ¡éªŒå’Œå†²çªæ£€æŸ¥ï¼‰
          const checkResult = await checkTaskCanDepart(this.taskDetail)
          uni.hideLoading();
          
          const activeTasks = response.data || [];
          console.log('出发检查结果:', checkResult);
          console.log('valid:', checkResult.valid);
          console.log('conflicts:', checkResult.conflicts);
          
          // è¿‡æ»¤æŽ‰å½“前任务本身(修复:防止 activeTasks ä¸º null)
          const otherActiveTasks = (activeTasks && Array.isArray(activeTasks)) ? activeTasks.filter(task => task.taskId !== this.taskId) : [];
          if (otherActiveTasks.length > 0) {
            // è½¦è¾†æœ‰å…¶ä»–正在进行中的任务
            const task = otherActiveTasks[0];
            const taskStatus = this.getStatusText(task.taskStatus);
            const message = `该车辆已有正在转运中的任务!
任务单号:${task.taskCode}
任务状态:${taskStatus}
请先完成当前任务后再出发新任务。`;
          if (!checkResult.valid) {
            // æ ¡éªŒå¤±è´¥ï¼Œæ˜¾ç¤ºæç¤ºä¿¡æ¯å¹¶æä¾›è·³è½¬é€‰é¡¹
            const conflicts = checkResult.conflicts || [];
            const conflictInfo = conflicts.length > 0 ? conflicts[0] : null;
            
            uni.showModal({
              title: '提示',
              content: message,
              showCancel: false,
              confirmText: '我知道了'
            });
            console.log('冲突信息:', conflictInfo);
            // å¦‚果有冲突任务信息,提供跳转按钮
            if (conflictInfo && conflictInfo.taskId) {
              console.log('显示带跳转按钮的弹窗,任务ID:', conflictInfo.taskId);
              const conflictTaskId = conflictInfo.taskId;
              const message = checkResult.message || conflictInfo.message || '存在冲突任务';
              uni.showModal({
                title: '提示',
                content: message,
                confirmText: '去处理',
                cancelText: '知道了',
                success: function(res) {
                  console.log('弹窗点击结果:', res);
                  if (res.confirm) {
                    // ç”¨æˆ·ç‚¹å‡»"现在去处理",跳转到冲突任务详情页
                    console.log('准备跳转到任务详情页:', conflictTaskId);
                    uni.redirectTo({
                      url: `/pagesTask/detail?id=${conflictTaskId}`
                    });
                  }
                },
                fail: function(err) {
                  console.error('显示弹窗失败:', err);
                }
              });
            } else {
              // æ²¡æœ‰å†²çªä»»åŠ¡ID,只显示提示
              console.log('显示普通提示弹窗');
              uni.showModal({
                title: '提示',
                content: checkResult.message || '任务校验失败',
                showCancel: false,
                confirmText: '知道了',
                fail: function(err) {
                  console.error('显示弹窗失败:', err);
                }
              });
            }
            return;
          }
          
          // è½¦è¾†æ²¡æœ‰å…¶ä»–正在进行中的任务,可以出发
          // æ‰€æœ‰æ£€æŸ¥é€šè¿‡ï¼Œå¯ä»¥å‡ºå‘
          this.$modal.confirm('确定要出发吗?').then(() => {
            this.updateTaskStatus('DEPARTING', '任务已出发')
          }).catch(() => {});
          
        }).catch(error => {
        } catch (error) {
          uni.hideLoading();
          console.error('检查车辆状态失败:', error);
          console.error('检查任务状态失败:', error);
          // æ£€æŸ¥å¤±è´¥æ—¶ï¼Œä»ç„¶å…è®¸å‡ºå‘
          this.$modal.confirm('检查车辆状态失败,是否继续出发?').then(() => {
          this.$modal.confirm('检查任务状态失败,是否继续出发?').then(() => {
            this.updateTaskStatus('DEPARTING', '任务已出发')
          }).catch(() => {});
        });
        }
      },
      
      // èŽ·å–ä»»åŠ¡è½¦è¾†ID
app/pagesTask/edit-emergency.vue
@@ -119,25 +119,61 @@
      
      <view class="form-section-title">转出医院信息</view>
      <HospitalSelector
        label="区院名称"
        label="医院名称"
        address-label="转出地址"
        :required="true"
        :show-department="false"
        v-model="taskForm.hospitalOut"
        :dept-id="selectedOrganizationId"
        @change="onHospitalOutChange"
        @address-selected="onHospitalOutAddressSelected"
      />
      <DepartmentSelector
        label="转出科室"
        :required="true"
        v-model="taskForm.hospitalOut.department"
        :department-id="taskForm.hospitalOut.departmentId"
        :is-home="taskForm.hospitalOut.name === '家中'"
        @change="onHospitalOutDepartmentChange"
      />
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalOut.bedNumber"
        />
      </view>
      
      <view class="form-section-title">转入医院信息</view>
      <HospitalSelector
        label="医院名称"
        address-label="转入地址"
        :required="true"
        :show-department="false"
        v-model="taskForm.hospitalIn"
        :dept-id="selectedOrganizationId"
        @change="onHospitalInChange"
        @address-selected="onHospitalInAddressSelected"
      />
      <DepartmentSelector
        label="转入科室"
        :required="true"
        v-model="taskForm.hospitalIn.department"
        :department-id="taskForm.hospitalIn.departmentId"
        :is-home="taskForm.hospitalIn.name === '家中'"
        @change="onHospitalInDepartmentChange"
      />
      <view class="form-item">
        <view class="form-label">床号</view>
        <input
          class="form-input"
          placeholder="请输入床号"
          v-model="taskForm.hospitalIn.bedNumber"
        />
      </view>
      
      <view class="form-item">
        <view class="form-label">转运距离</view>
@@ -204,6 +240,7 @@
import DiseaseSelector from './components/DiseaseSelector.vue'
import DepartureSelector from './components/DepartureSelector.vue'
import StaffSelector from './components/StaffSelector.vue'
import DepartmentSelector from './components/DepartmentSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
@@ -216,7 +253,8 @@
    HospitalSelector,
    DiseaseSelector,
    DepartureSelector,
    StaffSelector
    StaffSelector,
    DepartmentSelector
  },
  mixins: [distanceCalculator],
  data() {
@@ -526,6 +564,12 @@
      console.log('转出医院变化:', hospitalData)
      // ç»„件已经通过 v-model æ›´æ–°äº† taskForm.hospitalOut
      
      // å¦‚果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalOut.department = '其它'
        this.taskForm.hospitalOut.departmentId = null
      }
      // å¦‚果转入地址已填写,自动计算距离
      if (this.taskForm.hospitalIn.address) {
        // å¦‚果两个都不是"家中",使用医院距离计算
@@ -550,6 +594,12 @@
      console.log('转入医院变化:', hospitalData)
      // ç»„件已经通过 v-model æ›´æ–°äº† taskForm.hospitalIn
      
      // å¦‚果选择的是"家中",自动设置科室为"其它"
      if (hospitalData.name === '家中') {
        this.taskForm.hospitalIn.department = '其它'
        this.taskForm.hospitalIn.departmentId = null
      }
      // å¦‚果转出地址已填写,自动计算距离
      if (this.taskForm.hospitalOut.address) {
        // å¦‚果两个都不是"家中",使用医院距离计算
@@ -569,6 +619,28 @@
      }
    },
    
    // è½¬å‡ºç§‘室变化
    onHospitalOutDepartmentChange(data) {
      if (data && typeof data === 'object') {
        this.taskForm.hospitalOut.department = data.department
        this.taskForm.hospitalOut.departmentId = data.departmentId
      } else {
        this.taskForm.hospitalOut.department = data
        this.taskForm.hospitalOut.departmentId = null
      }
    },
    // è½¬å…¥ç§‘室变化
    onHospitalInDepartmentChange(data) {
      if (data && typeof data === 'object') {
        this.taskForm.hospitalIn.department = data.department
        this.taskForm.hospitalIn.departmentId = data.departmentId
      } else {
        this.taskForm.hospitalIn.department = data
        this.taskForm.hospitalIn.departmentId = null
      }
    },
    // ç—…情变化
    onDiseaseChange(diseases) {
      console.log('病情变化:', diseases)
app/utils/taskValidator.js
New file
@@ -0,0 +1,226 @@
/**
 * ä»»åŠ¡æ ¡éªŒå·¥å…·ç±»
 * ç”¨äºŽç»Ÿä¸€ç®¡ç†ä»»åŠ¡ç›¸å…³çš„å‰ç½®æ ¡éªŒé€»è¾‘
 */
import request from '@/utils/request'
/**
 * æ ¡éªŒä»»åŠ¡æ˜¯å¦å¯ä»¥æ‰§è¡Œæ“ä½œï¼ˆå‡ºå‘ã€ç»“ç®—ç­‰ï¼‰
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @param {Object} options - æ ¡éªŒé€‰é¡¹
 * @param {Boolean} options.checkAssignees - æ˜¯å¦æ£€æŸ¥æ‰§è¡Œäººå‘˜ï¼Œé»˜è®¤true
 * @param {Boolean} options.checkVehicles - æ˜¯å¦æ£€æŸ¥è½¦è¾†ï¼Œé»˜è®¤true
 * @param {Boolean} options.checkPlannedTime - æ˜¯å¦æ£€æŸ¥é¢„约时间,默认true
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validateTaskForAction(task, options = {}) {
  const {
    checkAssignees = true,
    checkVehicles = true,
    checkPlannedTime = true
  } = options
  // 1. æ£€æŸ¥æ‰§è¡Œäººå‘˜
  if (checkAssignees) {
    const assigneeResult = validateAssignees(task)
    if (!assigneeResult.valid) {
      return assigneeResult
    }
  }
  // 2. æ£€æŸ¥è½¦è¾†
  if (checkVehicles) {
    const vehicleResult = validateVehicles(task)
    if (!vehicleResult.valid) {
      return vehicleResult
    }
  }
  // 3. æ£€æŸ¥é¢„约时间
  if (checkPlannedTime) {
    const timeResult = validatePlannedTime(task)
    if (!timeResult.valid) {
      return timeResult
    }
  }
  return { valid: true, message: '', field: '' }
}
/**
 * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å·²åˆ†é…æ‰§è¡Œäººå‘˜
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validateAssignees(task) {
  if (!task.assignees || task.assignees.length === 0) {
    return {
      valid: false,
      message: '任务未分配执行人员',
      field: 'assignees'
    }
  }
  return { valid: true, message: '', field: '' }
}
/**
 * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å·²åˆ†é…è½¦è¾†
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validateVehicles(task) {
  // æ”¯æŒä¸¤ç§å­—段名:vehicleList å’Œ assignedVehicles
  const vehicles = task.vehicleList || task.assignedVehicles
  if (!vehicles || vehicles.length === 0) {
    return {
      valid: false,
      message: '任务未分配车辆',
      field: 'vehicles'
    }
  }
  return { valid: true, message: '', field: '' }
}
/**
 * æ£€æŸ¥ä»»åŠ¡é¢„çº¦æ—¶é—´æ˜¯å¦æœ‰æ•ˆ
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validatePlannedTime(task) {
  if (!task.plannedStartTime) {
    return {
      valid: false,
      message: '任务的转运时间未设置',
      field: 'plannedStartTime'
    }
  }
  // æ£€æŸ¥æ˜¯å¦ä¸ºæ— æ•ˆæ—¶é—´ï¼ˆ1900年)
  if (task.plannedStartTime.startsWith('1900')) {
    return {
      valid: false,
      message: '任务的转运时间无效',
      field: 'plannedStartTime'
    }
  }
  return { valid: true, message: '', field: '' }
}
/**
 * èŽ·å–ä»»åŠ¡çš„è½¦è¾†ID
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Number|null} è½¦è¾†ID,未找到返回null
 */
export function getTaskVehicleId(task) {
  if (!task) {
    return null
  }
  // ä»Žè½¦è¾†åˆ—表中获取第一个车辆的ID
  const vehicles = task.assignedVehicles || task.vehicleList
  if (vehicles && vehicles.length > 0) {
    return vehicles[0].vehicleId
  }
  // æˆ–者从单个车辆对象获取
  if (task.vehicleId) {
    return task.vehicleId
  }
  return null
}
/**
 * æ ¡éªŒä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validateTaskForDepart(task) {
  return validateTaskForAction(task, {
    checkAssignees: true,
    checkVehicles: true,
    checkPlannedTime: true
  })
}
/**
 * æ ¡éªŒä»»åŠ¡æ˜¯å¦å¯ä»¥ç»“ç®—
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Object} { valid: Boolean, message: String, field: String }
 */
export function validateTaskForSettlement(task) {
  return validateTaskForAction(task, {
    checkAssignees: true,
    checkVehicles: true,
    checkPlannedTime: true
  })
}
/**
 * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘ï¼ˆè°ƒç”¨åŽç«¯æŽ¥å£ï¼‰
 * æ£€æŸ¥ï¼š
 * 1. åŸºæœ¬æ ¡éªŒï¼šè½¦è¾†ã€æ‰§è¡Œäººå‘˜ã€é¢„约时间
 * 2. è½¦è¾†å†²çªæ£€æŸ¥ï¼šæ˜¯å¦æœ‰æœªå®Œæˆçš„任务
 * 3. äººå‘˜å†²çªæ£€æŸ¥ï¼šæ‰§è¡Œäººæ˜¯å¦æœ‰æœªå®Œæˆçš„任务
 *
 * @param {Object} task - ä»»åŠ¡å¯¹è±¡
 * @returns {Promise<Object>} { valid: Boolean, message: String, conflicts: Array }
 */
export async function checkTaskCanDepart(task) {
  // 1. å…ˆè¿›è¡ŒåŸºæœ¬æ ¡éªŒ
  const basicValidation = validateTaskForDepart(task)
  if (!basicValidation.valid) {
    return {
      ...basicValidation,
      conflicts: [] // ç¡®ä¿è¿”回的对象包含conflicts字段
    }
  }
  // 2. è°ƒç”¨åŽç«¯æŽ¥å£æ£€æŸ¥è½¦è¾†å’Œäººå‘˜å†²çª
  try {
    const response = await request({
      url: `/task/${task.taskId}/check-depart`,
      method: 'get'
    })
    console.log('后端返回的原始数据:', response)
    if (response.code === 200 && response.data) {
      const { valid, conflicts } = response.data
      console.log('valid:', valid)
      console.log('conflicts:', conflicts)
      if (!valid && conflicts && conflicts.length > 0) {
        // æœ‰å†²çªï¼Œè¿”回第一个冲突信息
        return {
          valid: false,
          message: conflicts[0].message,
          conflicts: conflicts
        }
      }
      return { valid: true, message: '', conflicts: [] }
    }
    return { valid: false, message: '检查失败,请重试', conflicts: [] }
  } catch (error) {
    console.error('检查任务是否可以出发失败:', error)
    // æŽ¥å£å¤±è´¥æ—¶ï¼Œåªè¿›è¡ŒåŸºæœ¬æ ¡éªŒï¼Œå…è®¸å‡ºå‘
    return { valid: true, message: '', conflicts: [] }
  }
}
export default {
  validateTaskForAction,
  validateAssignees,
  validateVehicles,
  validatePlannedTime,
  getTaskVehicleId,
  validateTaskForDepart,
  validateTaskForSettlement,
  checkTaskCanDepart
}
prd/תÔËÈÎÎñ״̬±ä¸ü¼Ç¼ͬ²½¹¦ÄÜ˵Ã÷.md
New file
@@ -0,0 +1,397 @@
# è½¬è¿ä»»åŠ¡çŠ¶æ€å˜æ›´è®°å½•åŒæ­¥åˆ°æ—§ç³»ç»ŸåŠŸèƒ½è¯´æ˜Ž
## ä¸€ã€åŠŸèƒ½æ¦‚è¿°
本功能实现了在转运任务状态发生变更时,自动将状态变更记录同步到SQL Server旧系统的 `DispatchOrd_Running` è¡¨ä¸­ã€‚该功能通过Spring事件监听机制实现,确保状态变更的完整记录和追踪。
## äºŒã€æ ¸å¿ƒç»„ä»¶
### 1. æ•°æ®åº“表结构
**旧系统表:DispatchOrd_Running**(SQL Server)
```sql
create table DispatchOrd_Running(
    id int comment "自增主键",
    DispatchOrdIDDt bigint comment '调度单ID',
    DispatchOrdState int comment '状态',
    DispatchOrdStartDate datetime comment '状态时间',
    DispatchOrdStartOA int comment '操作状态的OA用户ID',
    OA_latitude float comment '更新状态时的纬度',
    OA_longitude float comment '更新状态时的经度',
    OA_address nvarchar(400) comment '更新状态时的地址'
)
```
### 2. Mapper接口
**LegacyTransferSyncMapper.java**
新增方法:
```java
/**
 * æ’入调度单状态变更记录到 DispatchOrd_Running è¡¨
 *
 * @param dispatchOrdID è°ƒåº¦å•ID
 * @param dispatchOrdState çŠ¶æ€ç 
 * @param dispatchOrdStartDate çŠ¶æ€æ—¶é—´
 * @param dispatchOrdStartOA æ“ä½œçŠ¶æ€çš„OA用户ID
 * @param oaLatitude æ›´æ–°çŠ¶æ€æ—¶çš„çº¬åº¦
 * @param oaLongitude æ›´æ–°çŠ¶æ€æ—¶çš„ç»åº¦
 * @param oaAddress æ›´æ–°çŠ¶æ€æ—¶çš„åœ°å€
 * @return å½±å“è¡Œæ•°
 */
int insertDispatchOrdRunning(
    @Param("dispatchOrdID") Long dispatchOrdID,
    @Param("dispatchOrdState") Integer dispatchOrdState,
    @Param("dispatchOrdStartDate") java.util.Date dispatchOrdStartDate,
    @Param("dispatchOrdStartOA") Long dispatchOrdStartOA,
    @Param("oaLatitude") Double oaLatitude,
    @Param("oaLongitude") Double oaLongitude,
    @Param("oaAddress") String oaAddress
);
```
### 3. Mapper XML配置
**LegacyTransferSyncMapper.xml**
```xml
<!-- æ’入调度单状态变更记录到 DispatchOrd_Running è¡¨ -->
<insert id="insertDispatchOrdRunning">
    INSERT INTO DispatchOrd_Running (
        DispatchOrdIDDt,
        DispatchOrdState,
        DispatchOrdStartDate,
        DispatchOrdStartOA,
        OA_latitude,
        OA_longitude,
        OA_address
    ) VALUES (
        #{dispatchOrdID},
        #{dispatchOrdState},
        #{dispatchOrdStartDate},
        #{dispatchOrdStartOA},
        #{oaLatitude},
        #{oaLongitude},
        #{oaAddress}
    )
</insert>
```
### 4. äº‹ä»¶ç›‘听器
**DispatchOrdRunningListener.java**
- **位置**: `ruoyi-system/src/main/java/com/ruoyi/system/listener/`
- **功能**: ç›‘听任务状态变更事件(TaskStatusChangedEvent),自动同步状态变更记录到旧系统
- **特性**:
  - âœ… **异步处理**: ä½¿ç”¨ `@Async` æ³¨è§£ï¼Œä¸é˜»å¡žä¸»ä¸šåŠ¡æµç¨‹
  - âœ… **事件监听**: ä½¿ç”¨ `@EventListener` æ³¨è§£ç›‘听状态变更事件
  - âœ… **智能过滤**: åªå¤„理急救转运任务且已同步到旧系统的任务
  - âœ… **状态转换**: è‡ªåŠ¨å°†æ–°ç³»ç»ŸçŠ¶æ€è½¬æ¢ä¸ºæ—§ç³»ç»ŸçŠ¶æ€ç 
  - âœ… **GPS记录**: è®°å½•状态变更时的GPS位置和地址信息
  - âœ… **容错处理**: å¼‚常不影响主业务流程
## ä¸‰ã€å·¥ä½œæµç¨‹
```mermaid
graph TB
    A[转运任务状态变更] --> B[SysTaskServiceImpl.changeTaskStatus]
    B --> C[发布TaskStatusChangedEvent事件]
    C --> D[DispatchOrdRunningListener监听]
    D --> E{是否启用旧系统同步?}
    E -->|否| F[跳过同步]
    E -->|是| G{是否急救转运任务?}
    G -->|否| F
    G -->|是| H{调度单是否已同步?}
    H -->|否| F
    H -->|是| I[转换状态码]
    I --> J{状态需要同步?}
    J -->|否| F
    J -->|是| K[插入DispatchOrd_Running表]
    K --> L[记录日志]
```
## å››ã€çŠ¶æ€æ˜ å°„å…³ç³»
新系统状态 â†’ æ—§ç³»ç»ŸçŠ¶æ€ç æ˜ å°„ï¼š
| æ–°ç³»ç»ŸçŠ¶æ€ | TaskStatus枚举 | æ—§ç³»ç»ŸçŠ¶æ€ç  | æ—§ç³»ç»ŸçŠ¶æ€æè¿° |
|-----------|---------------|-------------|---------------|
| å‡ºå‘中 | DEPARTING | 4 | å·²å‡ºè½¦ï¼ˆåŽ»æŽ¥æ‚£è€…é€”ä¸­ï¼‰ |
| ä»»åС䏭 | IN_PROGRESS | 6 | å·²å‡ºè½¦ï¼ˆæœåŠ¡ä¸­ï¼‰ |
| è¿”程中 | RETURNING | 7 | å·²é€è¾¾ï¼ˆå›žç¨‹ä¸­ï¼‰ |
| å·²å®Œæˆ | COMPLETED | 8 | å·²è¿”回 |
| å·²å–消 | CANCELLED | 10 | å–消 |
| å¾…处理 | PENDING | - | ä¸åŒæ­¥ |
| å·²åˆ°è¾¾ | ARRIVED | - | ä¸åŒæ­¥ |
## äº”、同步条件
满足以下**所有条件**时才会同步状态变更记录:
1. âœ… æ—§ç³»ç»ŸåŒæ­¥åŠŸèƒ½å·²å¯ç”¨ï¼ˆ`legacy.system.enabled=true`)
2. âœ… ä»»åŠ¡ç±»åž‹ä¸ºæ€¥æ•‘è½¬è¿ï¼ˆ`EMERGENCY_TRANSFER`)
3. âœ… ä»»åŠ¡å·²åŒæ­¥åˆ°æ—§ç³»ç»Ÿï¼ˆ`legacy_dispatch_ord_id` ä¸ä¸ºç©ºï¼‰
4. âœ… æ–°çŠ¶æ€éœ€è¦åŒæ­¥åˆ°æ—§ç³»ç»Ÿï¼ˆå‚è§çŠ¶æ€æ˜ å°„è¡¨ï¼‰
## å…­ã€ä½¿ç”¨ç¤ºä¾‹
### 1. è‡ªåŠ¨è§¦å‘ï¼ˆæŽ¨èï¼‰
当转运任务状态发生变更时,系统会自动触发同步:
```java
// åœ¨SysTaskServiceImpl中变更状态
sysTaskService.changeTaskStatus(taskId, TaskStatus.DEPARTING, "司机已出发");
// ç³»ç»Ÿè‡ªåŠ¨ï¼š
// 1. å‘布TaskStatusChangedEvent事件
// 2. DispatchOrdRunningListener监听并处理
// 3. æ’入记录到DispatchOrd_Running表
```
### 2. æ—¥å¿—示例
**成功同步的日志**:
```
2025-12-05 14:30:15 INFO  DispatchOrdRunningListener - æ”¶åˆ°ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œå‡†å¤‡åŒæ­¥åˆ°DispatchOrd_Running,任务ID:1001,旧状态:PENDING,新状态:DEPARTING
2025-12-05 14:30:15 INFO  DispatchOrdRunningListener - å¼€å§‹åŒæ­¥çŠ¶æ€å˜æ›´è®°å½•åˆ°DispatchOrd_Running,DispatchOrdID: 12345, çŠ¶æ€ç : 4, çŠ¶æ€æè¿°: å·²å‡ºè½¦ï¼ˆåŽ»æŽ¥æ‚£è€…é€”ä¸­ï¼‰
2025-12-05 14:30:15 INFO  DispatchOrdRunningListener - çŠ¶æ€å˜æ›´è®°å½•å·²åŒæ­¥åˆ°DispatchOrd_Running,DispatchOrdID: 12345, çŠ¶æ€ç : 4 (已出车(去接患者途中)), GPS: [113.264385, 23.12911], åœ°å€: å¹¿å·žå¸‚越秀区XXè·¯XX号
```
**跳过同步的日志**:
```
2025-12-05 14:30:15 DEBUG DispatchOrdRunningListener - è°ƒåº¦å•未同步到旧系统,跳过DispatchOrd_Running同步,任务ID: 1001
```
## ä¸ƒã€æ•°æ®è®°å½•内容
每次状态变更会记录以下信息:
| å­—段 | è¯´æ˜Ž | æ•°æ®æ¥æº |
|-----|------|---------|
| DispatchOrdIDDt | è°ƒåº¦å•ID | sys_task_emergency.legacy_dispatch_ord_id |
| DispatchOrdState | çŠ¶æ€ç  | ä»Žæ–°ç³»ç»ŸTaskStatus转换而来 |
| DispatchOrdStartDate | çŠ¶æ€æ—¶é—´ | å½“前时间 |
| DispatchOrdStartOA | æ“ä½œäººID | TaskStatusChangedEvent.creatorId |
| OA_latitude | çº¬åº¦ | TaskStatusChangedEvent.latitude |
| OA_longitude | ç»åº¦ | TaskStatusChangedEvent.longitude |
| OA_address | åœ°å€ | TaskStatusChangedEvent.address |
## å…«ã€ç›‘控和调试
### 1. æŸ¥çœ‹çŠ¶æ€å˜æ›´è®°å½•
```sql
-- SQL Server中查询最近的状态变更记录
SELECT TOP 20
    DispatchOrdIDDt,
    DispatchOrdState,
    DispatchOrdStartDate,
    DispatchOrdStartOA,
    OA_latitude,
    OA_longitude,
    OA_address
FROM DispatchOrd_Running
ORDER BY DispatchOrdStartDate DESC
```
### 2. æŸ¥è¯¢ç‰¹å®šè°ƒåº¦å•的状态变更历史
```sql
-- æŸ¥è¯¢æŸä¸ªè°ƒåº¦å•的所有状态变更记录
SELECT
    DispatchOrdState,
    DispatchOrdStartDate,
    OA_latitude,
    OA_longitude,
    OA_address
FROM DispatchOrd_Running
WHERE DispatchOrdIDDt = 12345
ORDER BY DispatchOrdStartDate DESC
```
### 3. ç»Ÿè®¡ä»Šæ—¥çŠ¶æ€å˜æ›´æ¬¡æ•°
```sql
-- ç»Ÿè®¡ä»Šæ—¥å„状态的变更次数
SELECT
    DispatchOrdState,
    CASE DispatchOrdState
        WHEN 4 THEN '已出车(去接患者途中)'
        WHEN 6 THEN '已出车(服务中)'
        WHEN 7 THEN '已送达(回程中)'
        WHEN 8 THEN '已返回'
        WHEN 10 THEN '取消'
        ELSE '其他'
    END AS StateName,
    COUNT(*) AS ChangeCount
FROM DispatchOrd_Running
WHERE CONVERT(DATE, DispatchOrdStartDate) = CONVERT(DATE, GETDATE())
GROUP BY DispatchOrdState
ORDER BY DispatchOrdState
```
## ä¹ã€æž¶æž„优势
### 1. å®Œå…¨è§£è€¦
- ä¸šåŠ¡é€»è¾‘ä¸ŽåŒæ­¥é€»è¾‘åˆ†ç¦»
- çŠ¶æ€å˜æ›´ä¸ä¾èµ–æ—§ç³»ç»ŸåŒæ­¥ç»“æžœ
- å¯ç‹¬ç«‹æµ‹è¯•和维护
### 2. å¼‚步处理
- ä¸é˜»å¡žä¸»ä¸šåŠ¡æµç¨‹
- æé«˜ç³»ç»Ÿå“åº”速度
- å®¹é”™èƒ½åŠ›å¼º
### 3. è‡ªåŠ¨åŒ–
- æ— éœ€æ‰‹åŠ¨è§¦å‘
- å®žæ—¶åŒæ­¥çŠ¶æ€å˜æ›´
- å‡å°‘人工操作
### 4. å¯è¿½æº¯æ€§
- å®Œæ•´è®°å½•每次状态变更
- åŒ…含GPS位置信息
- ä¾¿äºŽé—®é¢˜æŽ’查和审计
## åã€æ³¨æ„äº‹é¡¹
### 1. æ•°æ®æºé…ç½®
确保 `LegacyTransferSyncMapper` ä½¿ç”¨SQL Server数据源:
```java
@DataSource(DataSourceType.SQLSERVER)
public interface LegacyTransferSyncMapper {
    // ...
}
```
### 2. å¼‚步线程池
异步处理依赖线程池配置(AsyncConfig.java):
```java
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        // ...
    }
}
```
### 3. å¼‚常处理
监听器中的异常不会影响主业务:
- è®°å½•详细日志
- ä¸æŠ›å‡ºå¼‚常
- ä¸å½±å“çŠ¶æ€å˜æ›´æ“ä½œ
### 4. æ€§èƒ½ä¼˜åŒ–
- ä½¿ç”¨ `@Async` å¼‚步处理
- åªåŒæ­¥å¿…要的状态变更
- æ™ºèƒ½è¿‡æ»¤ä¸éœ€è¦åŒæ­¥çš„任务
## åä¸€ã€æµ‹è¯•验证
### 1. æµ‹è¯•步骤
**步骤1:创建并同步转运任务**
```java
// åˆ›å»ºæ€¥æ•‘转运任务
Long taskId = sysTaskService.insertSysTask(createVO);
// ç¡®ä¿ä»»åŠ¡å·²åŒæ­¥åˆ°æ—§ç³»ç»Ÿ
legacySystemSyncService.syncDispatchOrderToLegacy(taskId);
```
**步骤2:变更任务状态**
```java
// å˜æ›´çŠ¶æ€ä¸º"出发中"
sysTaskService.changeTaskStatus(taskId, TaskStatus.DEPARTING, "司机已出发");
```
**步骤3:验证SQL Server记录**
```sql
-- æŸ¥è¯¢DispatchOrd_Running表
SELECT * FROM DispatchOrd_Running
WHERE DispatchOrdIDDt = (
    SELECT legacy_dispatch_ord_id
    FROM sys_task_emergency
    WHERE task_id = ?
)
ORDER BY DispatchOrdStartDate DESC
```
### 2. é¢„期结果
| æµ‹è¯•场景 | é¢„期结果 |
|---------|---------|
| æ€¥æ•‘转运任务状态变更 | æ’入记录到DispatchOrd_Running |
| éžæ€¥æ•‘转运任务状态变更 | ä¸æ’入记录 |
| æœªåŒæ­¥çš„任务状态变更 | ä¸æ’入记录 |
| ä¸éœ€è¦åŒæ­¥çš„状态(PENDING) | ä¸æ’入记录 |
| æ—§ç³»ç»ŸåŒæ­¥å·²ç¦ç”¨ | ä¸æ’入记录 |
## åäºŒã€ç›¸å…³æ–‡ä»¶æ¸…单
### æ–°å¢žæ–‡ä»¶
- `DispatchOrdRunningListener.java` - çŠ¶æ€å˜æ›´ç›‘å¬å™¨
### ä¿®æ”¹æ–‡ä»¶
- `LegacyTransferSyncMapper.java` - æ·»åŠ insertDispatchOrdRunning方法
- `LegacyTransferSyncMapper.xml` - æ·»åŠ æ’å…¥SQL
### ä¾èµ–文件(无需修改)
- `SysTaskServiceImpl.java` - å‘布状态变更事件
- `TaskStatusChangedEvent.java` - çŠ¶æ€å˜æ›´äº‹ä»¶
- `TaskStatusPushConverter.java` - çŠ¶æ€è½¬æ¢å·¥å…·
- `DispatchRunning.sql` - æ•°æ®åº“表结构定义
## åä¸‰ã€æ•…障排查
### é—®é¢˜1:状态变更没有同步到旧系统
**排查步骤**:
1. æ£€æŸ¥æ—§ç³»ç»ŸåŒæ­¥æ˜¯å¦å¯ç”¨ï¼š`legacy.system.enabled=true`
2. æ£€æŸ¥ä»»åŠ¡æ˜¯å¦ä¸ºæ€¥æ•‘è½¬è¿ä»»åŠ¡
3. æ£€æŸ¥è°ƒåº¦å•是否已同步:`legacy_dispatch_ord_id` ä¸ä¸ºç©º
4. æŸ¥çœ‹æ—¥å¿—确认是否有异常
5. æ£€æŸ¥çŠ¶æ€æ˜¯å¦éœ€è¦åŒæ­¥ï¼ˆå‚è€ƒçŠ¶æ€æ˜ å°„è¡¨ï¼‰
### é—®é¢˜2:日志中出现异常
**常见原因**:
- SQL Server连接失败
- æ•°æ®æºé…ç½®é”™è¯¯
- å­—段类型不匹配
**解决方法**:
- æ£€æŸ¥æ•°æ®æºé…ç½®
- éªŒè¯SQL Server连接
- æŸ¥çœ‹è¯¦ç»†é”™è¯¯æ—¥å¿—
### é—®é¢˜3:性能问题
**优化建议**:
- è°ƒæ•´çº¿ç¨‹æ± é…ç½®
- æ£€æŸ¥SQL Server性能
- ä¼˜åŒ–数据库索引
## åå››ã€æ€»ç»“
本功能通过Spring事件驱动机制,实现了转运任务状态变更记录到旧系统的自动同步,具有以下特点:
- âœ… **自动化**:状态变更自动同步,无需手动操作
- âœ… **解耦**:业务逻辑与同步逻辑完全分离
- âœ… **异步**:不影响主业务性能
- âœ… **完整**:记录GPS位置等详细信息
- âœ… **可靠**:容错处理,异常不影响主流程
- âœ… **可追溯**:完整的状态变更历史记录
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -1,7 +1,11 @@
package com.ruoyi.web.controller.task;
import java.util.List;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.system.service.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +20,8 @@
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskLog;
@@ -25,10 +31,9 @@
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.service.ISysTaskService;
import com.ruoyi.system.service.IVehicleInfoService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.StringUtils;
/**
 * ä»»åŠ¡ç®¡ç†Controller
@@ -45,6 +50,17 @@
    
    @Autowired
    private IVehicleInfoService vehicleInfoService;
    @Autowired
    private ISysUserService userService;
    @Autowired
    private ISysDeptService deptService;
    @Autowired
    @Qualifier("tiandituMapService")
    private IMapService mapService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†åˆ—è¡¨ï¼ˆåŽå°ç®¡ç†ç«¯ï¼‰
@@ -60,21 +76,52 @@
    /**
     * æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨ï¼ˆAPP端)
     * ä»…显示当前用户相关的任务:
     * 1. å½“前用户所在机构的任务
     * 2. å½“前用户创建的任务
     * 3. åˆ†é…ç»™å½“前用户的任务
     * æ ¹æ®ç”¨æˆ·æƒé™è¿”回不同范围的任务:
     * 1. æœ‰æŸ¥çœ‹æ‰€æœ‰å’¨è¯¢å•权限(canViewAllConsult='1'):返回该用户管理所有分公司下的所有任务单
     * 2. æ— æƒé™ï¼ˆcanViewAllConsult='0'):只返回分配给该用户的单或该用户创建的单
     */
    @GetMapping("/list")
    public TableDataInfo appList(TaskQueryVO queryVO) {
        // åœ¨åŽç«¯è‡ªåŠ¨èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯ï¼Œå®žçŽ°ç»¼åˆæŸ¥è¯¢
        // èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯
        Long currentUserId = getUserId();
        Long currentDeptId = getDeptId();
        SysUser currentUser = userService.selectUserById(currentUserId);
        
        // APP端强制使用当前登录用户信息进行过滤
        queryVO.setDeptId(currentDeptId);
        queryVO.setCreatorId(currentUserId);
        queryVO.setAssigneeId(currentUserId);
        if (currentUser == null) {
            return getDataTable(new java.util.ArrayList<>());
        }
        // åˆ¤æ–­ç”¨æˆ·æ˜¯å¦æœ‰æŸ¥çœ‹æ‰€æœ‰å’¨è¯¢å•的权限
        String canViewAllConsult = currentUser.getCanViewAllConsult();
        if ("1".equals(canViewAllConsult)) {
            // æœ‰æƒé™ï¼šè¿”回该用户管理所有分公司下的所有任务单
            // 1. èŽ·å–ç”¨æˆ·ç®¡ç†çš„åˆ†å…¬å¸åˆ—è¡¨
            List<SysDept> branchCompanies = deptService.computeBranchCompaniesForUser(currentUser);
            if (branchCompanies != null && !branchCompanies.isEmpty()) {
                // 2. æå–所有分公司ID
                List<Long> deptIds = new java.util.ArrayList<>();
                for (SysDept dept : branchCompanies) {
                    deptIds.add(dept.getDeptId());
                }
                // 3. è®¾ç½®æŸ¥è¯¢æ¡ä»¶ä¸ºåˆ†å…¬å¸ID列表(SQL会自动包含所有子部门)
                queryVO.setDeptIds(deptIds);
                // æ¸…空创建人和执行人过滤条件,返回这些分公司下的所有任务
                queryVO.setCreatorId(null);
                queryVO.setAssigneeId(null);
            } else {
                // å¦‚果没有找到分公司,返回空列表
                return getDataTable(new java.util.ArrayList<>());
            }
        } else {
            // æ— æƒé™ï¼šåªè¿”回分配给该用户的单或该用户创建的单
            // æ¸…空deptId和deptIds,使用creatorId和assigneeId进行OR查询
            queryVO.setDeptId(null);
            queryVO.setDeptIds(null);
            queryVO.setCreatorId(currentUserId);
            queryVO.setAssigneeId(currentUserId);
        }
        
        startPage();
        List<SysTask> list = sysTaskService.selectSysTaskList(queryVO);
@@ -214,6 +261,8 @@
        
        // å¦‚果包含GPS位置信息,使用带位置的方法
        if (request.getLatitude() != null && request.getLongitude() != null) {
           String address= mapService.reverseGeocoding(request.getLongitude(), request.getLatitude());
           request.setLocationAddress(address);
            SysTaskLog locationLog = new SysTaskLog();
            locationLog.setLatitude(request.getLatitude());
            locationLog.setLongitude(request.getLongitude());
@@ -225,10 +274,10 @@
            locationLog.setAltitude(request.getAltitude());
            locationLog.setSpeed(request.getSpeed());
            locationLog.setHeading(request.getHeading());
            return toAjax(sysTaskService.changeTaskStatusWithLocation(taskId, newStatus, request.getRemark(), locationLog));
        }
        return toAjax(sysTaskService.changeTaskStatus(taskId, newStatus, request.getRemark()));
    }
@@ -290,6 +339,20 @@
    }
    /**
     * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘ï¼ˆAPP端)
     * æ£€æŸ¥ï¼š
     * 1. è½¦è¾†æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     * 2. æ‰§è¡Œäººå‘˜æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     *
     * @param taskId ä»»åŠ¡ID
     * @return æ ¡éªŒç»“æžœ
     */
    @GetMapping("/{taskId}/check-depart")
    public AjaxResult checkTaskCanDepart(@PathVariable Long taskId) {
        return sysTaskService.checkTaskCanDepart(taskId);
    }
    /**
     * åˆ†é…ä»»åŠ¡è¯·æ±‚å¯¹è±¡
     */
    public static class AssignTaskRequest {
ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
    basename: i18n/messages
  profiles:
    # çŽ¯å¢ƒ dev|test|prod
    active: prod
    active: dev
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -103,6 +103,10 @@
    
    /** å¾®ä¿¡æ˜µç§° */
    private String wechatNickname;
    /** æ˜¯å¦å¯æŸ¥çœ‹æ‰€æœ‰å’¨è¯¢å•(0否 1是) */
    @Excel(name = "可查看所有咨询单", readConverterExp = "0=否,1=是")
    private String canViewAllConsult;
@@ -364,6 +368,15 @@
        this.wechatNickname = wechatNickname;
    }
    
    public String getCanViewAllConsult()
    {
        return canViewAllConsult;
    }
    public void setCanViewAllConsult(String canViewAllConsult)
    {
        this.canViewAllConsult = canViewAllConsult;
    }
    @Override
@@ -393,6 +406,7 @@
            .append("openId", getOpenId())
            .append("unionId", getUnionId())
            .append("wechatNickname", getWechatNickname())
            .append("canViewAllConsult", getCanViewAllConsult())
            .toString();
    }
ruoyi-system/src/main/java/com/ruoyi/system/domain/UserSyncDTO.java
@@ -22,6 +22,9 @@
    /** ç”¨æˆ·å¯ç”¨åˆ†å…¬å¸ç¼–码列表(来源于OA_User.OA_OrderClass) */
    private String oaOrderClass;
    /** æ˜¯å¦å¯æŸ¥çœ‹æ‰€æœ‰å’¨è¯¢å•(0否 1是) */
    private String canViewAllConsult;
    /** ç”¨æˆ·æ€§åˆ«ï¼ˆ0=男,1=女,2=未知) */
    private String sex;
@@ -111,6 +114,16 @@
    {
        this.oaOrderClass = oaOrderClass;
    }
    public String getCanViewAllConsult()
    {
        return canViewAllConsult;
    }
    public void setCanViewAllConsult(String canViewAllConsult)
    {
        this.canViewAllConsult = canViewAllConsult;
    }
    @Override
    public String toString()
@@ -124,6 +137,7 @@
                ", email='" + email + '\'' +
                ", phonenumber='" + phonenumber + '\'' +
                ", oaOrderClass='" + oaOrderClass + '\'' +
                ", canViewAllConsult='" + canViewAllConsult + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskQueryVO.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.domain.vo;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
@@ -33,6 +34,9 @@
    /** å½’属部门ID */
    private Long deptId;
    /** å½’属部门ID列表(用于查询多个分公司的任务) */
    private List<Long> deptIds;
    /** è®¡åˆ’开始时间-开始 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -108,6 +112,14 @@
    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }
    public List<Long> getDeptIds() {
        return deptIds;
    }
    public void setDeptIds(List<Long> deptIds) {
        this.deptIds = deptIds;
    }
    public Date getPlannedStartTimeBegin() {
        return plannedStartTimeBegin;
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskStatusChangedEvent.java
@@ -1,5 +1,8 @@
package com.ruoyi.system.event;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -8,6 +11,7 @@
 * @author ruoyi
 * @date 2025-10-25
 */
public class TaskStatusChangedEvent extends TaskEvent {
    
    private static final long serialVersionUID = 1L;
@@ -30,10 +34,18 @@
    /** åˆ›å»ºäººID */
    private Long creatorId;
    private Double longitude;
    /** çº¬åº¦ */
    private Double latitude;
    private String address;
    public TaskStatusChangedEvent(Object source, Long taskId, String taskCode,
                                 String oldStatus, String newStatus,
                                 String oldStatusDesc, String newStatusDesc,
                                 List<Long> assigneeIds, Long creatorId) {
                                 List<Long> assigneeIds, Long creatorId,Long operationId, Double longitude,
                                  Double latitude,String  address) {
        super(source, taskId, taskCode);
        this.oldStatus = oldStatus;
        this.newStatus = newStatus;
@@ -41,8 +53,32 @@
        this.newStatusDesc = newStatusDesc;
        this.assigneeIds = assigneeIds;
        this.creatorId = creatorId;
        this.longitude = longitude;
        this.latitude = latitude;
        this.address = address;
        this.setOperatorId(operationId);
    }
    public Double getLongitude() {
        return longitude;
    }
    public void setLongitude(Double longitude) {
        this.longitude = longitude;
    }
    public Double getLatitude() {
        return latitude;
    }
    public void setLatitude(Double latitude) {
        this.latitude = latitude;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getOldStatus() {
        return oldStatus;
    }
ruoyi-system/src/main/java/com/ruoyi/system/listener/DispatchOrdRunningListener.java
New file
@@ -0,0 +1,195 @@
package com.ruoyi.system.listener;
import com.ruoyi.common.config.LegacySystemConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.enums.TaskStatus;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import com.ruoyi.system.mapper.LegacyTransferSyncMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.utils.TaskStatusPushConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
 * è°ƒåº¦å•状态变更记录监听器
 * ç›‘听任务状态变更事件,并将状态变更记录同步到SQL Server的DispatchOrd_Running表
 *
 * @author ruoyi
 * @date 2025-12-05
 */
@Component
public class DispatchOrdRunningListener {
    private static final Logger log = LoggerFactory.getLogger(DispatchOrdRunningListener.class);
    @Autowired
    private LegacySystemConfig legacyConfig;
    @Autowired
    private SysTaskMapper sysTaskMapper;
    @Autowired
    private SysTaskEmergencyMapper sysTaskEmergencyMapper;
    @Autowired
    private LegacyTransferSyncMapper legacyTransferSyncMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    /**
     * ç›‘听任务状态变更事件
     *
     * @param event ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶
     */
    @Async
    @EventListener
    public void handleTaskStatusChangedEvent(TaskStatusChangedEvent event) {
        try {
            log.info("收到任务状态变更事件,准备同步到DispatchOrd_Running,任务ID:{},旧状态:{},新状态:{}",
                    event.getTaskId(), event.getOldStatus(), event.getNewStatus());
            // æ£€æŸ¥æ—§ç³»ç»ŸåŒæ­¥æ˜¯å¦å¯ç”¨
            if (!legacyConfig.isEnabled()) {
                log.debug("旧系统同步已禁用,跳过DispatchOrd_Running同步,任务ID: {}", event.getTaskId());
                return;
            }
            // æŸ¥è¯¢ä»»åŠ¡ä¿¡æ¯
            SysTask task = sysTaskMapper.selectSysTaskByTaskId(event.getTaskId());
            if (task == null) {
                log.error("任务不存在,任务ID: {}", event.getTaskId());
                return;
            }
            // åªå¤„理急救转运任务
            if (!"EMERGENCY_TRANSFER".equals(task.getTaskType())) {
                log.debug("非急救转运任务,跳过DispatchOrd_Running同步,任务ID: {}", event.getTaskId());
                return;
            }
            // æŸ¥è¯¢æ€¥æ•‘转运扩展信息
            SysTaskEmergency emergency = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(event.getTaskId());
            if (emergency == null) {
                log.error("急救转运扩展信息不存在,任务ID: {}", event.getTaskId());
                return;
            }
            // å¿…须已经同步过调度单
            if (emergency.getLegacyDispatchOrdId() == null || emergency.getLegacyDispatchOrdId() <= 0) {
                log.debug("调度单未同步到旧系统,跳过DispatchOrd_Running同步,任务ID: {}", event.getTaskId());
                return;
            }
            // èŽ·å–æ–°ç³»ç»ŸçŠ¶æ€
            TaskStatus newTaskStatus = TaskStatus.getByCode(event.getNewStatus());
            if (newTaskStatus == null) {
                log.error("无效的任务状态,任务ID: {}, çŠ¶æ€ç : {}", event.getTaskId(), event.getNewStatus());
                return;
            }
            SysUser user= sysUserMapper.selectUserById(event.getOperatorId());
            if(user==null){
                log.error("操作人不存在,任务ID: {}, æ“ä½œäººID: {}", event.getTaskId(), event.getOperatorId());
                return;
            }
            Integer oaUserId=user.getOaUserId();
            // è½¬æ¢ä¸ºæ—§ç³»ç»ŸçŠ¶æ€ç 
            Integer legacyStatusCode = TaskStatusPushConverter.convertToLegacyStatus(newTaskStatus);
            if (legacyStatusCode == null) {
                log.debug("任务状态不需要同步到旧系统,任务ID: {}, çŠ¶æ€: {}",
                    event.getTaskId(), newTaskStatus.getInfo());
                return;
            }
            // æ’入状态变更记录到DispatchOrd_Running表
            syncDispatchOrdRunning(
                emergency.getLegacyDispatchOrdId(),
                legacyStatusCode,
                new Date(), // ä½¿ç”¨å½“前时间作为状态变更时
                    oaUserId.longValue(),
                event.getLatitude(),
                event.getLongitude(),
                event.getAddress()
            );
        } catch (Exception e) {
            log.error("处理任务状态变更事件并同步到DispatchOrd_Running失败,任务ID: {}", event.getTaskId(), e);
            // ä¸æŠ›å‡ºå¼‚常,避免影响主流程
        }
    }
    /**
     * åŒæ­¥çŠ¶æ€å˜æ›´è®°å½•åˆ°DispatchOrd_Running表
     *
     * @param dispatchOrdId è°ƒåº¦å•ID
     * @param statusCode çŠ¶æ€ç 
     * @param statusTime çŠ¶æ€æ—¶é—´
     * @param operatorId æ“ä½œäººID
     * @param latitude çº¬åº¦
     * @param longitude ç»åº¦
     * @param address åœ°å€
     */
    private void syncDispatchOrdRunning(Long dispatchOrdId, Integer statusCode,
                                        Date statusTime, Long operatorId,
                                        Double latitude, Double longitude, String address) {
        try {
            // åˆ¤æ–­æ˜¯å¦æœ‰GPS信息
            boolean hasGps = (latitude != null && longitude != null);
            if (hasGps) {
                log.info("开始同步状态变更记录到DispatchOrd_Running,DispatchOrdID: {}, çŠ¶æ€ç : {} ({}), GPS: [{}, {}]",
                    dispatchOrdId, statusCode, TaskStatusPushConverter.getLegacyStatusDescription(statusCode),
                    latitude, longitude);
            } else {
                log.info("开始同步状态变更记录到DispatchOrd_Running,DispatchOrdID: {}, çŠ¶æ€ç : {} ({}), æ— GPS信息",
                    dispatchOrdId, statusCode, TaskStatusPushConverter.getLegacyStatusDescription(statusCode));
            }
            // è°ƒç”¨Mapper插入记录
            int rows = legacyTransferSyncMapper.insertDispatchOrdRunning(
                dispatchOrdId,
                statusCode,
                statusTime,
                operatorId,
                latitude,
                longitude,
                address
            );
            if (rows > 0) {
                if (hasGps) {
                    log.info("状态变更记录已同步到DispatchOrd_Running,DispatchOrdID: {}, çŠ¶æ€ç : {} ({}), GPS: [{}, {}], åœ°å€: {}",
                        dispatchOrdId,
                        statusCode,
                        TaskStatusPushConverter.getLegacyStatusDescription(statusCode),
                        latitude,
                        longitude,
                        address != null ? address : "无");
                } else {
                    log.info("状态变更记录已同步到DispatchOrd_Running,DispatchOrdID: {}, çŠ¶æ€ç : {} ({}), æ— GPS信息",
                        dispatchOrdId,
                        statusCode,
                        TaskStatusPushConverter.getLegacyStatusDescription(statusCode));
                }
            } else {
                log.warn("同步状态变更记录失败,未插入数据,DispatchOrdID: {}", dispatchOrdId);
            }
        } catch (Exception e) {
            log.error("同步状态变更记录到DispatchOrd_Running异常,DispatchOrdID: {}, çŠ¶æ€ç : {}",
                dispatchOrdId, statusCode, e);
            // ä¸æŠ›å‡ºå¼‚常,避免影响主流程
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
@@ -92,6 +92,8 @@
        }
    }
    /**
     * ç›‘听任务分配事件
     * 
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java
@@ -73,4 +73,26 @@
     * @return ç§‘室名称
     */
    String selectDepartmentNameByDeptID(@Param("deptID") String deptID);
    /**
     * æ’入调度单状态变更记录到 DispatchOrd_Running è¡¨
     *
     * @param dispatchOrdID è°ƒåº¦å•ID
     * @param dispatchOrdState çŠ¶æ€ç 
     * @param dispatchOrdStartDate çŠ¶æ€æ—¶é—´
     * @param dispatchOrdStartOA æ“ä½œçŠ¶æ€çš„OA用户ID
     * @param oaLatitude æ›´æ–°çŠ¶æ€æ—¶çš„çº¬åº¦
     * @param oaLongitude æ›´æ–°çŠ¶æ€æ—¶çš„ç»åº¦
     * @param oaAddress æ›´æ–°çŠ¶æ€æ—¶çš„åœ°å€
     * @return å½±å“è¡Œæ•°
     */
    int insertDispatchOrdRunning(
        @Param("dispatchOrdID") Long dispatchOrdID,
        @Param("dispatchOrdState") Integer dispatchOrdState,
        @Param("dispatchOrdStartDate") java.util.Date dispatchOrdStartDate,
        @Param("dispatchOrdStartOA") Long dispatchOrdStartOA,
        @Param("oaLatitude") Double oaLatitude,
        @Param("oaLongitude") Double oaLongitude,
        @Param("oaAddress") String oaAddress
    );
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -273,4 +273,15 @@
     */
    public boolean existsByLegacyDispatchOrdId(Long legacyDispatchOrdId);
    /**
     * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘
     * æ£€æŸ¥ï¼š
     * 1. è½¦è¾†æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     * 2. æ‰§è¡Œäººå‘˜æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     *
     * @param taskId ä»»åŠ¡ID
     * @return AjaxResult æ ¡éªŒç»“果,包含 valid å’Œ conflicts ä¿¡æ¯
     */
    public com.ruoyi.common.core.domain.AjaxResult checkTaskCanDepart(Long taskId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
@@ -89,6 +89,8 @@
    @Autowired
    private ITaskAttachmentService taskAttachmentService;
    
    /**
     * åŒæ­¥æ€¥æ•‘转运任务到旧系统
@@ -342,8 +344,70 @@
                return null;
            }
            // ====== å‰ç½®æ ¡éªŒï¼šç¡®ä¿ä»»åŠ¡æ•°æ®å®Œæ•´ ======
            // 1. æ£€æŸ¥æ˜¯å¦å·²åˆ†é…è½¦è¾†
            List<SysTaskVehicle> taskVehicles = sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId);
            if (taskVehicles == null || taskVehicles.isEmpty()) {
                log.warn("任务未分配车辆,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            // 2. æ£€æŸ¥æ˜¯å¦å·²åˆ†é…æ‰§è¡Œäººå‘˜
            List<SysTaskAssignee> taskAssignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId);
            if (taskAssignees == null || taskAssignees.isEmpty()) {
                log.warn("任务未分配执行人员,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            // 3. æ£€æŸ¥é¢„约时间是否有效(必须大于1970年)
            if (task.getPlannedStartTime() == null) {
                log.warn("任务未设置预约时间,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            // æ£€æŸ¥é¢„约时间是否大于1970-01-01(时间戳0对应1970-01-01 00:00:00)
            long timestamp1970 = 0L;
            if (task.getPlannedStartTime().getTime() <= timestamp1970) {
                log.warn("任务预约时间无效(小于等于1970年),跳过调度单同步,任务ID: {}, é¢„约时间: {}",
                    taskId, task.getPlannedStartTime());
                return null;
            }
            // 4. æ£€æŸ¥è½¬å‡ºåŒ»é™¢ä¿¡æ¯
            if (StringUtils.isEmpty(emergency.getHospitalOutName())) {
                log.warn("任务未设置转出医院,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            if (StringUtils.isEmpty(emergency.getHospitalOutAddress())) {
                log.warn("任务未设置转出医院地址,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            // 5. æ£€æŸ¥è½¬å…¥åŒ»é™¢ä¿¡æ¯
            if (StringUtils.isEmpty(emergency.getHospitalInName())) {
                log.warn("任务未设置转入医院,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            if (StringUtils.isEmpty(emergency.getHospitalInAddress())) {
                log.warn("任务未设置转入医院地址,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            // 6. æ£€æŸ¥æ‚£è€…基本信息
            if (StringUtils.isEmpty(emergency.getPatientName())) {
                log.warn("任务未设置患者姓名,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            if (StringUtils.isEmpty(emergency.getPatientPhone())) {
                log.warn("任务未设置患者电话,跳过调度单同步,任务ID: {}", taskId);
                return null;
            }
            log.info("任务数据校验通过,开始同步调度单,任务ID: {}", taskId);
            // æ›´æ–°åŒæ­¥çŠ¶æ€ä¸ºåŒæ­¥ä¸­
            emergency.setDispatchSyncStatus(1);
@@ -1181,7 +1245,47 @@
                return false;
            }
            
            log.info("开始重新同步调度单,任务ID: {}, DispatchOrdID: {}", taskId, emergency.getLegacyDispatchOrdId());
            // ====== å‰ç½®æ ¡éªŒï¼šç¡®ä¿ä»»åŠ¡æ•°æ®å®Œæ•´ ======
            // 1. æ£€æŸ¥æ˜¯å¦å·²åˆ†é…è½¦è¾†
            List<SysTaskVehicle> taskVehicles = sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId);
            if (taskVehicles == null || taskVehicles.isEmpty()) {
                log.warn("任务未分配车辆,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            // 2. æ£€æŸ¥æ˜¯å¦å·²åˆ†é…æ‰§è¡Œäººå‘˜
            List<SysTaskAssignee> taskAssignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId);
            if (taskAssignees == null || taskAssignees.isEmpty()) {
                log.warn("任务未分配执行人员,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            // 3. æ£€æŸ¥é¢„约时间是否有效
            if (task.getPlannedStartTime() == null || task.getPlannedStartTime().getTime() <= 0L) {
                log.warn("任务预约时间无效,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            // 4. æ£€æŸ¥è½¬å‡ºåŒ»é™¢ä¿¡æ¯
            if (StringUtils.isEmpty(emergency.getHospitalOutName()) || StringUtils.isEmpty(emergency.getHospitalOutAddress())) {
                log.warn("任务转出医院信息不完整,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            // 5. æ£€æŸ¥è½¬å…¥åŒ»é™¢ä¿¡æ¯
            if (StringUtils.isEmpty(emergency.getHospitalInName()) || StringUtils.isEmpty(emergency.getHospitalInAddress())) {
                log.warn("任务转入医院信息不完整,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            // 6. æ£€æŸ¥æ‚£è€…基本信息
            if (StringUtils.isEmpty(emergency.getPatientName()) || StringUtils.isEmpty(emergency.getPatientPhone())) {
                log.warn("任务患者信息不完整,跳过调度单重新同步,任务ID: {}", taskId);
                return false;
            }
            log.info("任务数据校验通过,开始重新同步调度单,任务ID: {}, DispatchOrdID: {}", taskId, emergency.getLegacyDispatchOrdId());
            
            // æž„建请求参数(使用相同的参数构建方法)
            Map<String, String> params = buildDispatchOrderParams(task, emergency);
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -170,22 +170,24 @@
                log.error("服务单ID不能为空");
                return false;
            }
            // ç›´æŽ¥æŸ¥è¯¢æŒ‡å®šçš„转运单信息
            List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrdersByIDs(serviceOrdID, dispatchOrdID);
            Map<String, Object> order = transferOrders.get(0);
            // æ£€æŸ¥æ˜¯å¦å·²åŒæ­¥
            if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) {
                log.info("转运单已同步,跳过: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                updateTransferOrder(serviceOrdID, dispatchOrdID, order);
                return true;
            }
            
            // ç›´æŽ¥æŸ¥è¯¢æŒ‡å®šçš„转运单信息
            List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrdersByIDs(serviceOrdID, dispatchOrdID);
            if (transferOrders == null || transferOrders.isEmpty()) {
                log.error("未查询到对应的转运单信息: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                return false;
            }
            Map<String, Object> order = transferOrders.get(0);
            
            // åŒæ­¥å•个转运单
            return syncSingleTransferOrder(serviceOrdID, dispatchOrdID, order);
@@ -294,7 +296,10 @@
            String serviceOrdClass = getStringValue(order,"ServiceOrdClass");
            String serviceOrdNo = getStringValue(order,"ServiceOrdNo");
            Integer oauserId=getIntegerValue(order,"ServiceOrd_NS_ID");
            /**
             * åˆ›å»ºäººID
             */
            Integer oauserId=getIntegerValue(order,"ServiceOrd_CC_ID");
            SysUser sysUser=sysUserService.selectUserByOaUserId(oauserId);
            Long taskCreatorId=sysUser==null?null:sysUser.getUserId();
            String createUserName=sysUser==null?"system":sysUser.getUserName();
@@ -495,9 +500,9 @@
                    hospitalInInfo.setName(hospitalInName);
                }
            }
            String DispatchOrdTraEnd = getStringValue(order, "DispatchOrdTraEnd");
            if(DispatchOrdTraEnd!= null){
                hospitalInInfo.setAddress(DispatchOrdTraEnd);
            String serviceOrdTraEnd = getStringValue(order, "ServiceOrdTraEnd");
            if(serviceOrdTraEnd!= null){
                hospitalInInfo.setAddress(serviceOrdTraEnd);
            }
            //转入床位
            String serviceOrdPtInServices =getStringValue(order, "ServiceOrdPtInServices");
@@ -523,14 +528,15 @@
            // è®¾ç½®ä»·æ ¼å’Œè·ç¦»ä¿¡æ¯
            createTaskVo.setPrice(getBigDecimalValue(order, "ServiceOrdTraTxnPrice"));
            // è·ç¦»ä¿¡æ¯éœ€è¦ä»Žå…¶ä»–字段计算或获取
            // è®¾ç½®æ‰§è¡Œäººä¿¡æ¯
            List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
            createTaskVo.setAssignees(assignees);
            if(!assignees.isEmpty()){
                createTaskVo.setAssigneeId(assignees.get(0).getUserId());
            if(dispatchOrdID!=null) {
                // è®¾ç½®æ‰§è¡Œäººä¿¡æ¯
                List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
                createTaskVo.setAssignees(assignees);
                if (!assignees.isEmpty()) {
                    createTaskVo.setAssigneeId(assignees.get(0).getUserId());
                }
            }
            // è®¾ç½®è½¦è¾†ä¿¡æ¯
            // è½¦è¾†ID需要根据DispatchOrdCarID查询获取
            String carID = getStringValue(order, "DispatchOrdCarID");
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -1024,7 +1024,6 @@
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int changeTaskStatus(Long taskId, TaskStatus newStatus, String remark) {
        return changeTaskStatusWithLocation(taskId, newStatus, remark, null);
    }
@@ -1039,7 +1038,6 @@
     * @return ç»“æžœ
     */
    @Override
    @Transactional
    public int changeTaskStatusWithLocation(Long taskId, TaskStatus newStatus, String remark, SysTaskLog locationLog) {
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (oldTask == null) {
@@ -1091,7 +1089,10 @@
                    .map(SysTaskAssignee::getUserId)
                    .collect(Collectors.toList());
            }
            Long userId=SecurityUtils.getUserId();
            Double lng=locationLog==null?null: locationLog.getLongitude();
            Double lat=locationLog==null?null: locationLog.getLatitude();
            String address=locationLog==null?null: locationLog.getLocationAddress();
            eventPublisher.publishEvent(new TaskStatusChangedEvent(
                this,
                oldTask.getTaskId(),
@@ -1101,7 +1102,11 @@
                oldTaskStatus.getInfo(),
                newStatus.getInfo(),
                assigneeIds,
                oldTask.getCreatorId()
                oldTask.getCreatorId(),
                userId,
                    lng, lat,
                    address
            ));
        }
        
@@ -2262,5 +2267,89 @@
        }
    }
    
    /**
     * æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å¯ä»¥å‡ºå‘
     * æ£€æŸ¥ï¼š
     * 1. è½¦è¾†æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     * 2. æ‰§è¡Œäººå‘˜æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
     *
     * @param taskId ä»»åŠ¡ID
     * @return AjaxResult æ ¡éªŒç»“æžœ
     */
    @Override
    public com.ruoyi.common.core.domain.AjaxResult checkTaskCanDepart(Long taskId) {
        // èŽ·å–ä»»åŠ¡è¯¦æƒ…
        SysTask task = this.getTaskDetail(taskId);
        if (task == null) {
            return com.ruoyi.common.core.domain.AjaxResult.error("任务不存在");
        }
        List<Map<String, Object>> conflicts = new ArrayList<>();
        // 1. æ£€æŸ¥è½¦è¾†æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
        List<SysTaskVehicle> taskVehicles = task.getAssignedVehicles();
        if (taskVehicles != null && !taskVehicles.isEmpty()) {
            for (SysTaskVehicle taskVehicle : taskVehicles) {
                Long vehicleId = taskVehicle.getVehicleId();
                List<SysTask> vehicleActiveTasks = this.checkVehicleActiveTasks(vehicleId);
                // è¿‡æ»¤æŽ‰å½“前任务本身
                vehicleActiveTasks = vehicleActiveTasks.stream()
                    .filter(t -> !t.getTaskId().equals(taskId))
                    .collect(Collectors.toList());
                if (!vehicleActiveTasks.isEmpty()) {
                    for (SysTask activeTask : vehicleActiveTasks) {
                        Map<String, Object> conflict = new HashMap<>();
                        conflict.put("type", "vehicle");
                        conflict.put("vehicleNo", taskVehicle.getVehicleNo());
                        conflict.put("taskId", activeTask.getTaskId());
                        conflict.put("taskCode", activeTask.getTaskCode());
                        conflict.put("taskStatus", activeTask.getTaskStatus());
                        conflict.put("message", String.format("车辆 %s å°šæœ‰æœªå®Œæˆçš„任务 %s,请先完成",
                            taskVehicle.getVehicleNo(), activeTask.getTaskCode()));
                        conflicts.add(conflict);
                    }
                }
            }
        }
        // 2. æ£€æŸ¥æ‰§è¡Œäººå‘˜æ˜¯å¦æœ‰æœªå®Œæˆçš„任务
        List<SysTaskAssignee> assignees = task.getAssignees();
        if (assignees != null && !assignees.isEmpty()) {
            for (SysTaskAssignee assignee : assignees) {
                Long userId = assignee.getUserId();
                // æŸ¥è¯¢è¯¥æ‰§è¡Œäººçš„æ‰€æœ‰æ­£åœ¨è¿›è¡Œä¸­çš„任务(排除PENDING、COMPLETED、CANCELLED)
                List<SysTask> userActiveTasks = this.selectMyTasks(userId).stream()
                    .filter(t -> !TaskStatus.PENDING.getCode().equals(t.getTaskStatus())
                              && !TaskStatus.COMPLETED.getCode().equals(t.getTaskStatus())
                              && !TaskStatus.CANCELLED.getCode().equals(t.getTaskStatus())
                              && !t.getTaskId().equals(taskId)) // è¿‡æ»¤æŽ‰å½“前任务
                    .collect(Collectors.toList());
                if (!userActiveTasks.isEmpty()) {
                    for (SysTask activeTask : userActiveTasks) {
                        Map<String, Object> conflict = new HashMap<>();
                        conflict.put("type", "assignee");
                        conflict.put("userName", assignee.getUserName());
                        conflict.put("taskId", activeTask.getTaskId());
                        conflict.put("taskCode", activeTask.getTaskCode());
                        conflict.put("taskStatus", activeTask.getTaskStatus());
                        conflict.put("message", String.format("执行人 %s å°šæœ‰æ­£åœ¨è¿›è¡Œä¸­çš„任务 %s,请先完成",
                            assignee.getUserName(), activeTask.getTaskCode()));
                        conflicts.add(conflict);
                    }
                }
            }
        }
        // è¿”回结果
        Map<String, Object> result = new HashMap<>();
        result.put("valid", conflicts.isEmpty());
        result.put("conflicts", conflicts);
        return com.ruoyi.common.core.domain.AjaxResult.success(result);
    }
   
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserSyncServiceImpl.java
@@ -223,6 +223,10 @@
        {
            existingUser.setOaOrderClass(dto.getOaOrderClass());
        }
        if (StringUtils.isNotEmpty(dto.getCanViewAllConsult()))
        {
            existingUser.setCanViewAllConsult(dto.getCanViewAllConsult());
        }
        sysUserMapper.updateUser(existingUser);
    }
@@ -239,6 +243,10 @@
        {
            newUser.setOaOrderClass(dto.getOaOrderClass());
        }
        if (StringUtils.isNotEmpty(dto.getCanViewAllConsult()))
        {
            newUser.setCanViewAllConsult(dto.getCanViewAllConsult());
        }
        
        if (deptId != null)
        {
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml
@@ -43,6 +43,7 @@
        <result property="Old_ServiceOrdID_TXT" column="Old_ServiceOrdID_TXT" />
        <result property="ServiceOrdTraDistance" column="ServiceOrdTraDistance" />
        <result property="ServiceOrdApptDate" column="ServiceOrdApptDate" />
        <result property="DispatchOrdState" column="DispatchOrdState" />
    </resultMap>
    
    <!-- æ‰§è¡Œäººç»“果映射 -->
@@ -87,16 +88,17 @@
            b.DispatchOrdActualDate,
            b.DispatchOrdReturnDate,
            b.DispatchOrdTraEnd,
            b.DispatchOrdState,
            b.DispatchOrdID,
            b.DispatchOrdCarID,
            a.ServiceOrdPtServices,
            a.ServiceOrdPtInServices,
            a.ServiceOrdPtName
        FROM ServiceOrder as a 
        INNER JOIN DispatchOrd b on a.ServiceOrdID = b.ServiceOrdIDDt
        WHERE a.ServiceOrdState = 3
        left JOIN DispatchOrd b on a.ServiceOrdID = b.ServiceOrdIDDt
        WHERE a.ServiceOrdState &lt;= 3
            AND a.ServiceOrd_CC_Time > #{startDate} 
            AND b.DispatchOrdState != 0
    </select>
    
    <!-- æ ¹æ®æœåŠ¡å•ID和调度单ID查询转运单数据 -->
@@ -137,13 +139,13 @@
            b.DispatchOrdCarID,
            a.ServiceOrdPtServices,
            a.ServiceOrdPtInServices,
            a.ServiceOrdPtName
            a.ServiceOrdPtName,
            b.DispatchOrdState
        FROM ServiceOrder as a 
        INNER JOIN DispatchOrd b on a.ServiceOrdID = b.ServiceOrdIDDt
        left JOIN DispatchOrd b on a.ServiceOrdID = b.ServiceOrdIDDt
        WHERE a.ServiceOrdID = #{serviceOrdID}
            AND b.DispatchOrdID = #{dispatchOrdID}
            AND a.ServiceOrdState = 3
            AND b.DispatchOrdState != 0
            AND a.ServiceOrdState &lt;=3
    </select>
    
    <!-- æ ¹æ®æœåŠ¡å•ID查询病情信息 -->
@@ -184,4 +186,25 @@
        WHERE vID = #{deptID} AND vtitle = 'HospitalDepartment'
    </select>
    
    <!-- æ’入调度单状态变更记录到 DispatchOrd_Running è¡¨ -->
    <insert id="insertDispatchOrdRunning">
        INSERT INTO DispatchOrd_Running (
            DispatchOrdIDDt,
            DispatchOrdState,
            DispatchOrdStartDate,
            DispatchOrdStartOA
            <if test="oaLatitude != null">,OA_latitude</if>
            <if test="oaLongitude != null">,OA_longitude</if>
            <if test="oaAddress != null and oaAddress != ''">,OA_address</if>
        ) VALUES (
            #{dispatchOrdID},
            #{dispatchOrdState},
            #{dispatchOrdStartDate},
            #{dispatchOrdStartOA}
            <if test="oaLatitude != null">,#{oaLatitude}</if>
            <if test="oaLongitude != null">,#{oaLongitude}</if>
            <if test="oaAddress != null and oaAddress != ''">,#{oaAddress}</if>
        )
    </insert>
</mapper>
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
@@ -88,16 +88,36 @@
            <if test="taskStatus != null  and taskStatus != ''"> and t.task_status = #{taskStatus}</if>
            <if test="vehicleNo != null  and vehicleNo != ''"> and v.vehicle_no like concat('%', #{vehicleNo}, '%')</if>
            <!-- ç»¼åˆæŸ¥è¯¢ï¼šå½“前用户所在机构 OR å½“前用户创建 OR åˆ†é…ç»™å½“前用户 -->
            <if test="(creatorId != null and creatorId != 0) or (assigneeId != null and assigneeId != 0) or (deptId != null and deptId != 0)">
            <if test="(creatorId != null and creatorId != 0) or (assigneeId != null and assigneeId != 0) or (deptId != null and deptId != 0) or (deptIds != null and deptIds.size() > 0)">
                and (
                    <if test="deptId != null and deptId != 0">t.dept_id = #{deptId}</if>
                    <if test="creatorId != null and creatorId != 0">
                    <!-- æŸ¥è¯¢æŒ‡å®šåˆ†å…¬å¸åŠå…¶æ‰€æœ‰å­éƒ¨é—¨çš„任务 -->
                    <if test="deptId != null and deptId != 0">
                        (t.dept_id = #{deptId} OR t.dept_id IN (
                            SELECT dept_id FROM sys_dept
                            WHERE del_flag = '0' AND find_in_set(#{deptId}, ancestors)
                        ))
                    </if>
                    <!-- æŸ¥è¯¢å¤šä¸ªåˆ†å…¬å¸åŠå…¶æ‰€æœ‰å­éƒ¨é—¨çš„任务 -->
                    <if test="deptIds != null and deptIds.size() > 0">
                        <if test="deptId != null and deptId != 0"> or </if>
                        (
                            <foreach collection="deptIds" item="branchDeptId" separator=" OR ">
                                (t.dept_id = #{branchDeptId} OR t.dept_id IN (
                                    SELECT dept_id FROM sys_dept
                                    WHERE del_flag = '0' AND find_in_set(#{branchDeptId}, ancestors)
                                ))
                            </foreach>
                        )
                    </if>
                    <if test="creatorId != null and creatorId != 0">
                        <if test="(deptId != null and deptId != 0) or (deptIds != null and deptIds.size() > 0)"> or </if>
                        t.creator_id = #{creatorId}
                    </if>
                    <if test="assigneeId != null and assigneeId != 0">
                        <if test="(deptId != null and deptId != 0) or (creatorId != null and creatorId != 0)"> or </if>
                        t.assignee_id = #{assigneeId}
                        <if test="(deptId != null and deptId != 0) or (deptIds != null and deptIds.size() > 0) or (creatorId != null and creatorId != 0)"> or </if>
                        t.task_id IN (
                            SELECT task_id FROM sys_task_assignee WHERE user_id = #{assigneeId}
                        )
                    </if>
                )
            </if>
@@ -145,7 +165,12 @@
    <select id="selectMyTasks" parameterType="Long" resultMap="SysTaskResult">
        <include refid="selectSysTaskVo"/>
        where t.del_flag = '0' and (t.creator_id = #{userId} or t.assignee_id = #{userId})
        where t.del_flag = '0' and (
            t.creator_id = #{userId}
            OR t.task_id IN (
                SELECT task_id FROM sys_task_assignee WHERE user_id = #{userId}
            )
        )
        order by 
            CASE t.task_status
                WHEN 'PENDING' THEN 1
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -28,6 +28,7 @@
        <result property="openId"       column="open_id"       />
        <result property="unionId"     column="union_id"     />
        <result property="wechatNickname" column="wechat_nickname" />
        <result property="canViewAllConsult" column="can_view_all_consult" />
        <association property="dept"    javaType="SysDept"         resultMap="deptResult" />
        <collection  property="roles"   javaType="java.util.List"  resultMap="RoleResult" />
    </resultMap>
@@ -52,7 +53,7 @@
    </resultMap>
    
    <sql id="selectUserVo">
        select u.user_id, u.dept_id, u.user_name,u.oa_user_id, u.oa_order_class, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.oa_user_id, u.create_by, u.create_time, u.remark,u.open_id,u.union_id,u.wechat_nickname,
        select u.user_id, u.dept_id, u.user_name,u.oa_user_id, u.oa_order_class, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.oa_user_id, u.create_by, u.create_time, u.remark,u.open_id,u.union_id,u.wechat_nickname,u.can_view_all_consult,
        d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        from sys_user u
@@ -169,6 +170,7 @@
             <if test="status != null and status != ''">status,</if>
             <if test="oaUserId != null">oa_user_id,</if>
             <if test="oaOrderClass != null and oaOrderClass != ''">oa_order_class,</if>
             <if test="canViewAllConsult != null and canViewAllConsult != ''">can_view_all_consult,</if>
             <if test="createBy != null and createBy != ''">create_by,</if>
             <if test="remark != null and remark != ''">remark,</if>
             create_time
@@ -185,6 +187,7 @@
             <if test="status != null and status != ''">#{status},</if>
             <if test="oaUserId != null">#{oaUserId},</if>
             <if test="oaOrderClass != null and oaOrderClass != ''">#{oaOrderClass},</if>
             <if test="canViewAllConsult != null and canViewAllConsult != ''">#{canViewAllConsult},</if>
             <if test="createBy != null and createBy != ''">#{createBy},</if>
             <if test="remark != null and remark != ''">#{remark},</if>
             sysdate()
@@ -204,6 +207,7 @@
             <if test="status != null and status != ''">status = #{status},</if>
             <if test="oaUserId != null">oa_user_id = #{oaUserId},</if>
             <if test="oaOrderClass != null">oa_order_class = #{oaOrderClass},</if>
             <if test="canViewAllConsult != null and canViewAllConsult != ''">can_view_all_consult = #{canViewAllConsult},</if>
             <if test="openId != null and openId != ''">open_id = #{openId},</if>
             <if test="unionId != null and unionId != ''">union_id = #{unionId},</if>
             <if test="loginIp != null and loginIp != ''">login_ip = #{loginIp},</if>
ruoyi-system/src/main/resources/mapper/system/UserSyncMapper.xml
@@ -13,6 +13,7 @@
        <result property="email" column="email" />
        <result property="phonenumber" column="phonenumber" />
        <result property="oaOrderClass" column="OA_OrderClass" />
        <result property="canViewAllConsult" column="can_view_all_consult" />
    </resultMap>
    <!-- æŸ¥è¯¢SQL Server中的OA用户列表 -->
@@ -26,7 +27,11 @@
            OA_gender AS sex,
            OA_email AS email,
            OA_mobile AS phonenumber,
            OA_OrderClass AS OA_OrderClass
            OA_OrderClass AS OA_OrderClass,
            CASE
                WHEN OA_Power LIKE '%,020112,%' THEN '1'
                ELSE '0'
            END AS can_view_all_consult
        FROM OA_User
        WHERE OA_User IS NOT NULL 
          AND OA_Name IS NOT NULL
ruoyi-system/src/test/java/com/ruoyi/system/mapper/SysDeptMapperTest.java
ruoyi-system/src/test/java/com/ruoyi/system/service/LegacySystemHttpsTest.java
File was deleted
ruoyi-system/src/test/java/com/ruoyi/system/service/SysDeptServiceTest.java
ruoyi-ui/src/views/system/user/index.vue
@@ -172,6 +172,16 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="查看所有咨询单">
              <el-radio-group v-model="form.canViewAllConsult">
                <el-radio label="0">否</el-radio>
                <el-radio label="1">是</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="岗位">
              <el-select v-model="form.postIds" multiple placeholder="请选择岗位">
                <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1" ></el-option>
@@ -451,7 +461,8 @@
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: []
        roleIds: [],
        canViewAllConsult: "0"
      };
      this.resetForm("form");
    },
sql/DispatchRunning.sql
New file
@@ -0,0 +1,10 @@
create table DispatchOrd_Running(
    id    int    comment "自增主键",
DispatchOrdIDDt    bigint    comment '调度单ID',
DispatchOrdState    int    comment '状态',
DispatchOrdStartDate    datetime    comment '状态时间',
DispatchOrdStartOA    int    comment '操作状态的OA用户ID',
OA_latitude    float    comment '更新状态时的纬度',
OA_longitude    float    comment '更新状态时的经度',
OA_address    nvarchar(400)    comment '更新状态时的地址'
)
sql/add_can_view_all_consult_to_sys_user.sql
New file
@@ -0,0 +1,12 @@
-- æ·»åŠ sys_user表字段:是否可以查看管理部门的所有咨询单
-- æ‰§è¡Œæ—¶é—´: 2025-12-04
-- åŠŸèƒ½è¯´æ˜Ž: ç”¨äºŽæ ‡è¯†ç”¨æˆ·æ˜¯å¦æ‹¥æœ‰æŸ¥çœ‹å…¶ç®¡ç†éƒ¨é—¨æ‰€æœ‰å’¨è¯¢å•的权限
-- æ·»åŠ å­—æ®µ
ALTER TABLE `sys_user` ADD COLUMN `can_view_all_consult` CHAR(1) DEFAULT '0' COMMENT '是否可查看所有咨询单(0否 1是)' AFTER `wechat_nickname`;
-- è¯´æ˜Ž
-- 1. can_view_all_consult: æ ‡è¯†ç”¨æˆ·æ˜¯å¦å¯ä»¥æŸ¥çœ‹ç®¡ç†éƒ¨é—¨çš„æ‰€æœ‰å’¨è¯¢å•
-- 2. é»˜è®¤å€¼ä¸º '0'(否),表示普通用户
-- 3. å€¼ä¸º '1'(是)表示该用户有查看所有咨询单的权限
-- 4. è¯¥æƒé™ä¸ŽOA系统中的OA_Power字段关联,包含',020112,'表示拥有该权限