d294abb765e4ed349907c92ce313689c6299ba7d..656d6f8029f8bf9b2daa9dcc89101a879a70b860
2025-12-03 wlzboy
feat:优先添加执行人
656d6f 对比 | 目录
2025-12-03 wlzboy
feat:用户附加同步
7c790c 对比 | 目录
2025-12-03 wlzboy
feat:优化同步
c6e38b 对比 | 目录
2个文件已添加
28个文件已修改
4604 ■■■■■ 已修改文件
app/pagesTask/components/StaffSelector.vue 646 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue 481 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit-emergency.vue 618 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit-welfare.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentNotifyService.java 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentService.java 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/AlipayConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/ThirdPartyConfig.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/WechatPayConfig.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentNotifyController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IAdditionalFeeSyncService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/AdditionalFeeSyncServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DepartmentSyncServiceImpl.java 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 103 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskPaymentServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 639 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskAdditionalFeeMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/detail.vue 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/StaffSelector.vue
New file
@@ -0,0 +1,646 @@
<template>
  <view class="staff-selector-component">
    <view class="form-item">
      <view class="form-label" :class="{ required: required }">{{ label }}</view>
      <view class="staff-list">
        <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
          <view class="staff-info">
            <text class="staff-name">{{ staff.nickName }}</text>
          </view>
          <uni-icons
            v-if="canRemove(index)"
            type="closeempty"
            size="20"
            color="#ff4d4f"
            @click="removeStaff(index)"
          ></uni-icons>
          <uni-icons
            v-else
            type="checkmarkempty"
            size="20"
            color="#007AFF"
          ></uni-icons>
        </view>
        <view class="add-staff" @click="showStaffSelector">
          <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
          <text>添加人员</text>
        </view>
      </view>
    </view>
    <!-- 人员选择弹窗 -->
    <uni-popup ref="staffPopup" type="bottom" :safe-area="true">
      <view class="staff-selector-popup">
        <view class="popup-header">
          <view class="popup-title">选择执行人员</view>
          <view class="popup-close" @click="closeStaffSelector">
            <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
          </view>
        </view>
        <view class="search-box">
          <uni-icons type="search" size="18" color="#999"></uni-icons>
          <input
            class="search-input"
            placeholder="搜索姓名、手机号"
            v-model="staffSearchKeyword"
            @input="onStaffSearch"
          />
        </view>
        <view class="staff-filter">
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'driver' }"
            @click="filterStaff('driver')"
          >司机</view>
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'doctor' }"
            @click="filterStaff('doctor')"
          >医生</view>
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'nurse' }"
            @click="filterStaff('nurse')"
          >护士</view>
        </view>
        <scroll-view class="staff-list-popup" scroll-y="true">
          <view
            class="staff-item-popup"
            v-for="staff in filteredStaffList"
            :key="staff.userId"
            @click="toggleStaffSelection(staff)"
          >
            <view class="staff-info">
              <view class="staff-name-row">
                <text class="staff-name">{{ staff.nickName }}</text>
                <text class="staff-phone">{{ staff.phonenumber }}</text>
              </view>
              <view class="staff-detail-row">
                <text class="staff-dept">{{ staff.deptName }}</text>
              </view>
            </view>
            <uni-icons
              v-if="isStaffSelected(staff.userId)"
              type="checkmarkempty"
              size="24"
              color="#007AFF"
            ></uni-icons>
            <view v-else class="checkbox-empty"></view>
          </view>
          <view class="no-data" v-if="filteredStaffList.length === 0">
            <uni-icons type="info" size="40" color="#ccc"></uni-icons>
            <text>暂无人员数据</text>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="cancel-btn" @click="closeStaffSelector">取消</button>
          <button class="confirm-btn" @click="confirmStaffSelection">确定(已选{{ selectedStaff.length }})</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
<script>
import { mapState } from 'vuex'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { listBranchUsers } from "@/api/system/user"
export default {
  name: 'StaffSelector',
  components: {
    uniPopup
  },
  props: {
    // 已选择的人员列表
    value: {
      type: Array,
      default: () => []
    },
    // 标签文本
    label: {
      type: String,
      default: '执行任务人员'
    },
    // 是否必填
    required: {
      type: Boolean,
      default: false
    },
    // 是否自动添加当前用户
    autoAddCurrentUser: {
      type: Boolean,
      default: true
    },
    // 当前用户是否可移除
    currentUserRemovable: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      selectedStaff: [],
      allStaffList: [],
      filteredStaffList: [],
      staffSearchKeyword: '',
      staffFilterType: 'driver' // 默认选中司机
    }
  },
  computed: {
    ...mapState({
      currentUser: state => ({
        userId: state.user.userId,
        nickName: state.user.nickName || '张三',
        phonenumber: state.user.phonenumber || '',
        deptId: state.user.deptId || 100,
        posts: state.user.posts || [],
        roles: state.user.roles || [],
        dept: state.user.dept || null
      })
    })
  },
  watch: {
    value: {
      handler(newVal) {
        if (newVal && Array.isArray(newVal)) {
          this.selectedStaff = [...newVal]
        }
      },
      immediate: true,
      deep: true
    }
  },
  mounted() {
    this.loadStaffList()
    // 如果需要自动添加当前用户且选中人员为空
    if (this.autoAddCurrentUser && this.selectedStaff.length === 0) {
      this.initWithCurrentUser()
    }
  },
  methods: {
    // 初始化选中的人员(默认包含当前用户)
    initWithCurrentUser() {
      const currentUserStaff = {
        userId: this.currentUser.userId,
        nickName: this.currentUser.nickName,
        phonenumber: this.currentUser.phonenumber,
        deptId: this.currentUser.deptId,
        posts: this.currentUser.posts || [],
        roles: this.currentUser.roles || [],
        dept: this.currentUser.dept || null
      }
      // 获取当前用户的所有角色类型(可能有多个)
      currentUserStaff.types = this.getUserTypes(currentUserStaff)
      currentUserStaff.type = currentUserStaff.types[0] || 'driver' // 主要类型
      this.selectedStaff = [currentUserStaff]
      this.emitChange()
    },
    // 加载人员列表
    loadStaffList() {
      listBranchUsers().then(response => {
        const userList = response.data || []
        this.allStaffList = userList.map(user => ({
          userId: user.userId,
          nickName: user.nickName,
          phonenumber: user.phonenumber,
          deptName: user.dept?.deptName || '',
          postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
          roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
          posts: user.posts || [],
          roles: user.roles || [],
          dept: user.dept || null,
          // 支持多种类型
          types: this.getUserTypes(user),
          type: this.getUserTypes(user)[0] || 'driver' // 主要类型(用于向后兼容)
        }))
        this.filterStaffList()
      }).catch(error => {
        console.error('加载人员列表失败:', error)
        this.$modal.showToast('加载人员列表失败')
      })
    },
    // 根据用户的岗位或角色判断所有类型(支持多种身份)
    getUserTypes(user) {
      const types = []
      const postNames = user.posts ? user.posts.map(p => p.postName).join('') : ''
      const roleNames = user.roles ? user.roles.map(r => r.roleName).join('') : ''
      const deptName = user.dept?.deptName || ''
      // 判断是否为司机
      if (postNames.includes('司机') || roleNames.includes('司机') ||
          deptName.includes('车队') || deptName.includes('司机')) {
        types.push('driver')
      }
      // 判断是否为医生
      if (postNames.includes('医生') || roleNames.includes('医生') ||
          deptName.includes('医生') || deptName.includes('医护')) {
        types.push('doctor')
      }
      // 判断是否为护士
      if (postNames.includes('护士') || roleNames.includes('护士') || deptName.includes('护士')) {
        types.push('nurse')
      }
      // 如果没有匹配到任何类型,默认为司机
      if (types.length === 0) {
        types.push('driver')
      }
      return types
    },
    // 获取类型名称
    getUserTypeName(staffType) {
      const typeMap = {
        'driver': '司机',
        'doctor': '医生',
        'nurse': '护士'
      }
      return typeMap[staffType] || staffType || '司机'
    },
    // 显示人员选择弹窗
    showStaffSelector() {
      this.$refs.staffPopup.open()
      this.filterStaffList()
    },
    // 关闭人员选择弹窗
    closeStaffSelector() {
      this.$refs.staffPopup.close()
      this.staffSearchKeyword = ''
      this.staffFilterType = 'driver' // 重置为默认司机
    },
    // 人员搜索
    onStaffSearch(e) {
      this.staffSearchKeyword = e.detail.value
      this.filterStaffList()
    },
    // 筛选人员类型
    filterStaff(type) {
      this.staffFilterType = type
      this.filterStaffList()
    },
    // 过滤人员列表
    filterStaffList() {
      let list = [...this.allStaffList]
      // 按关键词搜索
      if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') {
        const keyword = this.staffSearchKeyword.trim().toLowerCase()
        list = list.filter(staff => {
          return staff.nickName.toLowerCase().includes(keyword) ||
                 (staff.phonenumber && staff.phonenumber.includes(keyword))
        })
      }
      // 根据选中的类型,将有该身份的人排在前面
      list.sort((a, b) => {
        const aHasType = a.types.includes(this.staffFilterType)
        const bHasType = b.types.includes(this.staffFilterType)
        if (aHasType && !bHasType) return -1  // a有该身份,排前面
        if (!aHasType && bHasType) return 1   // b有该身份,排前面
        return 0  // 都有或都没有,保持原顺序
      })
      this.filteredStaffList = list
    },
    // 切换人员选中状态
    toggleStaffSelection(staff) {
      const index = this.selectedStaff.findIndex(s => s.userId === staff.userId)
      if (index > -1) {
        // 如果是第一个且不可移除(当前用户),不允许移除
        if (!this.canRemove(index)) {
          this.$modal.showToast('当前用户不能移除')
          return
        }
        // 已选中,移除
        this.selectedStaff.splice(index, 1)
      } else {
        // 未选中,添加
        this.selectedStaff.push({ ...staff })
      }
    },
    // 判断人员是否已选中
    isStaffSelected(userId) {
      return this.selectedStaff.some(staff => staff.userId === userId)
    },
    // 判断是否可以移除
    canRemove(index) {
      // 如果是第一个且当前用户不可移除
      if (index === 0 && !this.currentUserRemovable) {
        return false
      }
      return true
    },
    // 确认人员选择
    confirmStaffSelection() {
      if (this.selectedStaff.length === 0) {
        this.$modal.showToast('请至少选择一名人员')
        return
      }
      this.emitChange()
      this.closeStaffSelector()
    },
    // 移除人员
    removeStaff(index) {
      if (!this.canRemove(index)) {
        this.$modal.showToast('当前用户不能移除')
        return
      }
      this.selectedStaff.splice(index, 1)
      this.emitChange()
    },
    // 触发change事件
    emitChange() {
      this.$emit('input', this.selectedStaff)
      this.$emit('change', this.selectedStaff)
    }
  }
}
</script>
<style lang="scss" scoped>
.staff-selector-component {
  .form-item {
    margin-bottom: 40rpx;
    .form-label {
      font-size: 28rpx;
      margin-bottom: 15rpx;
      color: #333;
      &.required::before {
        content: '*';
        color: #ff4d4f;
        margin-right: 4rpx;
        font-weight: bold;
      }
    }
    .staff-list {
      .staff-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx;
        background-color: #f9f9f9;
        border-radius: 10rpx;
        margin-bottom: 20rpx;
        .staff-info {
          flex: 1;
          .staff-name {
            font-size: 28rpx;
            color: #333;
            margin-right: 10rpx;
            display: block;
            margin-bottom: 8rpx;
          }
          .staff-roles {
            display: flex;
            flex-wrap: wrap;
            gap: 8rpx;
            .staff-role {
              font-size: 22rpx;
              color: white;
              background-color: #007AFF;
              padding: 4rpx 12rpx;
              border-radius: 6rpx;
            }
          }
        }
      }
      .add-staff {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20rpx;
        border: 1rpx dashed #007AFF;
        border-radius: 10rpx;
        color: #007AFF;
        text {
          margin-left: 10rpx;
        }
      }
    }
  }
}
// 人员选择弹窗样式
.staff-selector-popup {
  background-color: white;
  border-radius: 20rpx 20rpx 0 0;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx;
    border-bottom: 1rpx solid #f0f0f0;
    flex-shrink: 0;
    .popup-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .search-box {
    display: flex;
    align-items: center;
    margin: 20rpx 30rpx;
    padding: 15rpx 20rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    flex-shrink: 0;
    .search-input {
      flex: 1;
      margin-left: 10rpx;
      font-size: 28rpx;
    }
  }
  .staff-filter {
    display: flex;
    padding: 0 30rpx 20rpx;
    gap: 15rpx;
    flex-shrink: 0;
    .filter-item {
      flex: 1;
      text-align: center;
      padding: 15rpx 0;
      background-color: #f5f5f5;
      border-radius: 10rpx;
      font-size: 26rpx;
      color: #666;
      &.active {
        background-color: #007AFF;
        color: white;
      }
    }
  }
  .staff-list-popup {
    flex: 1;
    overflow-y: auto;
    padding: 0 30rpx;
    .staff-item-popup {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 25rpx 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      &:active {
        background-color: #f5f5f5;
      }
      .staff-info {
        flex: 1;
        .staff-name-row {
          display: flex;
          align-items: center;
          margin-bottom: 10rpx;
          .staff-name {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
            margin-right: 20rpx;
          }
          .staff-phone {
            font-size: 24rpx;
            color: #999;
          }
        }
        .staff-detail-row {
          display: flex;
          align-items: center;
          flex-wrap: wrap;
          .staff-dept {
            font-size: 24rpx;
            color: #666;
            margin-right: 15rpx;
          }
          .staff-types {
            display: flex;
            gap: 8rpx;
            .type-tag {
              font-size: 20rpx;
              color: white;
              padding: 4rpx 10rpx;
              border-radius: 6rpx;
              &.type-driver {
                background-color: #007AFF;
              }
              &.type-doctor {
                background-color: #34C759;
              }
              &.type-nurse {
                background-color: #AF52DE;
              }
            }
          }
        }
      }
      .checkbox-empty {
        width: 40rpx;
        height: 40rpx;
        border: 2rpx solid #ddd;
        border-radius: 50%;
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
        font-size: 28rpx;
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
</style>
app/pagesTask/create-emergency.vue
@@ -59,34 +59,14 @@
          </view>
        </picker>
      </view>
      <view class="form-item">
        <view class="form-label">执行任务人员</view>
        <view class="staff-list">
          <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
            <view class="staff-info">
              <text class="staff-name">{{ staff.nickName }}</text>
              <text class="staff-role">({{ getUserTypeName(staff.type) || '未知职位' }})</text>
            </view>
            <uni-icons
              v-if="index > 0"
              type="closeempty"
              size="20"
              color="#ff4d4f"
              @click="removeStaff(index)"
            ></uni-icons>
            <uni-icons
              v-else
              type="checkmarkempty"
              size="20"
              color="#007AFF"
            ></uni-icons>
          </view>
          <view class="add-staff" @click="showStaffSelector">
            <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
            <text>添加人员</text>
          </view>
        </view>
      </view>
      <StaffSelector
        v-model="selectedStaff"
        label="执行任务人员"
        :required="false"
        :auto-add-current-user="true"
        :current-user-removable="false"
        @change="onStaffChange"
      />
      
    
      
@@ -213,82 +193,6 @@
      </view>
    </view>
    
    <!-- 人员选择弹窗 -->
    <uni-popup ref="staffPopup" type="bottom" :safe-area="true">
      <view class="staff-selector-popup">
        <view class="popup-header">
          <view class="popup-title">选择执行人员</view>
          <view class="popup-close" @click="closeStaffSelector">
            <uni-icons type="closeempty" size="24" color="#333"></uni-icons>
          </view>
        </view>
        <view class="search-box">
          <uni-icons type="search" size="18" color="#999"></uni-icons>
          <input
            class="search-input"
            placeholder="搜索姓名、手机号"
            v-model="staffSearchKeyword"
            @input="onStaffSearch"
          />
        </view>
        <view class="staff-filter">
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'driver' }"
            @click="filterStaff('driver')"
          >司机</view>
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'doctor' }"
            @click="filterStaff('doctor')"
          >医生</view>
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'nurse' }"
            @click="filterStaff('nurse')"
          >护士</view>
        </view>
        <scroll-view class="staff-list-popup" scroll-y="true">
          <view
            class="staff-item-popup"
            v-for="staff in filteredStaffList"
            :key="staff.userId"
            @click="toggleStaffSelection(staff)"
          >
            <view class="staff-info">
              <view class="staff-name-row">
                <text class="staff-name">{{ staff.nickName }}</text>
                <text class="staff-phone">{{ staff.phonenumber }}</text>
              </view>
              <view class="staff-detail-row">
                <text class="staff-dept">{{ staff.deptName }}</text>
                <text class="staff-post">{{ staff.postName || staff.roleName  || '未知职位' }}</text>
              </view>
            </view>
            <uni-icons
              v-if="isStaffSelected(staff.userId)"
              type="checkmarkempty"
              size="24"
              color="#007AFF"
            ></uni-icons>
            <view v-else class="checkbox-empty"></view>
          </view>
          <view class="no-data" v-if="filteredStaffList.length === 0">
            <uni-icons type="info" size="40" color="#ccc"></uni-icons>
            <text>暂无人员数据</text>
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="cancel-btn" @click="closeStaffSelector">取消</button>
          <button class="confirm-btn" @click="confirmStaffSelection">确定(已选{{ selectedStaff.length }})</button>
        </view>
      </view>
    </uni-popup>
    
   <!-- 智能识别弹窗 -->
    <uni-popup ref="smartParsePopup" type="bottom" :safe-area="true">
@@ -345,6 +249,7 @@
import OrganizationSelector from './components/OrganizationSelector.vue'
import HospitalSelector from './components/HospitalSelector.vue'
import DiseaseSelector from './components/DiseaseSelector.vue'
import StaffSelector from './components/StaffSelector.vue'
export default {
  components: {
@@ -354,7 +259,8 @@
    OrganizationSelector,
    HospitalSelector,
    DiseaseSelector,
    DepartureSelector
    DepartureSelector,
    StaffSelector
  },
  data() {
    return {
@@ -373,10 +279,6 @@
      mapSelectorType: '',
      // 人员选择相关
      selectedStaff: [], // 已选择的人员列表
      allStaffList: [], // 所有人员列表
      filteredStaffList: [], // 过滤后的人员列表
      staffSearchKeyword: '', // 人员搜索关键词
      staffFilterType: 'driver', // 人员筛选类型:driver/doctor/nurse,默认选中司机
      // 病情选择相关
      selectedDiseases: [], // 已选择的病情列表
      taskForm: {
@@ -452,8 +354,6 @@
    this.getAvailableVehicles().then(() => {
      this.getUserBoundVehicleInfo()
    })
    this.initSelectedStaff()
    this.loadDeptStaff()
    // 加载科室字典数据
    this.loadDepartments()
    // 加载任务类型数据
@@ -592,18 +492,7 @@
      this.selectedEmergencyTaskType = selected.text
      this.selectedEmergencyTaskTypeId = selected.id
    },
    getUserTypeName(staffType){
      switch(staffType){
        case "nurse":
          return "护士";
        case "doctor":
          return "医生";
        case "driver":
          return "司机";
        default:
          return "司机";
      }
    },
    
    // 加载单据类型数据
    loadDocumentTypes() {
@@ -684,182 +573,10 @@
      }
    },
    
    // 初始化选中的人员(默认包含当前用户)
    initSelectedStaff() {
      // 构建当前用户对象,包含完整的角色信息
      const currentUserStaff = {
        userId: this.currentUser.userId,
        nickName: this.currentUser.nickName,
        phonenumber: this.currentUser.phonenumber,
        postName: this.currentUser.position,
        deptId: this.currentUser.deptId,
        posts: this.currentUser.posts || [],
        roles: this.currentUser.roles || [],
        dept: this.currentUser.dept || null
      }
      // 为当前用户设置角色类型
      currentUserStaff.type = this.getUserType(currentUserStaff)
      this.selectedStaff = [currentUserStaff]
    },
    // 加载当前用户所在分公司的所有人员
    loadDeptStaff() {
      console.log('开始加载人员列表')
      // 调用新接口,自动根据当前用户的oaOrderClass获取分公司下的用户
      listBranchUsers().then(response => {
        console.log('人员列表API响应:', response)
        const userList = response.data || []
        console.log('解析出的用户列表:', userList, '数量:', userList.length)
        this.allStaffList = userList.map(user => ({
          userId: user.userId,
          nickName: user.nickName,
          phonenumber: user.phonenumber,
          deptName: user.dept?.deptName || '',
          postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
          roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
          // 根据岗位名称或角色名称判断类型
          type: this.getUserType(user)
        }))
        console.log('处理后的人员列表:', this.allStaffList, '数量:', this.allStaffList.length)
        // 初始化过滤列表
        this.filterStaffList()
      }).catch(error => {
        console.error('加载人员列表失败:', error)
        this.$modal.showToast('加载人员列表失败')
      })
    },
    // 根据用户的岗位或角色判断类型
    getUserType(user) {
      const postName = user.posts && user.posts.length > 0 ? user.posts[0].postName : ''
      const roleName = user.roles && user.roles.length > 0 ? user.roles[0].roleName : ''
      const deptName = user.dept?.deptName || ''
      // console.log("获取用户类型:", postName, roleName,user)
      // 判断是否为司机
      if (postName.includes('司机') || roleName.includes('司机') || deptName.includes('车队') || deptName.includes('司机')) {
        return 'driver'
      }
      // 判断是否为护士
      if (postName.includes('护士') || roleName.includes('护士') || deptName.includes('护士')) {
        return 'nurse'
      }
      // 判断是否为医生
      if (postName.includes('医生') || roleName.includes('医生') || deptName.includes('医生') ) {
        return 'doctor'
      }
      if( deptName.includes("医护")){
        return 'doctor'
      }
      // 其他类型,默认为司机
      return 'driver'
    },
    // 显示人员选择弹窗
    showStaffSelector() {
      this.$refs.staffPopup.open()
      this.filterStaffList()
    },
    // 关闭人员选择弹窗
    closeStaffSelector() {
      this.$refs.staffPopup.close()
      this.staffSearchKeyword = ''
      this.staffFilterType = 'driver' // 重置为默认的司机类型
    },
    // 人员搜索
    onStaffSearch(e) {
      this.staffSearchKeyword = e.detail.value
      this.filterStaffList()
    },
    // 筛选人员类型
    filterStaff(type) {
      this.staffFilterType = type
      this.filterStaffList()
    },
    // 过滤人员列表
    filterStaffList() {
      console.log('开始过滤人员列表,原始数量:', this.allStaffList.length)
      let list = [...this.allStaffList]
      // 按类型过滤
      if (this.staffFilterType === 'driver') {
        list = list.filter(staff => staff.type === 'driver')
      } else if (this.staffFilterType === 'doctor') {
        list = list.filter(staff => staff.type === 'doctor')
      } else if (this.staffFilterType === 'nurse') {
        list = list.filter(staff => staff.type === 'nurse')
      }
      console.log('按类型过滤后:', this.staffFilterType, '数量:', list.length)
      // 按关键词搜索
      if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') {
        const keyword = this.staffSearchKeyword.trim().toLowerCase()
        list = list.filter(staff => {
          return staff.nickName.toLowerCase().includes(keyword) ||
                 (staff.phonenumber && staff.phonenumber.includes(keyword))
        })
      }
      console.log('按关键词过滤后,数量:', list.length)
      this.filteredStaffList = list
      // console.log('最终过滤结果:', this.filteredStaffList)
    },
    // 切换人员选中状态
    toggleStaffSelection(staff) {
      const index = this.selectedStaff.findIndex(s => s.userId === staff.userId)
      if (index > -1) {
        // 如果是第一个(当前用户),不允许移除
        if (index === 0) {
          this.$modal.showToast('当前用户不能移除')
          return
        }
        // 已选中,移除
        this.selectedStaff.splice(index, 1)
      } else {
        // 未选中,添加
        this.selectedStaff.push(staff)
      }
    },
    // 判断人员是否已选中
    isStaffSelected(userId) {
      return this.selectedStaff.some(staff => staff.userId === userId)
    },
    // 确认人员选择
    confirmStaffSelection() {
      if (this.selectedStaff.length === 0) {
        this.$modal.showToast('请至少选择一名人员')
        return
      }
      this.closeStaffSelector()
    },
    // 移除人员
    removeStaff(index) {
      if (index === 0) {
        this.$modal.showToast('当前用户不能移除')
        return
      }
      this.selectedStaff.splice(index, 1)
    },
    addStaff() {
      this.showStaffSelector()
    // 人员变化事件
    onStaffChange(staff) {
      console.log('选中人员变化:', staff)
      // 组件已经通过 v-model 更新了 selectedStaff
    },
    calculateDistanceByManualAddress() {
      const fromAddress = this.taskForm.hospitalOut.address
@@ -1969,172 +1686,6 @@
          color: #999;
        }
      }
    }
  }
}
// 人员选择弹窗样式
.staff-selector-popup {
  background-color: white;
  border-radius: 20rpx 20rpx 0 0;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx;
    border-bottom: 1rpx solid #f0f0f0;
    flex-shrink: 0;
    .popup-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .search-box {
    display: flex;
    align-items: center;
    margin: 20rpx 30rpx;
    padding: 15rpx 20rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    flex-shrink: 0;
    .search-input {
      flex: 1;
      margin-left: 10rpx;
      font-size: 28rpx;
    }
  }
  .staff-filter {
    display: flex;
    padding: 0 30rpx 20rpx;
    gap: 20rpx;
    flex-shrink: 0;
    .filter-item {
      flex: 1;
      text-align: center;
      padding: 15rpx 0;
      background-color: #f5f5f5;
      border-radius: 10rpx;
      font-size: 28rpx;
      color: #666;
      &.active {
        background-color: #007AFF;
        color: white;
      }
    }
  }
  .staff-list-popup {
    flex: 1;
    overflow-y: auto;
    padding: 0 30rpx;
    .staff-item-popup {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 25rpx 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      &:active {
        background-color: #f5f5f5;
      }
      .staff-info {
        flex: 1;
        .staff-name-row {
          display: flex;
          align-items: center;
          margin-bottom: 10rpx;
          .staff-name {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
            margin-right: 20rpx;
          }
          .staff-phone {
            font-size: 24rpx;
            color: #999;
          }
        }
        .staff-detail-row {
          display: flex;
          align-items: center;
          .staff-dept {
            font-size: 24rpx;
            color: #666;
            margin-right: 20rpx;
          }
          .staff-post {
            font-size: 24rpx;
            color: #007AFF;
          }
        }
      }
      .checkbox-empty {
        width: 40rpx;
        height: 40rpx;
        border: 2rpx solid #ddd;
        border-radius: 50%;
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
        font-size: 28rpx;
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
app/pagesTask/detail.vue
@@ -28,9 +28,44 @@
          <view class="label">执行车辆</view>
          <view class="value">{{ getVehicleInfo(taskDetail) }}</view>
        </view>
        <view class="info-item">
          <view class="label">执行人员</view>
          <view class="value">{{ taskDetail.assigneeName || '未分配' }}</view>
      </view>
      <!-- 执行人员列表 -->
      <view class="detail-section">
        <view class="section-title">执行人员</view>
        <view v-if="taskDetail.assignees && taskDetail.assignees.length > 0" class="assignee-list">
          <view
            class="assignee-item"
            v-for="(assignee, index) in taskDetail.assignees"
            :key="assignee.userId || index"
          >
            <view class="assignee-index">{{ index + 1 }}</view>
            <view class="assignee-info">
              <view class="assignee-name">
                {{ assignee.userName }}
                <view v-if="assignee.isPrimary === '1'" class="primary-badge">
                  <uni-icons type="star-filled" size="12" color="#ff9500"></uni-icons>
                  <text>负责人</text>
                </view>
              </view>
              <view class="assignee-role">
                <view
                  class="role-tag"
                  :class="{
                    'role-driver': assignee.userType === 'driver',
                    'role-doctor': assignee.userType === 'doctor',
                    'role-nurse': assignee.userType === 'nurse'
                  }"
                >
                  {{ getUserTypeLabel(assignee.userType) }}
                </view>
              </view>
            </view>
          </view>
        </view>
        <view v-else class="empty-assignee">
          <uni-icons type="info" size="40" color="#ccc"></uni-icons>
          <text>暂无执行人员</text>
        </view>
      </view>
      
@@ -531,12 +566,12 @@
        getTask(this.taskId).then(response => {
          this.taskDetail = response.data || response
          // 调试:打印返回的数据
          console.log('任务详情完整数据:', JSON.stringify(this.taskDetail, null, 2))
          console.log('任务类型字段值:', this.taskDetail.taskType)
          console.log('任务状态字段值:', this.taskDetail.taskStatus)
          console.log('出发地址:', this.taskDetail.departureAddress)
          console.log('目的地址:', this.taskDetail.destinationAddress)
          console.log('转运任务信息 (emergencyInfo):', this.taskDetail.emergencyInfo)
          // console.log('任务详情完整数据:', JSON.stringify(this.taskDetail, null, 2))
          // console.log('任务类型字段值:', this.taskDetail.taskType)
          // console.log('任务状态字段值:', this.taskDetail.taskStatus)
          // console.log('出发地址:', this.taskDetail.departureAddress)
          // console.log('目的地址:', this.taskDetail.destinationAddress)
          // console.log('转运任务信息 (emergencyInfo):', this.taskDetail.emergencyInfo)
          
          // 如果是转运任务,加载支付信息
          if (this.taskDetail.taskType === 'EMERGENCY_TRANSFER') {
@@ -600,9 +635,9 @@
        return remaining > 0 ? remaining.toFixed(2) : '0.00'
      },
      
      // 获取车辆信息
      // 获取车辆信息(修复:防止 assignedVehicles 为 null)
      getVehicleInfo(task) {
        if (task.assignedVehicles && task.assignedVehicles.length > 0) {
        if (task.assignedVehicles && Array.isArray(task.assignedVehicles) && task.assignedVehicles.length > 0) {
          const firstVehicle = task.assignedVehicles[0]
          let vehicleInfo = firstVehicle.vehicleNo || '未知车牌'
          if (task.assignedVehicles.length > 1) {
@@ -705,6 +740,16 @@
        return typeMap[type] || '未知类型'
      },
      
      // 获取用户类型标签
      getUserTypeLabel(userType) {
        const typeMap = {
          'driver': '司机',
          'doctor': '医生',
          'nurse': '护士'
        }
        return typeMap[userType] || userType || '未知'
      },
      // 处理结算
      handleSettlement() {
        uni.navigateTo({
@@ -776,8 +821,8 @@
          
          const activeTasks = response.data || [];
          
          // 过滤掉当前任务本身
          const otherActiveTasks = activeTasks.filter(task => task.taskId !== this.taskId);
          // 过滤掉当前任务本身(修复:防止 activeTasks 为 null)
          const otherActiveTasks = (activeTasks && Array.isArray(activeTasks)) ? activeTasks.filter(task => task.taskId !== this.taskId) : [];
          
          if (otherActiveTasks.length > 0) {
            // 车辆有其他正在进行中的任务
@@ -1225,6 +1270,104 @@
        }
      }
      
      // 执行人员列表样式
      .assignee-list {
        .assignee-item {
          display: flex;
          align-items: center;
          padding: 20rpx;
          margin-bottom: 15rpx;
          background-color: #f9f9f9;
          border-radius: 10rpx;
          &:last-child {
            margin-bottom: 0;
          }
          .assignee-index {
            width: 50rpx;
            height: 50rpx;
            border-radius: 50%;
            background-color: #007AFF;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24rpx;
            font-weight: bold;
            margin-right: 20rpx;
            flex-shrink: 0;
          }
          .assignee-info {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 10rpx;
            .assignee-name {
              display: flex;
              align-items: center;
              font-size: 30rpx;
              color: #333;
              font-weight: 500;
              .primary-badge {
                display: inline-flex;
                align-items: center;
                gap: 4rpx;
                margin-left: 12rpx;
                padding: 4rpx 12rpx;
                background-color: #fff3e0;
                border-radius: 6rpx;
                text {
                  font-size: 20rpx;
                  color: #ff9500;
                  font-weight: normal;
                }
              }
            }
            .assignee-role {
              .role-tag {
                display: inline-block;
                padding: 4rpx 12rpx;
                border-radius: 6rpx;
                font-size: 22rpx;
                color: white;
                &.role-driver {
                  background-color: #007AFF;
                }
                &.role-doctor {
                  background-color: #34C759;
                }
                &.role-nurse {
                  background-color: #AF52DE;
                }
              }
            }
          }
        }
      }
      .empty-assignee {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 60rpx 0;
        color: #999;
        text {
          margin-top: 20rpx;
          font-size: 28rpx;
        }
      }
      .info-item {
        display: flex;
        margin-bottom: 20rpx;
app/pagesTask/edit-emergency.vue
@@ -27,6 +27,16 @@
        />
      </view>
      
      <DepartureSelector
        :address.sync="departureAddress"
        :longitude.sync="departureLongitude"
        :latitude.sync="departureLatitude"
        :region="selectedRegion"
        :required="false"
        @address-selected="onDepartureAddressSelected"
        @location-success="onDepartureLocationSuccess"
      />
      <view class="form-item">
        <view class="form-label required">转运时间</view>
        <uni-datetime-picker 
@@ -37,27 +47,14 @@
        />
      </view>
      
      <view class="form-item">
        <view class="form-label">执行任务人员</view>
        <view class="staff-list">
          <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
            <view class="staff-info">
              <text class="staff-name">{{ staff.nickName }}</text>
              <text class="staff-role">({{ getUserTypeName(staff.type) || '未知职位' }})</text>
            </view>
            <uni-icons
              type="closeempty"
              size="20"
              color="#ff4d4f"
              @click="removeStaff(index)"
            ></uni-icons>
          </view>
          <view class="add-staff" @click="showStaffSelector">
            <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
            <text>添加人员</text>
          </view>
        </view>
      </view>
      <StaffSelector
        v-model="selectedStaff"
        label="执行任务人员"
        :required="false"
        :auto-add-current-user="false"
        :current-user-removable="true"
        @change="onStaffChange"
      />
      
      <view class="form-section-title">患者信息</view>
      <view class="form-item">
@@ -188,80 +185,6 @@
        ></map-selector>
      </view>
    </uni-popup>
    <!-- 人员选择器弹窗 -->
    <uni-popup ref="staffPopup" type="bottom" :mask-click="false">
      <view class="staff-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择执行人员</view>
          <view class="close-btn" @click="closeStaffSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <view class="search-bar">
          <input
            class="search-input"
            placeholder="搜索姓名或电话"
            v-model="staffSearchKeyword"
            @input="onStaffSearch"
          />
        </view>
        <view class="filter-tabs">
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'driver' }"
            @click="filterStaff('driver')"
          >
            司机
          </view>
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'doctor' }"
            @click="filterStaff('doctor')"
          >
            医生
          </view>
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'nurse' }"
            @click="filterStaff('nurse')"
          >
            护士
          </view>
        </view>
        <scroll-view class="staff-list-scroll" scroll-y="true">
          <view
            class="staff-list-item"
            v-for="staff in filteredStaffList"
            :key="staff.userId"
            @click="toggleStaffSelection(staff)"
          >
            <view class="staff-item-info">
              <text class="staff-item-name">{{ staff.nickName }}</text>
              <text class="staff-item-dept">{{ staff.deptName }}</text>
            </view>
            <view class="staff-item-check">
              <uni-icons
                v-if="isStaffSelected(staff.userId)"
                type="checkmarkempty"
                size="24"
                color="#007AFF"
              ></uni-icons>
            </view>
          </view>
          <view v-if="filteredStaffList.length === 0" class="empty-tip">
            暂无人员数据
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="confirm-btn" @click="confirmStaffSelection">确定(已选{{ selectedStaff.length }})</button>
        </view>
      </view>
    </uni-popup>
  </scroll-view>
</template>
@@ -270,7 +193,6 @@
import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { getTask, updateTask } from "@/api/task"
import { listBranchUsers } from "@/api/system/user"
import { baiduDistanceByAddress } from "@/api/map"
import { calculateTransferPrice } from "@/api/price"
import MapSelector from './components/map-selector.vue'
@@ -278,6 +200,8 @@
import OrganizationSelector from './components/OrganizationSelector.vue'
import HospitalSelector from './components/HospitalSelector.vue'
import DiseaseSelector from './components/DiseaseSelector.vue'
import DepartureSelector from './components/DepartureSelector.vue'
import StaffSelector from './components/StaffSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
@@ -288,7 +212,9 @@
    VehicleSelector,
    OrganizationSelector,
    HospitalSelector,
    DiseaseSelector
    DiseaseSelector,
    DepartureSelector,
    StaffSelector
  },
  mixins: [distanceCalculator],
  data() {
@@ -300,16 +226,19 @@
      selectedOrganizationId: null,
      selectedRegion: '',
      mapSelectorType: '',
      // 扩展 addressCoordinates 支持多种键名
      addressCoordinates: {
        start: null,
        end: null,
        hospitalOutAddress: null,
        hospitalInAddress: null
      },
      // 出发地信息
      departureAddress: '',
      departureLongitude: null,
      departureLatitude: null,
      selectedDiseases: [], // 已选择的病情列表
      selectedStaff: [], // 已选择的人员列表
      allStaffList: [], // 所有人员列表
      filteredStaffList: [], // 过滤后的人员列表
      staffSearchKeyword: '', // 人员搜索关键词
      staffFilterType: 'driver', // 人员筛选类型
      selectedDiseases: [], // 已选择的病情列表(确保初始化为空数组)
      selectedStaff: [], // 已选择的人员列表(确保初始化为空数组)
      taskForm: {
        transferTime: '',
        patient: {
@@ -356,7 +285,6 @@
    if (options.id) {
      this.taskId = options.id
      this.loadTaskDetail()
      this.loadDeptStaff() // 加载人员列表
    } else {
      this.$modal.showToast('任务ID不能为空')
      setTimeout(() => {
@@ -403,8 +331,8 @@
          this.taskForm.patient.idCard = info.patientIdCard || ''
          this.taskForm.patient.condition = info.patientCondition || ''
          
          // 解析病情信息
          this.parseDiseaseInfo(info.patientCondition, info.diseaseIds)
          // 解析病情信息(修复:使用与创建界面一致的逻辑)
          this.parseDiseaseInfo(info.patientCondition, info.diseases)
          
          // 转出医院信息
          this.taskForm.hospitalOut.id = info.hospitalOutId || null
@@ -489,8 +417,8 @@
          console.log('设置目标地坐标:', this.taskDetail.destinationLongitude, this.taskDetail.destinationLatitude)
        }
        
        // 设置执行人员
        if (this.taskDetail.assignees && this.taskDetail.assignees.length > 0) {
        // 设置执行人员(修复:确保 assignees 不为 null)
        if (this.taskDetail.assignees && Array.isArray(this.taskDetail.assignees) && this.taskDetail.assignees.length > 0) {
          console.log('原始执行人员数据:', this.taskDetail.assignees)
          this.selectedStaff = this.taskDetail.assignees.map(assignee => ({
            userId: assignee.userId,
@@ -503,6 +431,8 @@
        } else {
          console.warn('任务没有分配执行人员或assignees为空')
          console.log('taskDetail.assignees:', this.taskDetail.assignees)
          // 确保 selectedStaff 初始化为空数组
          this.selectedStaff = []
        }
        
        console.log('表单数据填充完成:', this.taskForm)
@@ -523,9 +453,33 @@
    
    // 归属机构选择变化
    onOrganizationChange(orgData) {
      // orgData 包含:deptId, deptName, serviceOrderClass, region
      // orgData 包含:deptId, deptName, serviceOrderClass, region, departureAddress, departureLongitude, departureLatitude
      this.selectedOrganizationId = orgData.deptId
      console.log('选中归属机构:', orgData.deptName, '部门ID:', orgData.deptId)
      this.selectedRegion = orgData.region
      // 自动填充出发地信息(机构的地址和坐标)
      if (orgData.departureAddress) {
        this.departureAddress = orgData.departureAddress
        this.departureLongitude = orgData.departureLongitude || null
        this.departureLatitude = orgData.departureLatitude || null
        console.log('自动填充机构出发地:', this.departureAddress, '坐标:', this.departureLongitude, this.departureLatitude)
      }
      console.log('选中归属机构:', orgData.deptName, '部门ID:', orgData.deptId, '地域:', orgData.region)
    },
    // 出发地地址选择(从地图建议中选择)
    onDepartureAddressSelected(data) {
      // data 包含: address, longitude, latitude, location
      console.log('出发地地址选择:', data)
      // 组件已经通过 .sync 更新了 departureAddress, departureLongitude, departureLatitude
    },
    // 出发地GPS定位成功
    onDepartureLocationSuccess(data) {
      // data 包含: address, longitude, latitude
      console.log('出发地GPS定位成功:', data)
      // 组件已经通过 .sync 更新了 departureAddress, departureLongitude, departureLatitude
    },
    
    // 转出医院变化
@@ -582,211 +536,107 @@
      // 组件已经通过 v-model 更新了 selectedDiseases
    },
    
    // 加载当前用户所在分公司的所有人员
    loadDeptStaff() {
      console.log('开始加载人员列表')
    // 人员变化
    onStaffChange(staff) {
      console.log('选中人员变化:', staff)
      // 组件已经通过 v-model 更新了 selectedStaff
    },
    // 解析病情信息(从字符串解析出ICD-10疾病列表)- 修复:与创建界面保持一致
    parseDiseaseInfo(conditionText, diseases) {
      console.log('========== 开始解析病情信息 ==========')
      console.log('conditionText:', conditionText)
      console.log('diseases数组:', diseases)
      
      listBranchUsers().then(response => {
        console.log('人员列表API响应:', response)
        const userList = response.data || []
        console.log('解析出的用户列表:', userList, '数量:', userList.length)
        this.allStaffList = userList.map(user => ({
          userId: user.userId,
          nickName: user.nickName,
          phonenumber: user.phonenumber,
          deptName: user.dept?.deptName || '',
          postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
          roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
          type: this.getUserType(user)
        }))
        console.log('处理后的人员列表:', this.allStaffList, '数量:', this.allStaffList.length)
        this.filterStaffList()
      }).catch(error => {
        console.error('加载人员列表失败:', error)
        this.$modal.showToast('加载人员列表失败')
      })
    },
    // 根据用户的岗位或角色判断类型
    getUserType(user) {
      const postName = user.posts && user.posts.length > 0 ? user.posts[0].postName : ''
      const roleName = user.roles && user.roles.length > 0 ? user.roles[0].roleName : ''
      const deptName = user.dept?.deptName || ''
      if (postName.includes('司机') || roleName.includes('司机') || deptName.includes('车队') || deptName.includes('司机')) {
        return 'driver'
      }
      if (postName.includes('护士') || roleName.includes('护士') || deptName.includes('护士')) {
        return 'nurse'
      }
      if (postName.includes('医生') || roleName.includes('医生') || deptName.includes('医生')) {
        return 'doctor'
      }
      if (deptName.includes('医护')) {
        return 'doctor'
      }
      return 'driver'
    },
    getUserTypeName(staffType) {
      switch(staffType) {
        case 'nurse':
          return '护士'
        case 'doctor':
          return '医生'
        case 'driver':
          return '司机'
        default:
          return '司机'
      }
    },
    // 显示人员选择弹窗
    showStaffSelector() {
      this.$refs.staffPopup.open()
      this.filterStaffList()
    },
    // 关闭人员选择弹窗
    closeStaffSelector() {
      this.$refs.staffPopup.close()
      this.staffSearchKeyword = ''
      this.staffFilterType = 'driver'
    },
    // 人员搜索
    onStaffSearch(e) {
      this.staffSearchKeyword = e.detail.value
      this.filterStaffList()
    },
    // 筛选人员类型
    filterStaff(type) {
      this.staffFilterType = type
      this.filterStaffList()
    },
    // 过滤人员列表
    filterStaffList() {
      console.log('开始过滤人员列表,原始数量:', this.allStaffList.length)
      let list = [...this.allStaffList]
      // 按类型过滤
      if (this.staffFilterType === 'driver') {
        list = list.filter(staff => staff.type === 'driver')
      } else if (this.staffFilterType === 'doctor') {
        list = list.filter(staff => staff.type === 'doctor')
      } else if (this.staffFilterType === 'nurse') {
        list = list.filter(staff => staff.type === 'nurse')
      }
      console.log('按类型过滤后:', this.staffFilterType, '数量:', list.length)
      // 按关键词搜索
      if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') {
        const keyword = this.staffSearchKeyword.trim().toLowerCase()
        list = list.filter(staff => {
          return staff.nickName.toLowerCase().includes(keyword) ||
                 (staff.phonenumber && staff.phonenumber.includes(keyword))
        })
      }
      console.log('按关键词过滤后,数量:', list.length)
      this.filteredStaffList = list
    },
    // 切换人员选中状态
    toggleStaffSelection(staff) {
      const index = this.selectedStaff.findIndex(s => s.userId === staff.userId)
      if (index > -1) {
        // 已选中,移除
        this.selectedStaff.splice(index, 1)
      } else {
        // 未选中,添加
        this.selectedStaff.push(staff)
      }
    },
    // 判断人员是否已选中
    isStaffSelected(userId) {
      return this.selectedStaff.some(staff => staff.userId === userId)
    },
    // 确认人员选择
    confirmStaffSelection() {
      if (this.selectedStaff.length === 0) {
        this.$modal.showToast('请至少选择一名人员')
        return
      }
      this.closeStaffSelector()
    },
    // 移除人员
    removeStaff(index) {
      this.selectedStaff.splice(index, 1)
    },
    // 解析病情信息(从字符串解析出ICD-10疾病列表)
    parseDiseaseInfo(conditionText, diseaseIds) {
      if (!conditionText) {
        console.log('病情文本为空,清空选中病情')
        this.selectedDiseases = []
        this.taskForm.patient.otherCondition = ''
        return
      }
      // 解析diseaseIds(逗号分隔的字符串转为数组)
      const diseaseIdArray = diseaseIds ? diseaseIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) : []
      
      // 如果包含"其他:"分隔符,拆分病情和其他描述
      if (conditionText.includes('\n其他:')) {
        const parts = conditionText.split('\n其他:')
        const diseasePart = parts[0]
        this.taskForm.patient.otherCondition = parts[1] || ''
        console.log('病情部分:', diseasePart)
        console.log('其他描述:', this.taskForm.patient.otherCondition)
        
        // 解析病情部分
        this.parseDiseaseList(diseasePart, diseaseIdArray)
        this.parseDiseaseList(diseasePart, diseases)
      } else {
        // 没有"其他"部分,全部作为其他描述
        this.taskForm.patient.otherCondition = conditionText
        this.selectedDiseases = []
        // 没有"其他"部分,尝试解析是否有疾病列表
        const hasDiseasesFormat = /([^(]+)\(([^)]+)\)/.test(conditionText)
        console.log('是否包含疾病格式:', hasDiseasesFormat)
        if (hasDiseasesFormat) {
          // 有疾病格式,解析疾病列表
          this.parseDiseaseList(conditionText, diseases)
          this.taskForm.patient.otherCondition = ''
        } else {
          // 没有疾病格式,全部作为其他描述
          console.log('无疾病格式,作为其他描述')
          this.taskForm.patient.otherCondition = conditionText
          this.selectedDiseases = []
        }
      }
      console.log('解析完成,selectedDiseases:', JSON.stringify(this.selectedDiseases))
      console.log('解析完成,otherCondition:', this.taskForm.patient.otherCondition)
      console.log('========================================\n')
    },
    
    // 解析病情列表(格式:疾病名(编码)、疾病名(编码))
    parseDiseaseList(diseasePart, diseaseIdArray) {
    parseDiseaseList(diseasePart, diseasesData) {
      console.log('--- parseDiseaseList 开始 ---')
      console.log('diseasePart:', diseasePart)
      console.log('diseasesData:', diseasesData)
      if (!diseasePart) {
        console.log('病情部分为空')
        this.selectedDiseases = []
        return
      }
      
      // 如果后端已经返回了diseases数组,直接使用
      if (diseasesData && Array.isArray(diseasesData) && diseasesData.length > 0) {
        console.log('使用后端返回的diseases数组,数量:', diseasesData.length)
        this.selectedDiseases = diseasesData.map(d => ({
          id: d.icdId || null,
          icdCode: d.icdCode || '',
          icdName: d.icdName || ''
        }))
        console.log('解析后的selectedDiseases:', JSON.stringify(this.selectedDiseases))
        return
      }
      // 否则,从字符串解析
      console.log('从字符串解析病情')
      // 匹配格式:疾病名(编码)
      const regex = /([^(]+)\(([^)]+)\)/g
      const diseases = []
      let match
      let index = 0
      
      while ((match = regex.exec(diseasePart)) !== null) {
        const icdName = match[1].replace(/[、,\s]+$/, '').replace(/^[、,\s]+/, '').trim()
        const icdCode = match[2].trim()
        
        console.log('匹配到病情 - 名称:', icdName, '编码:', icdCode)
        // 只添加病情名称不为空的项
        if (icdName) {
          diseases.push({
            icdName: icdName,
            id: null, // 从字符串解析时,没有ID
            icdCode: icdCode,
            id: diseaseIdArray && index < diseaseIdArray.length ? diseaseIdArray[index] : null
            icdName: icdName
          })
          index++
        }
      }
      
      console.log('从字符串解析完成,数量:', diseases.length)
      this.selectedDiseases = diseases
      console.log('--- parseDiseaseList 结束 ---\n')
    },
    
    // 选择转出医院地址
@@ -889,6 +739,14 @@
    },
    
    buildSubmitData() {
      // 确保 selectedDiseases 和 selectedStaff 不为 null(修复:防止迭代 null 导致错误)
      if (!this.selectedDiseases) {
        this.selectedDiseases = []
      }
      if (!this.selectedStaff) {
        this.selectedStaff = []
      }
      // 合并病情信息:选中的ICD-10疾病 + 其他描述
      let conditionText = ''
      if (this.selectedDiseases.length > 0) {
@@ -909,47 +767,46 @@
        }
      }
      
      // 构建提交数据 - 使用与 TaskCreateVO 一致的结构
      const submitData = {
        taskId: this.taskId,
        taskType: 'EMERGENCY_TRANSFER',
        deptId: this.selectedOrganizationId,
        vehicleIds: this.selectedVehicleId ? [this.selectedVehicleId] : [],
        plannedStartTime: this.taskForm.transferTime,
        // 出发地使用 departureAddress(如果有),否则使用转出医院地址
        // 出发地地址和坐标(使用转出医院地址)
        departureAddress: this.departureAddress || this.taskForm.hospitalOut.address,
        // 目标地使用转入医院地址
        departureLongitude: this.departureLongitude || (this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lng : null),
        departureLatitude: this.departureLatitude || (this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lat : null),
        // 目标地地址和坐标(使用转入医院地址)
        destinationAddress: this.taskForm.hospitalIn.address,
        destinationLongitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lng : null,
        destinationLatitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lat : null,
        // 转运距离和价格(主任务字段)
        distance: this.taskForm.transferDistance ? parseFloat(this.taskForm.transferDistance) : null,
        price: this.taskForm.price ? parseFloat(this.taskForm.price) : null,
        // 病情ID列表(用于同步调度单的OrdICD_ID参数)
        diseaseIds: this.selectedDiseases.map(d => d.id).filter(id => id !== null),
        // 执行人员列表
        assignees: this.selectedStaff.map(staff => ({
        // 执行人员列表(确保不为 null)
        assignees: (this.selectedStaff && Array.isArray(this.selectedStaff)) ? this.selectedStaff.map(staff => ({
          userId: staff.userId,
          userName: staff.nickName,
          userType: staff.type
        })),
        emergencyInfo: {
          patientContact: this.taskForm.patient.contact,
          patientPhone: this.taskForm.patient.phone,
          patientName: this.taskForm.patient.name,
          patientGender: this.taskForm.patient.gender,
          patientIdCard: this.taskForm.patient.idCard,
          patientCondition: conditionText, // 使用合并后的病情信息
          hospitalOutName: this.taskForm.hospitalOut.name,
          hospitalOutDepartment: this.taskForm.hospitalOut.department,
          hospitalOutBedNumber: this.taskForm.hospitalOut.bedNumber,
          hospitalOutAddress: this.taskForm.hospitalOut.address,
          // 转出医院GPS坐标(保存到扩展表)
          hospitalOutLongitude: this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lng : null,
          hospitalOutLatitude: this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lat : null,
          hospitalInName: this.taskForm.hospitalIn.name,
          hospitalInDepartment: this.taskForm.hospitalIn.department,
          hospitalInBedNumber: this.taskForm.hospitalIn.bedNumber,
          hospitalInAddress: this.taskForm.hospitalIn.address,
          // 转入医院GPS坐标(保存到扩展表)
          hospitalInLongitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lng : null,
          hospitalInLatitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lat : null,
          transferDistance: this.taskForm.transferDistance ? parseFloat(this.taskForm.transferDistance) : null,
          transferPrice: this.taskForm.price ? parseFloat(this.taskForm.price) : null,
        })) : [],
        // 患者信息(嵌套对象)
        patient: {
          contact: this.taskForm.patient.contact,
          phone: this.taskForm.patient.phone,
          name: this.taskForm.patient.name,
          gender: this.taskForm.patient.gender,
          idCard: this.taskForm.patient.idCard,
          condition: conditionText, // 使用合并后的病情信息
          // 病情详细信息(过滤掉空的病情名称)
          diseases: this.selectedDiseases
            .filter(d => d.icdName && d.icdName.trim())
@@ -958,16 +815,32 @@
              icdCode: d.icdCode,
              icdName: d.icdName
            }))
        },
        // 转出医院信息(嵌套对象)
        hospitalOut: {
          id: this.taskForm.hospitalOut.id,
          name: this.taskForm.hospitalOut.name,
          address: this.taskForm.hospitalOut.address,
          longitude: this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lng : null,
          latitude: this.addressCoordinates.hospitalOutAddress ? this.addressCoordinates.hospitalOutAddress.lat : null,
          department: this.taskForm.hospitalOut.department,
          departmentId: this.taskForm.hospitalOut.departmentId,
          bedNumber: this.taskForm.hospitalOut.bedNumber
        },
        // 转入医院信息(嵌套对象)
        hospitalIn: {
          id: this.taskForm.hospitalIn.id,
          name: this.taskForm.hospitalIn.name,
          address: this.taskForm.hospitalIn.address,
          longitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lng : null,
          latitude: this.addressCoordinates.hospitalInAddress ? this.addressCoordinates.hospitalInAddress.lat : null,
          department: this.taskForm.hospitalIn.department,
          departmentId: this.taskForm.hospitalIn.departmentId,
          bedNumber: this.taskForm.hospitalIn.bedNumber
        }
      }
      // 出发地GPS坐标
      if (this.departureLongitude && this.departureLatitude) {
        submitData.departureLongitude = this.departureLongitude
        submitData.departureLatitude = this.departureLatitude
      }
      // 目标地GPS坐标由后端自动获取
      
      return submitData
    },
@@ -1343,133 +1216,6 @@
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
  .staff-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    display: flex;
    flex-direction: column;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
    .search-bar {
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .search-input {
        height: 70rpx;
        padding: 0 20rpx;
        background-color: #f5f5f5;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
    }
    .filter-tabs {
      display: flex;
      padding: 20rpx 30rpx;
      gap: 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .filter-tab {
        flex: 1;
        height: 60rpx;
        line-height: 60rpx;
        text-align: center;
        background-color: #f5f5f5;
        border-radius: 10rpx;
        font-size: 28rpx;
        color: #666;
        &.active {
          background-color: #007AFF;
          color: white;
        }
      }
    }
    .staff-list-scroll {
      flex: 1;
      padding: 20rpx 30rpx;
      .staff-list-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx;
        margin-bottom: 15rpx;
        background-color: #f8f8f8;
        border-radius: 10rpx;
        .staff-item-info {
          flex: 1;
          .staff-item-name {
            display: block;
            font-size: 30rpx;
            color: #333;
            margin-bottom: 8rpx;
          }
          .staff-item-dept {
            display: block;
            font-size: 24rpx;
            color: #999;
          }
        }
        .staff-item-check {
          width: 50rpx;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
      .empty-tip {
        text-align: center;
        padding: 100rpx 0;
        color: #999;
        font-size: 28rpx;
      }
    }
    .popup-footer {
      padding: 20rpx 30rpx;
      border-top: 1rpx solid #f0f0f0;
      .confirm-btn {
        width: 100%;
        height: 80rpx;
        background-color: #007AFF;
        color: white;
        border-radius: 10rpx;
        font-size: 32rpx;
      }
    }
  }
app/pagesTask/edit-welfare.vue
@@ -171,6 +171,13 @@
      selectedVehicleId: null,
      selectedOrganization: '',
      mapSelectorType: '',
      // 扩展 addressCoordinates 支持多种键名
      addressCoordinates: {
        start: null,
        end: null,
        startAddress: null,
        endAddress: null
      },
      taskForm: {
        serviceTime: '',
        passenger: {
@@ -237,16 +244,22 @@
        
        // 设置地址坐标(使用mixin中的方法)
        if (this.taskDetail.departureLongitude && this.taskDetail.departureLatitude) {
          this.setStartLocation({
          const startLocation = {
            lng: this.taskDetail.departureLongitude,
            lat: this.taskDetail.departureLatitude
          })
          }
          this.setStartLocation(startLocation)
          // 同时保存到 startAddress 键
          this.addressCoordinates.startAddress = startLocation
        }
        if (this.taskDetail.destinationLongitude && this.taskDetail.destinationLatitude) {
          this.setEndLocation({
          const endLocation = {
            lng: this.taskDetail.destinationLongitude,
            lat: this.taskDetail.destinationLatitude
          })
          }
          this.setEndLocation(endLocation)
          // 同时保存到 endAddress 键
          this.addressCoordinates.endAddress = endLocation
        }
      }).catch(error => {
        console.error('加载任务详情失败:', error)
@@ -288,10 +301,14 @@
      
      if (this.mapSelectorType === 'startAddress') {
        this.taskForm.startAddress = formattedAddress
        this.setLocationByAddress('start', address)
        const location = this.setLocationByAddress('start', address)
        // 同时保存到 startAddress 键
        this.addressCoordinates.startAddress = location
      } else if (this.mapSelectorType === 'endAddress') {
        this.taskForm.endAddress = formattedAddress
        this.setLocationByAddress('end', address)
        const location = this.setLocationByAddress('end', address)
        // 同时保存到 endAddress 键
        this.addressCoordinates.endAddress = location
      }
      
      // 监听mixin中的距离计算完成事件
@@ -345,29 +362,32 @@
    },
    
    buildSubmitData() {
      // 构建提交数据 - 使用与 TaskCreateVO 一致的结构
      const submitData = {
        taskId: this.taskId,
        taskType: 'WELFARE',
        vehicleIds: this.selectedVehicleId ? [this.selectedVehicleId] : [],
        plannedStartTime: this.taskForm.serviceTime,
        welfareInfo: {
          passengerContact: this.taskForm.passenger.contact,
          passengerPhone: this.taskForm.passenger.phone,
          pickupAddress: this.taskForm.startAddress,
          destinationAddress: this.taskForm.endAddress,
          serviceDistance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
          servicePrice: this.taskForm.price ? parseFloat(this.taskForm.price) : null
        // 出发地地址和坐标
        departureAddress: this.taskForm.startAddress,
        departureLongitude: this.addressCoordinates.startAddress ? this.addressCoordinates.startAddress.lng : null,
        departureLatitude: this.addressCoordinates.startAddress ? this.addressCoordinates.startAddress.lat : null,
        // 目标地地址和坐标
        destinationAddress: this.taskForm.endAddress,
        destinationLongitude: this.addressCoordinates.endAddress ? this.addressCoordinates.endAddress.lng : null,
        destinationLatitude: this.addressCoordinates.endAddress ? this.addressCoordinates.endAddress.lat : null,
        // 距离和价格(主任务字段)
        distance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
        price: this.taskForm.price ? parseFloat(this.taskForm.price) : null,
        // 乘客信息(嵌套对象)
        passenger: {
          contact: this.taskForm.passenger.contact,
          phone: this.taskForm.passenger.phone
        }
      }
      if (this.addressCoordinates.startAddress) {
        submitData.departureLongitude = this.addressCoordinates.startAddress.lng
        submitData.departureLatitude = this.addressCoordinates.startAddress.lat
      }
      if (this.addressCoordinates.endAddress) {
        submitData.destinationLongitude = this.addressCoordinates.endAddress.lng
        submitData.destinationLatitude = this.addressCoordinates.endAddress.lat
      }
      
      return submitData
app/pagesTask/edit.vue
@@ -150,6 +150,13 @@
      taskId: null,
      taskDetail: null,
      mapSelectorType: '',
      // 扩展 addressCoordinates 支持多种键名
      addressCoordinates: {
        start: null,
        end: null,
        startLocation: null,
        endLocation: null
      },
      taskForm: {
        taskDescription: '',
        taskType: '',
@@ -213,16 +220,22 @@
        
        // 设置地址坐标(使用mixin中的方法)
        if (this.taskDetail.departureLongitude && this.taskDetail.departureLatitude) {
          this.setStartLocation({
          const startLocation = {
            lng: this.taskDetail.departureLongitude,
            lat: this.taskDetail.departureLatitude
          })
          }
          this.setStartLocation(startLocation)
          // 同时保存到 startLocation 键
          this.addressCoordinates.startLocation = startLocation
        }
        if (this.taskDetail.destinationLongitude && this.taskDetail.destinationLatitude) {
          this.setEndLocation({
          const endLocation = {
            lng: this.taskDetail.destinationLongitude,
            lat: this.taskDetail.destinationLatitude
          })
          }
          this.setEndLocation(endLocation)
          // 同时保存到 endLocation 键
          this.addressCoordinates.endLocation = endLocation
        }
      }).catch(error => {
        console.error('加载任务详情失败:', error)
@@ -264,10 +277,14 @@
      
      if (this.mapSelectorType === 'startLocation') {
        this.taskForm.startLocation = formattedAddress
        this.setLocationByAddress('start', address)
        const location = this.setLocationByAddress('start', address)
        // 同时保存到 startLocation 键
        this.addressCoordinates.startLocation = location
      } else if (this.mapSelectorType === 'endLocation') {
        this.taskForm.endLocation = formattedAddress
        this.setLocationByAddress('end', address)
        const location = this.setLocationByAddress('end', address)
        // 同时保存到 endLocation 键
        this.addressCoordinates.endLocation = location
      }
      
      // 监听mixin中的距离计算完成事件
@@ -323,6 +340,7 @@
    },
    
    buildSubmitData() {
      // 构建提交数据 - 使用与 TaskCreateVO 一致的结构
      const submitData = {
        taskId: this.taskId,
        taskDescription: this.taskForm.taskDescription,
@@ -330,20 +348,21 @@
        vehicleIds: this.taskForm.vehicleId ? [this.taskForm.vehicleId] : [],
        plannedStartTime: this.taskForm.plannedStartTime,
        plannedEndTime: this.taskForm.plannedEndTime,
        // 出发地地址和坐标
        departureAddress: this.taskForm.startLocation,
        departureLongitude: this.addressCoordinates.startLocation ? this.addressCoordinates.startLocation.lng : null,
        departureLatitude: this.addressCoordinates.startLocation ? this.addressCoordinates.startLocation.lat : null,
        // 目标地地址和坐标
        destinationAddress: this.taskForm.endLocation,
        estimatedDistance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
        destinationLongitude: this.addressCoordinates.endLocation ? this.addressCoordinates.endLocation.lng : null,
        destinationLatitude: this.addressCoordinates.endLocation ? this.addressCoordinates.endLocation.lat : null,
        // 距离(主任务字段)
        distance: this.taskForm.distance ? parseFloat(this.taskForm.distance) : null,
        remark: this.taskForm.remark
      }
      if (this.addressCoordinates.startLocation) {
        submitData.departureLongitude = this.addressCoordinates.startLocation.lng
        submitData.departureLatitude = this.addressCoordinates.startLocation.lat
      }
      if (this.addressCoordinates.endLocation) {
        submitData.destinationLongitude = this.addressCoordinates.endLocation.lng
        submitData.destinationLatitude = this.addressCoordinates.endLocation.lat
      }
      
      return submitData
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentNotifyService.java
@@ -62,7 +62,7 @@
            // 通信成功且业务成功
            if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
                // 查询订单和交易(使用业务订单号查询)
                PaymentOrder order = paymentOrderMapper.selectByBizOrderIdAndChannel(outTradeNo, "WECHAT");
                PaymentOrder order = paymentOrderMapper.selectById(Long.parseLong(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
@@ -134,7 +134,7 @@
            // 交易成功
            if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
                // 查询订单和交易(使用业务订单号查询)
                PaymentOrder order = paymentOrderMapper.selectByBizOrderIdAndChannel(outTradeNo, "ALIPAY");
                PaymentOrder order = paymentOrderMapper.selectById(Long.parseLong(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
@@ -183,6 +183,276 @@
    }
    /**
     * 处理第三方支付宝回调
     * 第三方支付宝通过GET方式回调,参数通过URL传递
     *
     * 参数格式:
     * method: 方法名
     * APPID: 应用ID
     * PaidMoneyType: 支付类型
     * PaidMoney: 支付金额(分)
     * PaidRemarks: 支付备注(包含transaction_id)
     * UnixTime: 时间戳
     * Sign: 签名
     */
    @Transactional(rollbackFor = Exception.class)
    public void processAlipayThirdPartyNotify(Map<String, String> params) {
        String method = params.get("method"); // 方法名
        String appId = params.get("APPID"); // 应用ID
        String paidMoneyType = params.get("PaidMoneyType"); // 支付类型
        String paidMoney = params.get("PaidMoney"); // 支付金额(分)
        String paidRemarks = params.get("PaidRemarks"); // 支付备注(可能包含订单号和交易号)
        String unixTime = params.get("UnixTime"); // 时间戳
        String sign = params.get("Sign"); // 签名
        log.info("处理第三方支付宝回调,method: {}, APPID: {}, PaidMoneyType: {}, PaidMoney: {}, PaidRemarks: {}, UnixTime: {}, Sign: {}",
                method, appId, paidMoneyType, paidMoney, paidRemarks, unixTime, sign);
        log.info("第三方支付宝回调完整参数: {}", params);
        // 从PaidRemarks中提取订单号(out_trade_no)
        // PaidRemarks格式可能是: "订单号+交易号" 或其他格式,需要解析
        String outTradeNo = extractOutTradeNo(paidRemarks);
        String transactionId = extractTransactionId(paidRemarks);
        if (outTradeNo == null || outTradeNo.isEmpty()) {
            log.error("无法从PaidRemarks中提取订单号: {}", paidRemarks);
            return;
        }
        log.info("解析出订单号: {}, 交易号: {}", outTradeNo, transactionId);
        // 检查幂等性(使用订单号或签名)
        String notifyId = transactionId != null ? transactionId : sign;
        if (isNotifyProcessed("ALIPAY_THIRD_PARTY", notifyId)) {
            log.info("第三方支付宝回调已处理过,标识: {}", notifyId);
            return;
        }
        // 记录回调日志
        NotifyLog notifyLog = saveNotifyLog("ALIPAY_THIRD_PARTY", notifyId, JSON.toJSONString(params), true);
        try {
            // 第三方支付宝成功的判断条件(根据实际情况调整)
            // 通常只要能收到回调就表示支付成功
            if (paidMoney != null && Integer.parseInt(paidMoney) > 0) {
                // 查询订单(使用商户订单号,这里的outTradeNo实际是我们的订单ID)
                PaymentOrder order = paymentOrderMapper.selectById(Long.valueOf(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
                    return;
                }
                // 验证金额是否匹配
                if (!order.getAmount().equals(Integer.parseInt(paidMoney))) {
                    log.error("订单金额不匹配,订单金额: {}, 回调金额: {}", order.getAmount(), paidMoney);
                    updateNotifyLog(notifyLog.getId(), false, "金额不匹配");
                    return;
                }
                // 查询待支付交易
                PaymentTransaction transaction = paymentTransactionMapper.selectPendingByOrderId(order.getId());
                if (transaction == null) {
                    // 如果没有待支付交易,尝试查询最新交易
                    transaction = paymentTransactionMapper.selectLatestByOrderId(order.getId());
                    if (transaction == null || "SUCCEEDED".equals(transaction.getStatus())) {
                        log.warn("交易已完成或不存在,订单ID: {}", order.getId());
                        updateNotifyLog(notifyLog.getId(), true, "交易已完成");
                        return;
                    }
                }
                // 更新交易状态
                transaction.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    transaction.setChannelTradeNo(transactionId);
                }
                transaction.setPaidAt(LocalDateTime.now());
                transaction.setResponseSnapshot(JSON.toJSONString(params));
                paymentTransactionMapper.update(transaction);
                // 更新订单状态
                order.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    order.setChannelTradeNo(transactionId);
                }
                order.setPaidAt(LocalDateTime.now());
                order.setLatestTransactionId(transaction.getId());
                paymentOrderMapper.update(order);
                // 更新通知日志
                notifyLog.setOrderId(order.getId());
                notifyLog.setTransactionId(transaction.getId());
                updateNotifyLog(notifyLog.getId(), true, "处理成功");
                // 触发业务回调
                bizCallbackService.triggerCallback(order, transaction);
                log.info("第三方支付宝回调处理成功,订单ID: {}, 交易ID: {}", order.getId(), transaction.getId());
            } else {
                log.warn("第三方支付宝支付金额异常: {}", paidMoney);
                updateNotifyLog(notifyLog.getId(), true, "金额异常: " + paidMoney);
            }
        } catch (Exception e) {
            log.error("处理第三方支付宝回调异常", e);
            updateNotifyLog(notifyLog.getId(), false, "处理异常: " + e.getMessage());
            throw e;
        }
    }
    /**
     * 从PaidRemarks中提取订单号
     * PaidRemarks可能包含订单号和交易号,需要解析
     */
    private String extractOutTradeNo(String paidRemarks) {
        if (paidRemarks == null || paidRemarks.isEmpty()) {
            return null;
        }
        // 假设PaidRemarks格式: "订单号+交易号" 或 "订单号"
        // 根据实际格式调整解析逻辑
        // 这里假设订单号在最前面,可能用某个分隔符分隔
        // 尝试提取纯数字作为订单ID
        String[] parts = paidRemarks.split("[^0-9]+");
        for (String part : parts) {
            if (part.length() > 10) { // 订单ID通常是长整型,长度较长
                return part;
            }
        }
        // 如果没有找到,返回整个字符串的数字部分
        return paidRemarks.replaceAll("[^0-9]", "");
    }
    /**
     * 从PaidRemarks中提取交易号
     */
    private String extractTransactionId(String paidRemarks) {
        if (paidRemarks == null || paidRemarks.isEmpty()) {
            return null;
        }
        // 如果PaidRemarks包含支付宝交易号(通常以2开头的28位数字)
        // 这里简单返回null,可以根据实际格式调整
        return null;
    }
    /**
     * 处理第三方微信回调
     * 第三方微信通过GET方式回调,参数通过URL传递
     *
     * 参数格式:
     * method: 方法名
     * APPID: 应用ID
     * PaidMoneyType: 支付类型
     * PaidMoney: 支付金额(分)
     * PaidRemarks: 支付备注(包含transaction_id)
     * UnixTime: 时间戳
     */
    @Transactional(rollbackFor = Exception.class)
    public void processWechatThirdPartyNotify(Map<String, String> params) {
        String method = params.get("method"); // 方法名
        String appId = params.get("APPID"); // 应用ID
        String paidMoneyType = params.get("PaidMoneyType"); // 支付类型
        String paidMoney = params.get("PaidMoney"); // 支付金额(分)
        String paidRemarks = params.get("PaidRemarks"); // 支付备注(可能包含订单号和交易号)
        String unixTime = params.get("UnixTime"); // 时间戳
        log.info("处理第三方微信回调,method: {}, APPID: {}, PaidMoneyType: {}, PaidMoney: {}, PaidRemarks: {}, UnixTime: {}",
                method, appId, paidMoneyType, paidMoney, paidRemarks, unixTime);
        log.info("第三方微信回调完整参数: {}", params);
        // 从PaidRemarks中提取订单号(out_trade_no)
        String outTradeNo = extractOutTradeNo(paidRemarks);
        String transactionId = extractTransactionId(paidRemarks);
        if (outTradeNo == null || outTradeNo.isEmpty()) {
            log.error("无法从PaidRemarks中提取订单号: {}", paidRemarks);
            return;
        }
        log.info("解析出订单号: {}, 交易号: {}", outTradeNo, transactionId);
        // 检查幂等性(使用订单号或时间戳)
        String notifyId = transactionId != null ? transactionId : (outTradeNo + "_" + unixTime);
        if (isNotifyProcessed("WECHAT_THIRD_PARTY", notifyId)) {
            log.info("第三方微信回调已处理过,标识: {}", notifyId);
            return;
        }
        // 记录回调日志
        NotifyLog notifyLog = saveNotifyLog("WECHAT_THIRD_PARTY", notifyId, JSON.toJSONString(params), true);
        try {
            // 第三方微信成功的判断条件(通常只要能收到回调就表示支付成功)
            if (paidMoney != null && Integer.parseInt(paidMoney) > 0) {
                // 查询订单(使用商户订单号,这里的outTradeNo实际是我们的订单ID)
                PaymentOrder order = paymentOrderMapper.selectById(Long.valueOf(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
                    return;
                }
                // 验证金额是否匹配
                if (!order.getAmount().equals(Integer.parseInt(paidMoney))) {
                    log.error("订单金额不匹配,订单金额: {}, 回调金额: {}", order.getAmount(), paidMoney);
                    updateNotifyLog(notifyLog.getId(), false, "金额不匹配");
                    return;
                }
                // 查询待支付交易
                PaymentTransaction transaction = paymentTransactionMapper.selectPendingByOrderId(order.getId());
                if (transaction == null) {
                    // 如果没有待支付交易,尝试查询最新交易
                    transaction = paymentTransactionMapper.selectLatestByOrderId(order.getId());
                    if (transaction == null || "SUCCEEDED".equals(transaction.getStatus())) {
                        log.warn("交易已完成或不存在,订单ID: {}", order.getId());
                        updateNotifyLog(notifyLog.getId(), true, "交易已完成");
                        return;
                    }
                }
                // 更新交易状态
                transaction.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    transaction.setChannelTradeNo(transactionId);
                }
                transaction.setPaidAt(LocalDateTime.now());
                transaction.setResponseSnapshot(JSON.toJSONString(params));
                paymentTransactionMapper.update(transaction);
                // 更新订单状态
                order.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    order.setChannelTradeNo(transactionId);
                }
                order.setPaidAt(LocalDateTime.now());
                order.setLatestTransactionId(transaction.getId());
                paymentOrderMapper.update(order);
                // 更新通知日志
                notifyLog.setOrderId(order.getId());
                notifyLog.setTransactionId(transaction.getId());
                updateNotifyLog(notifyLog.getId(), true, "处理成功");
                // 触发业务回调
                bizCallbackService.triggerCallback(order, transaction);
                log.info("第三方微信回调处理成功,订单ID: {}, 交易ID: {}", order.getId(), transaction.getId());
            } else {
                log.warn("第三方微信支付金额异常: {}", paidMoney);
                updateNotifyLog(notifyLog.getId(), true, "金额异常: " + paidMoney);
            }
        } catch (Exception e) {
            log.error("处理第三方微信回调异常", e);
            updateNotifyLog(notifyLog.getId(), false, "处理异常: " + e.getMessage());
            throw e;
        }
    }
    /**
     * 检查通知是否已处理(幂等性)
     */
    private boolean isNotifyProcessed(String channel, String notifyId) {
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentService.java
@@ -12,6 +12,7 @@
import com.ruoyi.payment.infrastructure.channel.wechat.WxPayV2Client;
import com.ruoyi.payment.infrastructure.config.AlipayConfig;
import com.ruoyi.payment.infrastructure.config.QrCodeConfig;
import com.ruoyi.payment.infrastructure.config.WechatPayConfig;
import com.ruoyi.payment.infrastructure.persistence.mapper.PaymentOrderMapper;
import com.ruoyi.payment.infrastructure.persistence.mapper.PaymentTransactionMapper;
import com.ruoyi.payment.infrastructure.util.QrCodeUtil;
@@ -60,6 +61,9 @@
    @Autowired
    private AlipayConfig alipayConfig;
    @Autowired
    private WechatPayConfig wechatPayConfig;
    /**
     * 发起微信Native支付
     */
@@ -99,6 +103,7 @@
        requestParams.put("bizOrderId", request.getBizOrderId());
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("notifyUrl",wechatPayConfig.getNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -150,6 +155,7 @@
        requestParams.put("bizOrderId", request.getBizOrderId());
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("notifyUrl",alipayConfig.getNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -202,6 +208,7 @@
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("thirdParty", true);
        requestParams.put("notifyUrl",alipayConfig.getThirdParty().getDefaultNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -283,11 +290,11 @@
    private String callAlipayThirdPartyPrecreate(PaymentOrder order, PaymentRequest request) {
        try {
            // 使用AlipayConfig中配置的回调地址
            String notifyUrl = alipayConfig.getNotifyUrl();
            String notifyUrl = alipayConfig.getThirdParty().getDefaultNotifyUrl();
            String outTradeNo = String.valueOf(order.getId());
            Integer totalFee = order.getAmount(); // 单位:分
            String serviceOrdId = request.getBizOrderId(); // 业务订单ID
            return alipayThirdPartyClient.createQrCodeUrl(notifyUrl, outTradeNo, totalFee, serviceOrdId);
        } catch (Exception e) {
            log.error("调用第三方支付宝接口失败", e);
@@ -335,4 +342,298 @@
            throw new RuntimeException("查询交易状态失败: " + e.getMessage(), e);
        }
    }
}
    /**
     * 轮询查询支付状态(用于回调失败的补偿机制)
     * 支持微信和支付宝
     *
     * @param orderId 订单ID
     * @return 轮询结果(包含订单状态和交易状态)
     */
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> pollPaymentStatus(Long orderId) {
        log.info("开始轮询查询支付状态,订单ID: {}", orderId);
        Map<String, Object> result = new HashMap<>();
        // 1. 查询订单信息
        PaymentOrder order = paymentOrderMapper.selectById(orderId);
        if (order == null) {
            log.error("订单不存在,订单ID: {}", orderId);
            throw new RuntimeException("订单不存在");
        }
        // 如果订单已经是成功状态,直接返回
        if (OrderStatus.SUCCEEDED.getCode().equals(order.getStatus())) {
            log.info("订单已成功,无需轮询,订单ID: {}", orderId);
            result.put("orderStatus", order.getStatus());
            result.put("needPoll", false);
            result.put("message", "订单已成功");
            return result;
        }
        // 2. 查询最新交易记录
        PaymentTransaction transaction = paymentTransactionMapper.selectLatestByOrderId(orderId);
        if (transaction == null) {
            log.error("未找到交易记录,订单ID: {}", orderId);
            throw new RuntimeException("未找到交易记录");
        }
        // 如果交易已成功,但订单状态未更新,更新订单状态
        if (TransactionStatus.SUCCEEDED.getCode().equals(transaction.getStatus())) {
            log.info("交易已成功,同步订单状态,订单ID: {}", orderId);
            order.setStatus(OrderStatus.SUCCEEDED.getCode());
            order.setUpdatedAt(LocalDateTime.now());
            paymentOrderMapper.update(order);
            result.put("orderStatus", OrderStatus.SUCCEEDED.getCode());
            result.put("transactionStatus", transaction.getStatus());
            result.put("needPoll", false);
            result.put("message", "支付成功");
            return result;
        }
        // 3. 根据支付渠道调用相应的查询接口
        String channel = order.getChannel();
        boolean paymentSuccess = false;
        String tradeStatus = null;
        try {
            if (PayChannel.WECHAT.getCode().equals(channel)) {
                // 查询微信支付状态
                paymentSuccess = queryWechatPaymentStatus(order, transaction);
                tradeStatus = paymentSuccess ? "SUCCESS" : "NOTPAY";
                result.put("paymethod","WECHAT");
            } else if (PayChannel.ALIPAY.getCode().equals(channel)) {
                // 判断是否为第三方支付宝
                boolean isThirdParty = isThirdPartyAlipay(transaction);
                result.put("paymethod","ALIPAY");
                if (isThirdParty) {
                    // 查询第三方支付宝状态
                    tradeStatus = queryAlipayThirdPartyTradeStatus(orderId);
                    paymentSuccess = "SUCCESS".equals(tradeStatus);
                } else {
                    // 查询官方支付宝状态
                    paymentSuccess = queryAlipayPaymentStatus(order, transaction);
                    tradeStatus = paymentSuccess ? "TRADE_SUCCESS" : "WAIT_BUYER_PAY";
                }
            } else {
                log.error("不支持的支付渠道: {}", channel);
                throw new RuntimeException("不支持的支付渠道");
            }
        } catch (Exception e) {
            log.error("查询支付状态异常,订单ID: {}", orderId, e);
            result.put("orderStatus", order.getStatus());
            result.put("transactionStatus", transaction.getStatus());
            result.put("needPoll", true);
            result.put("error", e.getMessage());
            result.put("message", "查询异常,请稍后重试");
            return result;
        }
        // 4. 如果支付成功,更新订单和交易状态
        if (paymentSuccess) {
            log.info("轮询查询发现支付成功,订单ID: {}, 交易状态: {}", orderId, tradeStatus);
            // 更新交易状态
            transaction.setStatus(TransactionStatus.SUCCEEDED.getCode());
            transaction.setPaidAt(LocalDateTime.now());
            paymentTransactionMapper.update(transaction);
            // 更新订单状态
            order.setStatus(OrderStatus.SUCCEEDED.getCode());
            order.setUpdatedAt(LocalDateTime.now());
            paymentOrderMapper.update(order);
            result.put("orderStatus", OrderStatus.SUCCEEDED.getCode());
            result.put("transactionStatus", TransactionStatus.SUCCEEDED.getCode());
            result.put("tradeStatus", tradeStatus);
            result.put("needPoll", false);
            result.put("message", "支付成功");
            log.info("轮询查询处理完成,订单和交易状态已更新,订单ID: {}", orderId);
        } else {
            log.info("轮询查询,支付尚未完成,订单ID: {}, 交易状态: {}", orderId, tradeStatus);
            result.put("orderStatus", order.getStatus());
            result.put("transactionStatus", transaction.getStatus());
            result.put("tradeStatus", tradeStatus);
            result.put("needPoll", true);
            result.put("message", "支付尚未完成,请继续轮询");
        }
        return result;
    }
    /**
     * 查询微信支付状态
     */
    private boolean queryWechatPaymentStatus(PaymentOrder order, PaymentTransaction transaction) throws Exception {
        log.info("查询微信支付状态,订单ID: {}", order.getId());
        String outTradeNo = String.valueOf(order.getId());
        Map<String, String> queryResult = wxPayV2Client.queryOrder(outTradeNo);
        // 检查业务结果
        if (!"SUCCESS".equals(queryResult.get("result_code"))) {
            log.warn("微信订单查询业务失败: {}", queryResult.get("err_code_des"));
            return false;
        }
        // 检查交易状态
        String tradeState = queryResult.get("trade_state");
        log.info("微信订单状态: {}", tradeState);
        return "SUCCESS".equals(tradeState);
    }
    /**
     * 查询支付宝支付状态(官方接口)
     */
    private boolean queryAlipayPaymentStatus(PaymentOrder order, PaymentTransaction transaction) throws Exception {
        log.info("查询支付宝支付状态,订单ID: {}", order.getId());
        String outTradeNo = String.valueOf(order.getId());
        com.alipay.api.response.AlipayTradeQueryResponse queryResult = alipayF2FClient.queryOrder(outTradeNo);
        // 检查交易状态
        String tradeStatus = queryResult.getTradeStatus();
        log.info("支付宝订单状态: {}", tradeStatus);
        log.info("支付宝订单查询结果: {}", queryResult.getBody());
        // TRADE_SUCCESS 或 TRADE_FINISHED 表示支付成功
        return "TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus);
    }
    /**
     * 判断是否为第三方支付宝
     */
    private boolean isThirdPartyAlipay(PaymentTransaction transaction) {
        if (transaction.getRequestParams() == null) {
            return false;
        }
        try {
            Map<String, Object> params = JSON.parseObject(transaction.getRequestParams(), Map.class);
            return params.containsKey("thirdParty") && Boolean.TRUE.equals(params.get("thirdParty"));
        } catch (Exception e) {
            log.warn("解析交易请求参数失败", e);
            return false;
        }
    }
    /**
     * 根据交易单号查询微信支付状态
     *
     * @param outTradeNo 商户订单号(交易单号)
     * @return 交易状态信息
     */
    public Map<String, Object> queryWechatTradeByOutTradeNo(String outTradeNo) {
        log.info("根据交易单号查询微信支付状态,商户订单号: {}", outTradeNo);
        Map<String, Object> result = new HashMap<>();
        try {
            Map<String, String> queryResult = wxPayV2Client.queryOrder(outTradeNo);
            // 检查调用是否成功
            if (!"SUCCESS".equals(queryResult.get("return_code"))) {
                result.put("success", false);
                result.put("message", "微信查询接口调用失败: " + queryResult.get("return_msg"));
                return result;
            }
            // 检查业务结果
            if (!"SUCCESS".equals(queryResult.get("result_code"))) {
                result.put("success", false);
                result.put("message", "微信订单查询业务失败: " + queryResult.get("err_code_des"));
                result.put("errorCode", queryResult.get("err_code"));
                return result;
            }
            // 返回交易信息
            result.put("success", true);
            result.put("tradeState", queryResult.get("trade_state")); // 交易状态
            result.put("tradeStateDesc", queryResult.get("trade_state_desc")); // 交易状态描述
            result.put("outTradeNo", queryResult.get("out_trade_no")); // 商户订单号
            result.put("transactionId", queryResult.get("transaction_id")); // 微信支付订单号
            result.put("totalFee", queryResult.get("total_fee")); // 订单金额(分)
            result.put("timeEnd", queryResult.get("time_end")); // 支付完成时间
            result.put("openid", queryResult.get("openid")); // 用户标识
            // 判断是否支付成功
            boolean isPaid = "SUCCESS".equals(queryResult.get("trade_state"));
            result.put("isPaid", isPaid);
            result.put("message", isPaid ? "支付成功" : "支付未完成");
            log.info("微信交易查询成功,交易状态: {}", queryResult.get("trade_state"));
        } catch (Exception e) {
            log.error("查询微信交易状态异常,商户订单号: {}", outTradeNo, e);
            result.put("success", false);
            result.put("message", "查询异常: " + e.getMessage());
        }
        return result;
    }
    /**
     * 根据交易单号查询支付宝支付状态
     *
     * @param outTradeNo 商户订单号(交易单号)
     * @return 交易状态信息
     */
    public Map<String, Object> queryAlipayTradeByOutTradeNo(String outTradeNo) {
        log.info("根据交易单号查询支付宝支付状态,商户订单号: {}", outTradeNo);
        Map<String, Object> result = new HashMap<>();
        try {
            com.alipay.api.response.AlipayTradeQueryResponse queryResult = alipayF2FClient.queryOrder(outTradeNo);
            // 检查调用是否成功
            if (!queryResult.isSuccess()) {
                result.put("success", false);
                result.put("message", "支付宝查询失败: " + queryResult.getSubMsg());
                result.put("errorCode", queryResult.getSubCode());
                return result;
            }
            // 返回交易信息
            result.put("success", true);
            result.put("tradeStatus", queryResult.getTradeStatus()); // 交易状态
            result.put("outTradeNo", queryResult.getOutTradeNo()); // 商户订单号
            result.put("tradeNo", queryResult.getTradeNo()); // 支付宝交易号
            result.put("totalAmount", queryResult.getTotalAmount()); // 订单金额(元)
            result.put("buyerPayAmount", queryResult.getBuyerPayAmount()); // 买家实际支付金额
            result.put("sendPayDate", queryResult.getSendPayDate()); // 交易支付时间
            result.put("buyerLogonId", queryResult.getBuyerLogonId()); // 买家支付宝账号
            // 判断是否支付成功
            String tradeStatus = queryResult.getTradeStatus();
            boolean isPaid = "TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus);
            result.put("isPaid", isPaid);
            // 状态说明
            String message;
            if ("TRADE_SUCCESS".equals(tradeStatus)) {
                message = "交易支付成功";
            } else if ("TRADE_FINISHED".equals(tradeStatus)) {
                message = "交易结束,不可退款";
            } else if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
                message = "交易创建,等待买家付款";
            } else if ("TRADE_CLOSED".equals(tradeStatus)) {
                message = "未付款交易超时关闭,或支付完成后全额退款";
            } else {
                message = "未知状态: " + tradeStatus;
            }
            result.put("message", message);
            log.info("支付宝交易查询成功,交易状态: {}", tradeStatus);
        } catch (Exception e) {
            log.error("查询支付宝交易状态异常,商户订单号: {}", outTradeNo, e);
            result.put("success", false);
            result.put("message", "查询异常: " + e.getMessage());
        }
        return result;
    }
}
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/AlipayConfig.java
@@ -48,7 +48,7 @@
        /** 第三方接口URL */
        private String url = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
        /** 默认回调地址 */
        private String defaultNotifyUrl = "https://dsp.966120.com.cn/alipay/pay_notify";
        private String defaultNotifyUrl = "https://api.966120.com.cn/alipay/DspNotifyUrl.php";
        /** 超时时间(毫秒) */
        private int timeout = 30000;
    }
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/ThirdPartyConfig.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.payment.infrastructure.config;
import lombok.Data;
import java.io.Serializable;
@Data
public class ThirdPartyConfig implements Serializable {
    /** 是否启用 */
    private boolean enabled = false;
    /** 第三方接口URL */
    private String url = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
    /** 默认回调地址 */
    private String defaultNotifyUrl = "https://api.966120.com.cn/alipay/DspNotifyUrl.php";
    /** 超时时间(毫秒) */
    private int timeout = 30000;
}
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/WechatPayConfig.java
@@ -33,4 +33,10 @@
    /** 签名类型 */
    private String signType = "MD5";
    /**
     * 第三方支付配置
     */
    private ThirdPartyConfig thirdParty = new ThirdPartyConfig();
}
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentController.java
@@ -33,6 +33,7 @@
    /**
     * 发起微信Native支付
     */
    @Anonymous()
    @PostMapping("/wechat/native")
    public AjaxResult createWechatNativePayment(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -47,6 +48,7 @@
    /**
     * 发起支付宝当面付
     */
    @Anonymous()
    @PostMapping("/alipay/precreate")
    public AjaxResult createAlipayPrecreate(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -61,6 +63,7 @@
    /**
     * 发起支付宝当面付(第三方接口)
     */
    @Anonymous()
    @PostMapping("/alipay/thirdparty/precreate")
    public AjaxResult createAlipayThirdPartyPrecreate(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -75,6 +78,7 @@
    /**
     * 查询订单
     */
    @Anonymous()
    @GetMapping("/orders/{orderId}")
    public AjaxResult getOrder(@PathVariable Long orderId) {
        try {
@@ -92,6 +96,7 @@
    /**
     * 查询最新交易
     */
    @Anonymous()
    @GetMapping("/orders/{orderId}/transactions/latest")
    public AjaxResult getLatestTransaction(@PathVariable Long orderId) {
        try {
@@ -109,6 +114,7 @@
    /**
     * 查询支付宝第三方交易状态
     */
    @Anonymous()
    @GetMapping("/alipay/thirdparty/query/{orderId}")
    public AjaxResult queryAlipayThirdPartyTradeStatus(@PathVariable Long orderId) {
        try {
@@ -119,4 +125,92 @@
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
}
    /**
     * 轮询查询支付状态(用于回调失败的补偿机制)
     * <p>
     * 这个接口用于在支付回调未成功时,主动轮询查询支付状态。
     * 支持微信和支付宝(包括第三方支付宝)。
     * <p>
     * 返回结果包含:
     * - orderStatus: 订单状态
     * - transactionStatus: 交易状态
     * - tradeStatus: 第三方交易状态
     * - needPoll: 是否需要继续轮询
     * - message: 提示信息
     * <p>
     * 建议轮询策略:
     * 1. 首次轮询:立即轮询
     * 2. 后续轮询:间隔3-5秒
     * 3. 最多轮询20次,约100秒后停止
     * 4. 如果 needPoll=false,表示支付已完成或确认失败,无需继续轮询
     */
    @Anonymous()
    @GetMapping("/poll/{orderId}")
    public AjaxResult pollPaymentStatus(@PathVariable Long orderId) {
        try {
            java.util.Map<String, Object> result = paymentService.pollPaymentStatus(orderId);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("轮询查询支付状态失败", e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
    /**
     * 根据交易单号查询微信支付状态
     * <p>
     * 该接口用于通过商户订单号(交易单号)直接查询微信支付状态。
     * <p>
     * 返回结果包含:
     * - success: 查询是否成功
     * - isPaid: 是否已支付
     * - tradeState: 交易状态(SUCCESS-支付成功,NOTPAY-未支付,CLOSED-已关闭等)
     * - transactionId: 微信支付订单号
     * - totalFee: 订单金额(分)
     * - timeEnd: 支付完成时间
     * - message: 提示信息
     *
     * @param outTradeNo 商户订单号(交易单号)
     */
    @Anonymous()
    @GetMapping("/wechat/query/{outTradeNo}")
    public AjaxResult queryWechatTrade(@PathVariable String outTradeNo) {
        try {
            java.util.Map<String, Object> result = paymentService.queryWechatTradeByOutTradeNo(outTradeNo);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("查询微信交易状态失败,交易单号: {}", outTradeNo, e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
    /**
     * 根据交易单号查询支付宝支付状态
     * <p>
     * 该接口用于通过商户订单号(交易单号)直接查询支付宝支付状态。
     * <p>
     * 返回结果包含:
     * - success: 查询是否成功
     * - isPaid: 是否已支付
     * - tradeStatus: 交易状态(TRADE_SUCCESS-支付成功,WAIT_BUYER_PAY-等待付款,TRADE_CLOSED-已关闭等)
     * - tradeNo: 支付宝交易号
     * - totalAmount: 订单金额(元)
     * - sendPayDate: 交易支付时间
     * - buyerLogonId: 买家支付宝账号
     * - message: 提示信息
     *
     * @param outTradeNo 商户订单号(交易单号)
     */
    @Anonymous()
    @GetMapping("/alipay/query/{outTradeNo}")
    public AjaxResult queryAlipayTrade(@PathVariable String outTradeNo) {
        try {
            java.util.Map<String, Object> result = paymentService.queryAlipayTradeByOutTradeNo(outTradeNo);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("查询支付宝交易状态失败,交易单号: {}", outTradeNo, e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
}
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentNotifyController.java
@@ -37,7 +37,7 @@
     * 微信支付回调
     */
    @Anonymous
    @PostMapping("/wechat")
    @PostMapping("/wechat/notify")
    public String wechatNotify(HttpServletRequest request) {
        try {
            log.info("接收到微信支付回调");
@@ -78,7 +78,7 @@
     * 支付宝回调
     */
    @Anonymous
    @PostMapping("/alipay")
    @PostMapping("/alipay/notify")
    public String alipayNotify(HttpServletRequest request) {
        try {
            log.info("接收到支付宝回调");
@@ -113,6 +113,105 @@
    }
    /**
     * 第三方支付宝回调(GET请求)
     * <p>
     * 第三方支付宝通过GET方式回调,参数通过URL传递
     * <p>
     * 参数格式:
     * method=xxx&APPID=xxx&PaidMoneyType=xxx&PaidMoney=xxx&PaidRemarks=xxx&UnixTime=xxx&Sign=xxx
     * <p>
     * 参数说明:
     * - method: 方法名
     * - APPID: 应用ID
     * - PaidMoneyType: 支付类型
     * - PaidMoney: 支付金额(单位:分)
     * - PaidRemarks: 支付备注(包含订单号和交易号信息)
     * - UnixTime: 时间戳
     * - Sign: 签名
     * <p>
     * 示例:
     * GET /api/pay/notify/alipay/thirdparty?method=alipay.pay&APPID=123456&PaidMoneyType=alipay&PaidMoney=100&PaidRemarks=1234567890&UnixTime=1638360000&Sign=abc123
     */
    @Anonymous
    @GetMapping("/alipay/thirdparty")
    public String alipayThirdPartyNotify(HttpServletRequest request) {
        try {
            log.info("接收到第三方支付宝回调(GET请求)");
            // 1. 获取所有参数
            Map<String, String> params = new HashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String name = parameterNames.nextElement();
                params.put(name, request.getParameter(name));
            }
            log.info("第三方支付宝回调参数: {}", params);
            // 2. 第三方支付宝不需要验签,或者根据第三方平台的签名规则进行验证
            // 这里直接处理,如果需要验签可以在Service层添加
            // 3. 处理回调(更新订单状态、触发业务回调)
            paymentNotifyService.processAlipayThirdPartyNotify(params);
            // 4. 返回第三方支付宝要求的应答
            return "success";
        } catch (Exception e) {
            log.error("处理第三方支付宝回调失败", e);
            return "fail";
        }
    }
    /**
     * 第三方微信回调(GET请求)
     * <p>
     * 第三方微信通过GET方式回调,参数通过URL传递
     * <p>
     * 参数格式:
     * method=xxx&APPID=xxx&PaidMoneyType=xxx&PaidMoney=xxx&PaidRemarks=xxx&UnixTime=xxx
     * <p>
     * 参数说明:
     * - method: 方法名
     * - APPID: 应用ID
     * - PaidMoneyType: 支付类型
     * - PaidMoney: 支付金额(单位:分)
     * - PaidRemarks: 支付备注(包含订单号和交易号信息)
     * - UnixTime: 时间戳
     * <p>
     * 示例:
     * GET /api/pay/notify/wechat/thirdparty?method=wechat.pay&APPID=123456&PaidMoneyType=wechat&PaidMoney=100&PaidRemarks=1234567890&UnixTime=1638360000
     */
    @Anonymous
    @GetMapping("/wechat/thirdparty")
    public String wechatThirdPartyNotify(HttpServletRequest request) {
        try {
            log.info("接收到第三方微信回调(GET请求)");
            // 1. 获取所有参数
            Map<String, String> params = new HashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String name = parameterNames.nextElement();
                params.put(name, request.getParameter(name));
            }
            log.info("第三方微信回调参数: {}", params);
            // 2. 第三方微信不需要验签,或者根据第三方平台的签名规则进行验证
            // 这里直接处理,如枟需要验签可以在Service层添加
            // 3. 处理回调(更新订单状态、触发业务回调)
            paymentNotifyService.processWechatThirdPartyNotify(params);
            // 4. 返回第三方微信要求的应答
            return "success";
        } catch (Exception e) {
            log.error("处理第三方微信回调失败", e);
            return "fail";
        }
    }
    /**
     * 读取请求体
     */
    private String readRequestBody(HttpServletRequest request) throws Exception {
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -136,7 +136,7 @@
    @Log(title = "任务管理", businessType = BusinessType.UPDATE)
    @PutMapping("/admin")
    public AjaxResult adminEdit(@RequestBody TaskUpdateVO updateVO) {
        return toAjax(sysTaskService.updateSysTask(updateVO));
        return toAjax(sysTaskService.updateSysTask(updateVO,false));
    }
    /**
@@ -145,7 +145,7 @@
    @Log(title = "任务修改", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult appEdit(@RequestBody TaskUpdateVO updateVO) {
        return toAjax(sysTaskService.updateSysTask(updateVO));
        return toAjax(sysTaskService.updateSysTask(updateVO,false));
    }
    /**
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskVehicleController.java
@@ -1,6 +1,8 @@
package com.ruoyi.web.controller.task;
import java.util.List;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -69,7 +71,9 @@
    @PostMapping("/assign/{taskId}")
    public AjaxResult assignVehicle(@PathVariable("taskId") Long taskId, @RequestBody AssignVehicleRequest request) {
        try {
            int result = sysTaskService.assignVehicleToTask(taskId, request.getVehicleId(), request.getRemark());
            Long userId= SecurityUtils.getUserId();
            String username = SecurityUtils.getUsername();
            int result = sysTaskService.assignVehicleToTask(taskId, request.getVehicleId(), request.getRemark(), userId, username);
            if (result > 0) {
                return success("分配成功");
            } else {
@@ -89,7 +93,9 @@
        try {
            // 设置请求对象中的taskId,确保参数一致性
            request.setTaskId(taskId);
            int result = sysTaskService.assignMultipleVehiclesToTask(request.getTaskId(), request.getVehicleIds(), request.getRemark());
            Long userId= SecurityUtils.getUserId();
            String username = SecurityUtils.getUsername();
            int result = sysTaskService.assignMultipleVehiclesToTask(request.getTaskId(), request.getVehicleIds(), request.getRemark(), userId, username);
            if (result > 0) {
                return success("批量分配成功,共分配 " + result + " 辆车");
            } else {
ruoyi-admin/src/main/resources/application.yml
@@ -205,9 +205,14 @@
    mchId: 1573728151
    appName: 民航医疗快线
    mchKey: Xz0ClPK3f5sCeT6SGTaha1vpVmyUFcbp
    notifyUrl: https://dsp.966120.com/api/pay/notify/wechat
    notifyUrl: https://dsp.966120.com.cn/api/pay/notify/wechat/notify
    signType: MD5
    checkSign: false
    thirdParty:
      enabled: false
      url: https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php
      defaultNotifyUrl: https://api.966120.com.cn/weixin/DspNotifyUrl.asp
      timeout: 30000
  
  # 支付宝配置
  alipay:
@@ -215,7 +220,7 @@
    privateKey: MIIEpgIBAAKCAQEArt273bWTEPXPjCsUYwFx7CNjhcQlm1NtbNjfeIsZ2g9sbFCQP9qpyufp6zkBv6eq+6WEztkC1KwSsuDjP5LvgY/1pmGFlr8r7cjeZI4bTeIe9jG5UaHolnzbdXUlSoInzgWRvbYXxuQZciwVpokwviW27YK9wPIzz9OTiRquL8b3YWPZLO7xK0gBMa2KfFfUXxCB8gHJtidQ+FjjYXb2WpnScKLJdKfWcGWFnyGiZOknyFR9kI8cm0cYPNHtecQId0bQ1ee7YDLD8dBPd2Pd/JBC4Wn6HuOvZOLqZvIpIj+8q0zGXjUUns6MsjNL3MUKuhKy6hQGwP5sGrPcVcwqiwIDAQABAoIBAQCTeW9iSSsRx61VUlOsN+DDPQlHHCh3OcH0ZWb6e52+2Pkg1EUDhT9jX3lZJsfBwf8iofJCnKSVhdVzRNSCnkIdq7KJsn9+phW/QYPFnE+MvKJOEZtwLDNDD2PqSHS9xM0bJHlIXNTqqR6IuoM740HXa2k+H+A2ZE2r/YzUuUqkASwAYPKYzWa1wivg4CZrvoPZ5bXvYOHoV0jZEtyUQB9dHuCz+bghbR+28vGkYwEGInLsOCe6Gl5D61F0l2qAXRQky3P0jXxIPXPFmhBYutAAUufLpryruQgL3MDDm5dhBJpwp89qwFDjc8fWVS/FFYJ0KDQOpAxAI800YHgaJ4VBAoGBAOdCcoYS49Q9Iula7gFGVXeto7QSUsP0CnQZM/tsAU6TiX3XG5pxBYoVmYSIQPylgagRTBZHD5t/LGa+I+KYMHSVAH/kndPdgTO7EiwfUCzi1DmGZVWs7XJ2zRfTmYRVdsElPy3Z8Pm802jd7mhffw/5p6y9pzNJqOjmGOUbYGrrAoGBAMGS1CGHkK4nD2BsIWJoKW2Lxph3Bq5hN1iaE8ZjOAvFT9drNqfwRa6BVGyfYEXPZTvUT0FUNMdukcTCM6O3FRU8EABJIcVue2QA+BvkfwPEU3JxMA8DrWHO7IcgtG9wjbxretDsf+SZmkQgK0ZUPod5ZZSycFxM3/GiXDQjGhbhAoGBAL/Y/+j6AscvcKbmKEwmbQC7q/LWwJKPAZ0Oy3DoSK1G9+jNarjUyiOjh5fK8R6mrskekGBq0yfMeKlDU8HHP2t3sNJodgYs2+JubsTrtTeHdUfDlo1cyB8NL1d00wZVYA8bNy5yftavLzLv6bfsgRxfoBpNu0dw9A9B06U88N/BAoGBAKCN/nEJFlG8iB570Xzj1GjOJJzVLK96ZwOQWJKWPShWMhEFFkJZIhLJppKp5ppAmUD0qgAPre80oKdIRLin5E7GkKcMAXzWVHXv79qCvW8MagJkK25oqGiVzs2NrNs5yfXcV/PuFW4wkSmsXPhqa6rGYCDjmBqWkLDE8CE2dC9BAoGBAJ2+QfAAvB27mr48+vY4HxZyPRrCBA1YkokraWQ0IlfD0MKsFw0Af4Iu3oy7V6NlE2GwL/AcObyHeGZt7DLbViAQOgmb9BpUrjUZ4bXSBuGPfRe11HCu6j6W67qa76TAoy3A0Dfm0OE/m9r4H99NaLzeBm1KluySKkfYXoqyueQw
    alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl8BjXYaudmGT+sEos0AXEUrKl6+wyw6++hBoJDdY7P+P7poa34GN4YSkkavTA/HplmRM1wTLcY+NqhxhLpNrcgy08AbC/GGcLM2wxQGFa+L+DQLz34uBAShXDK8yN6O49UdbbJ2RRaJSAb+nW4ZVCPMGtMu4S3lXTymQgizM1IYo9L92U5QPRzZRSP8+AmQPzwofRqEgvkO02s66xU2G5AAdkVg5BQm34eM0Io2CmcWF9jSoWQTJdyd7tw3oec9NqD7x3CfcsN3NAJOQLz4+bWWqDWelyviyAr2reeH6AuBVjaWwAvAJx3yuLevKMXTzPC95Ja7w4XYSB9Fg2+aKmwIDAQAB
    serverUrl: https://openapi.alipay.com/gateway.do
    notifyUrl: https://dsp.966120.com/api/pay/notify/alipay
    notifyUrl: https://dsp.966120.com.cn/api/pay/notify/alipay/notify
    signType: RSA2
    checkSign: false
    # 支付方式: OFFICIAL(官方支付宝) 或 THIRD_PARTY(第三方支付宝)
@@ -224,7 +229,8 @@
    thirdParty:
      enabled: true
      url: https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php
      defaultNotifyUrl: https://dsp.966120.com.cn/alipay/pay_notify
#      defaultNotifyUrl: https://api.966120.com.cn/alipay/DspNotifyUrl.php
      defaultNotifyUrl: https://dsp.966120.com.cn/api/pay/notify/alipay/notify
      timeout: 30000
  
  # 业务回调配置
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
@@ -228,7 +228,7 @@
     * cron表达式: 0 0/10 * * * ? (每10分钟执行一次)
     */
    public void syncAdditionalFeeToLegacy() {
        log.info("开始执行附加费用同步定时任务(新系统 -> 旧系统)");
        log.info("开始执行新系统附加费用同步定时任务(新系统 -> 旧系统)");
        try {
            int successCount = additionalFeeSyncService.batchSyncAdditionalFeeToLegacy();
            log.info("附加费用同步完成,成功同步: {} 条记录", successCount);
@@ -248,11 +248,11 @@
     * cron表达式: 0 0/15 * * * ? (每15分钟执行一次)
     */
    public void syncAdditionalFeeFromLegacy() {
        log.info("开始执行附加费用同步定时任务(旧系统 -> 新系统)");
        log.info("开始执行旧系统附加费用同步定时任务(旧系统 -> 新系统)");
        try {
            // 同步最近24小时内的记录
            int successCount = additionalFeeSyncService.batchSyncAdditionalFeeFromLegacy(24);
            log.info("附加费用同步完成,成功同步: {} 条记录", successCount);
            int successCount = additionalFeeSyncService.batchSyncAdditionalFeeFromLegacy(72);
            log.info("旧系统附加费用同步完成,成功同步: {} 条记录", successCount);
        } catch (Exception e) {
            log.error("附加费用同步异常", e);
        }
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/TaskUpdateVO.java
@@ -1,523 +1,24 @@
package com.ruoyi.system.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 任务更新对象
 * 任务更新对象(继承自TaskCreateVO)
 * 
 * @author ruoyi
 * @date 2024-01-15
 */
public class TaskUpdateVO {
@Data
@EqualsAndHashCode(callSuper = true)
public class TaskUpdateVO extends TaskCreateVO {
    
    /** 任务ID */
    /** 任务ID(必填,用于更新指定任务) */
    private Long taskId;
    /** 任务描述 */
    private String taskDescription;
    /** 出发地址 */
    private String departureAddress;
    /** 目的地址 */
    private String destinationAddress;
    /** 计划开始时间 */
    /** 更新时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedStartTime;
    /** 计划结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date plannedEndTime;
    /** 执行人ID */
    private Long assigneeId;
    /** 备注 */
    private String remark;
    /** 出发地经度 */
    private BigDecimal departureLongitude;
    /** 出发地纬度 */
    private BigDecimal departureLatitude;
    /** 目的地经度 */
    private BigDecimal destinationLongitude;
    /** 目的地纬度 */
    private BigDecimal destinationLatitude;
    /** 任务类型 */
    private String taskType;
    /** 部门ID */
    private Long deptId;
    /** 车辆ID列表 */
    private List<Long> vehicleIds;
    /** 病情ID列表(用于同步调度单的OrdICD_ID参数) */
    private List<Long> diseaseIds;
    /** 执行人员列表(包含角色类型) */
    private List<AssigneeInfo> assignees;
    /** 急救转运任务扩展信息 */
    private EmergencyInfoVO emergencyInfo;
    public Long getTaskId() {
        return taskId;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public String getTaskDescription() {
        return taskDescription;
    }
    public void setTaskDescription(String taskDescription) {
        this.taskDescription = taskDescription;
    }
    public String getDepartureAddress() {
        return departureAddress;
    }
    public void setDepartureAddress(String departureAddress) {
        this.departureAddress = departureAddress;
    }
    public String getDestinationAddress() {
        return destinationAddress;
    }
    public void setDestinationAddress(String destinationAddress) {
        this.destinationAddress = destinationAddress;
    }
    public Date getPlannedStartTime() {
        return plannedStartTime;
    }
    public void setPlannedStartTime(Date plannedStartTime) {
        this.plannedStartTime = plannedStartTime;
    }
    public Date getPlannedEndTime() {
        return plannedEndTime;
    }
    public void setPlannedEndTime(Date plannedEndTime) {
        this.plannedEndTime = plannedEndTime;
    }
    public Long getAssigneeId() {
        return assigneeId;
    }
    public void setAssigneeId(Long assigneeId) {
        this.assigneeId = assigneeId;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public BigDecimal getDepartureLongitude() {
        return departureLongitude;
    }
    public void setDepartureLongitude(BigDecimal departureLongitude) {
        this.departureLongitude = departureLongitude;
    }
    public BigDecimal getDepartureLatitude() {
        return departureLatitude;
    }
    public void setDepartureLatitude(BigDecimal departureLatitude) {
        this.departureLatitude = departureLatitude;
    }
    public BigDecimal getDestinationLongitude() {
        return destinationLongitude;
    }
    public void setDestinationLongitude(BigDecimal destinationLongitude) {
        this.destinationLongitude = destinationLongitude;
    }
    public BigDecimal getDestinationLatitude() {
        return destinationLatitude;
    }
    public void setDestinationLatitude(BigDecimal destinationLatitude) {
        this.destinationLatitude = destinationLatitude;
    }
    public String getTaskType() {
        return taskType;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public Long getDeptId() {
        return deptId;
    }
    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }
    public List<Long> getVehicleIds() {
        return vehicleIds;
    }
    public void setVehicleIds(List<Long> vehicleIds) {
        this.vehicleIds = vehicleIds;
    }
    public List<Long> getDiseaseIds() {
        return diseaseIds;
    }
    public void setDiseaseIds(List<Long> diseaseIds) {
        this.diseaseIds = diseaseIds;
    }
    public EmergencyInfoVO getEmergencyInfo() {
        return emergencyInfo;
    }
    public void setEmergencyInfo(EmergencyInfoVO emergencyInfo) {
        this.emergencyInfo = emergencyInfo;
    }
    public List<AssigneeInfo> getAssignees() {
        return assignees;
    }
    public void setAssignees(List<AssigneeInfo> assignees) {
        this.assignees = assignees;
    }
    /**
     * 急救转运任务扩展信息内部类
     */
    public static class EmergencyInfoVO {
        /** 患者联系人 */
        private String patientContact;
        /** 患者联系电话 */
        private String patientPhone;
        /** 患者姓名 */
        private String patientName;
        /** 患者性别 */
        private String patientGender;
        /** 患者身份证号 */
        private String patientIdCard;
        /** 患者病情描述 */
        private String patientCondition;
        /** 转出医院ID */
        private Long hospitalOutId;
        /** 转出医院名称 */
        private String hospitalOutName;
        /** 转出医院科室 */
        private String hospitalOutDepartment;
        /** 转出医院科室ID */
        private String hospitalOutDepartmentId;
        /** 转出医院床号 */
        private String hospitalOutBedNumber;
        /** 转出医院地址 */
        private String hospitalOutAddress;
        /** 转出医院经度 */
        private BigDecimal hospitalOutLongitude;
        /** 转出医院纬度 */
        private BigDecimal hospitalOutLatitude;
        /** 转入医院ID */
        private Long hospitalInId;
        /** 转入医院名称 */
        private String hospitalInName;
        /** 转入医院科室 */
        private String hospitalInDepartment;
        /** 转入医院科室ID */
        private String hospitalInDepartmentId;
        /** 转入医院床号 */
        private String hospitalInBedNumber;
        /** 转入医院地址 */
        private String hospitalInAddress;
        /** 转入医院经度 */
        private BigDecimal hospitalInLongitude;
        /** 转入医院纬度 */
        private BigDecimal hospitalInLatitude;
        /** 转运公里数 */
        private BigDecimal transferDistance;
        /** 转运费用 */
        private BigDecimal transferPrice;
        // Getters and Setters
        public String getPatientContact() {
            return patientContact;
        }
        public void setPatientContact(String patientContact) {
            this.patientContact = patientContact;
        }
        public String getPatientPhone() {
            return patientPhone;
        }
        public void setPatientPhone(String patientPhone) {
            this.patientPhone = patientPhone;
        }
        public String getPatientName() {
            return patientName;
        }
        public void setPatientName(String patientName) {
            this.patientName = patientName;
        }
        public String getPatientGender() {
            return patientGender;
        }
        public void setPatientGender(String patientGender) {
            this.patientGender = patientGender;
        }
        public String getPatientIdCard() {
            return patientIdCard;
        }
        public void setPatientIdCard(String patientIdCard) {
            this.patientIdCard = patientIdCard;
        }
        public String getPatientCondition() {
            return patientCondition;
        }
        public void setPatientCondition(String patientCondition) {
            this.patientCondition = patientCondition;
        }
        public Long getHospitalOutId() {
            return hospitalOutId;
        }
        public void setHospitalOutId(Long hospitalOutId) {
            this.hospitalOutId = hospitalOutId;
        }
        public String getHospitalOutName() {
            return hospitalOutName;
        }
        public void setHospitalOutName(String hospitalOutName) {
            this.hospitalOutName = hospitalOutName;
        }
        public String getHospitalOutDepartment() {
            return hospitalOutDepartment;
        }
        public void setHospitalOutDepartment(String hospitalOutDepartment) {
            this.hospitalOutDepartment = hospitalOutDepartment;
        }
        public String getHospitalOutDepartmentId() {
            return hospitalOutDepartmentId;
        }
        public void setHospitalOutDepartmentId(String hospitalOutDepartmentId) {
            this.hospitalOutDepartmentId = hospitalOutDepartmentId;
        }
        public String getHospitalOutBedNumber() {
            return hospitalOutBedNumber;
        }
        public void setHospitalOutBedNumber(String hospitalOutBedNumber) {
            this.hospitalOutBedNumber = hospitalOutBedNumber;
        }
        public String getHospitalOutAddress() {
            return hospitalOutAddress;
        }
        public void setHospitalOutAddress(String hospitalOutAddress) {
            this.hospitalOutAddress = hospitalOutAddress;
        }
        public BigDecimal getHospitalOutLongitude() {
            return hospitalOutLongitude;
        }
        public void setHospitalOutLongitude(BigDecimal hospitalOutLongitude) {
            this.hospitalOutLongitude = hospitalOutLongitude;
        }
        public BigDecimal getHospitalOutLatitude() {
            return hospitalOutLatitude;
        }
        public void setHospitalOutLatitude(BigDecimal hospitalOutLatitude) {
            this.hospitalOutLatitude = hospitalOutLatitude;
        }
        public Long getHospitalInId() {
            return hospitalInId;
        }
        public void setHospitalInId(Long hospitalInId) {
            this.hospitalInId = hospitalInId;
        }
        public String getHospitalInName() {
            return hospitalInName;
        }
        public void setHospitalInName(String hospitalInName) {
            this.hospitalInName = hospitalInName;
        }
        public String getHospitalInDepartment() {
            return hospitalInDepartment;
        }
        public void setHospitalInDepartment(String hospitalInDepartment) {
            this.hospitalInDepartment = hospitalInDepartment;
        }
        public String getHospitalInDepartmentId() {
            return hospitalInDepartmentId;
        }
        public void setHospitalInDepartmentId(String hospitalInDepartmentId) {
            this.hospitalInDepartmentId = hospitalInDepartmentId;
        }
        public String getHospitalInBedNumber() {
            return hospitalInBedNumber;
        }
        public void setHospitalInBedNumber(String hospitalInBedNumber) {
            this.hospitalInBedNumber = hospitalInBedNumber;
        }
        public String getHospitalInAddress() {
            return hospitalInAddress;
        }
        public void setHospitalInAddress(String hospitalInAddress) {
            this.hospitalInAddress = hospitalInAddress;
        }
        public BigDecimal getHospitalInLongitude() {
            return hospitalInLongitude;
        }
        public void setHospitalInLongitude(BigDecimal hospitalInLongitude) {
            this.hospitalInLongitude = hospitalInLongitude;
        }
        public BigDecimal getHospitalInLatitude() {
            return hospitalInLatitude;
        }
        public void setHospitalInLatitude(BigDecimal hospitalInLatitude) {
            this.hospitalInLatitude = hospitalInLatitude;
        }
        public BigDecimal getTransferDistance() {
            return transferDistance;
        }
        public void setTransferDistance(BigDecimal transferDistance) {
            this.transferDistance = transferDistance;
        }
        public BigDecimal getTransferPrice() {
            return transferPrice;
        }
        public void setTransferPrice(BigDecimal transferPrice) {
            this.transferPrice = transferPrice;
        }
    }
    /**
     * 执行人员信息内部类
     */
    public static class AssigneeInfo {
        /** 用户ID */
        private Long userId;
        /** 用户姓名 */
        private String userName;
        /** 用户类型(角色):driver-司机, doctor-医生, nurse-护士 */
        private String userType;
        public Long getUserId() {
            return userId;
        }
        public void setUserId(Long userId) {
            this.userId = userId;
        }
        public String getUserName() {
            return userName;
        }
        public void setUserName(String userName) {
            this.userName = userName;
        }
        public String getUserType() {
            return userType;
        }
        public void setUserType(String userType) {
            this.userType = userType;
        }
    }
    private Date updateTime;
}
ruoyi-system/src/main/java/com/ruoyi/system/service/IAdditionalFeeSyncService.java
@@ -1,5 +1,8 @@
package com.ruoyi.system.service;
import com.ruoyi.system.domain.PaidMoneyAdd;
import com.ruoyi.system.domain.SysTaskAdditionalFee;
/**
 * 附加费用同步Service接口
 * 
@@ -14,16 +17,16 @@
     * @param feeId 附加费用ID
     * @return 是否同步成功
     */
    boolean syncAdditionalFeeToLegacy(Long feeId);
    boolean syncAdditionalFeeToLegacy(SysTaskAdditionalFee fee);
    
    /**
     * 将旧系统PaidMoney_Add记录同步到新系统
     * 
     * @param paidMoneyAddId 旧系统附加费用记录ID
     * @param paidMoneyAdd 旧系统附加费用记录ID
     * @return 是否同步成功
     */
    boolean syncAdditionalFeeFromLegacy(Long paidMoneyAddId);
    boolean syncAdditionalFeeFromLegacy(PaidMoneyAdd paidMoneyAdd);
    /**
     * 批量同步新系统未同步的附加费用到旧系统
     * 
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -2,15 +2,13 @@
import java.util.Date;
import java.util.List;
import com.ruoyi.system.domain.vo.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskLog;
import com.ruoyi.system.domain.SysTaskVehicle;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.domain.vo.TaskStatisticsVO;
import com.ruoyi.system.domain.enums.TaskStatus;
/**
@@ -63,7 +61,22 @@
     * @param updateVO 任务更新对象
     * @return 结果
     */
    public int updateSysTask(TaskUpdateVO updateVO);
    public int updateSysTask(TaskUpdateVO updateVO,Boolean updateFromLegacy);
    /**
     * 修改任务管理(允许从外部传入用户信息、部门信息和时间信息) 用于从旧系统中同步过来
     * @param updateVO
     * @param serviceOrderId
     * @param dispatchOrderId
     * @param serviceOrdNo
     * @param userId
     * @param userName
     * @param deptId
     * @param createTime
     * @param updateTime
     * @return
     */
    public int updateTask(TaskUpdateVO updateVO, String serviceOrderId, String dispatchOrderId, String serviceOrdNo, Long userId, String userName, Long deptId, Date createTime, Date updateTime);
    /**
     * 批量删除任务管理
@@ -151,7 +164,7 @@
     * @param remark 备注
     * @return 结果
     */
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark);
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark,Long userId,String userName);
    /**
     * 取消任务车辆分配
@@ -170,7 +183,7 @@
     * @param remark 备注
     * @return 结果
     */
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark);
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark,Long userId,String userName);
    /**
     * 查询任务关联的车辆
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/AdditionalFeeSyncServiceImpl.java
@@ -1,6 +1,5 @@
package com.ruoyi.system.service.impl;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -9,11 +8,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.domain.PaidMoneyAdd;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskAdditionalFee;
@@ -92,19 +87,13 @@
    }
    
    @Override
    public boolean syncAdditionalFeeToLegacy(Long feeId) {
    public boolean syncAdditionalFeeToLegacy(SysTaskAdditionalFee fee) {
        Long feeId= fee.getId();
        try {
            // 1. 查询新系统附加费用记录
            SysTaskAdditionalFee fee = additionalFeeMapper.selectByTaskId(null).stream()
                .filter(f -> f.getId().equals(feeId))
                .findFirst()
                .orElse(null);
            
            if (fee == null) {
                log.info("新系统附加费用记录不存在,feeId: {}", feeId);
                return false;
            }
         // 1. 检查是否已同步
            // 2. 如果已同步过,跳过
            if (fee.getPid() != null && fee.getPid() > 0) {
                log.info("附加费用已同步,feeId: {}, pid: {}", feeId, fee.getPid());
@@ -172,44 +161,47 @@
            return false;
        }
    }
    /**
     * 从旧系统同步附加费用
     * @param paidMoneyAdd 旧系统附加费用记录ID
     * @return
     */
    @Override
    public boolean syncAdditionalFeeFromLegacy(Long paidMoneyAddId) {
    public boolean syncAdditionalFeeFromLegacy(PaidMoneyAdd paidMoneyAdd) {
        try {
            // 1. 查询旧系统PaidMoney_Add记录
            PaidMoneyAdd paidMoneyAdd = paidMoneyAddMapper.selectById(paidMoneyAddId);
            if (paidMoneyAdd == null) {
                log.error("旧系统附加费用记录不存在,paidMoneyAddId: {}", paidMoneyAddId);
                return false;
            }
            Long paidMoneyAddId = paidMoneyAdd.getId();
            log.info("1.开始同步旧系统附加费用,paidMoneyAddId: {}", paidMoneyAddId);
            // 2. 检查是否已同步过
            SysTaskAdditionalFee existFee = additionalFeeMapper.selectByPid(paidMoneyAddId);
            if (existFee != null) {
                log.info("旧系统附加费用记录已同步,paidMoneyAddId: {}, feeId: {}", paidMoneyAddId, existFee.getId());
                return true;
            }
            log.info("2.开始同步旧系统附加费用,paidMoneyAddId: {}", paidMoneyAddId);
            
            // 3. 根据ServiceOrdID查询新系统任务
            if (paidMoneyAdd.getToServiceOrdID() == null) {
                log.warn("旧系统附加费用记录缺少ServiceOrdID,paidMoneyAddId: {},跳过同步", paidMoneyAddId);
                return false;
            }
            log.info("3.开始同步旧系统附加费用,paidMoneyAddId: {}, ServiceOrdID: {}", paidMoneyAddId, paidMoneyAdd.getToServiceOrdID());
            
            SysTaskEmergency emergency = sysTaskEmergencyMapper.selectByLegacyServiceOrdId(paidMoneyAdd.getToServiceOrdID());
            if (emergency == null) {
                log.warn("未找到对应的转运任务,ServiceOrdID: {},跳过同步", paidMoneyAdd.getToServiceOrdID());
                log.warn("4.开始同步旧系统附加费用,未找到对应的转运任务,ServiceOrdID: {},跳过同步", paidMoneyAdd.getToServiceOrdID());
                return false;
            }
            log.info("4.开始同步旧系统附加费用,paidMoneyAddId: {}, ServiceOrdID: {}, DispatchOrdID: {}", paidMoneyAddId, paidMoneyAdd.getToServiceOrdID(), paidMoneyAdd.getToDispatchOrdID());
            // 4. 验证DispatchOrdID是否匹配
            if (paidMoneyAdd.getToDispatchOrdID() == null) {
                log.warn("旧系统附加费用记录缺少DispatchOrdID,paidMoneyAddId: {},跳过同步", paidMoneyAddId);
                return false;
            }
            log.info("5.开始同步旧系统附加费用,paidMoneyAddId: {}, ServiceOrdID: {}, DispatchOrdID: {}", paidMoneyAddId, paidMoneyAdd.getToServiceOrdID(), paidMoneyAdd.getToDispatchOrdID());
            if (!paidMoneyAdd.getToDispatchOrdID().equals(emergency.getLegacyDispatchOrdId())) {
                log.warn("转运任务DispatchOrdID不匹配,ServiceOrdID: {}, 附加费用DispatchOrdID: {} vs 任务DispatchOrdID: {},跳过同步",
                log.warn("5.转运任务DispatchOrdID不匹配,ServiceOrdID: {}, 附加费用DispatchOrdID: {} vs 任务DispatchOrdID: {},跳过同步",
                    paidMoneyAdd.getToServiceOrdID(), paidMoneyAdd.getToDispatchOrdID(), emergency.getLegacyDispatchOrdId());
                return false;
            }
@@ -228,19 +220,19 @@
            fee.setPid(paidMoneyAddId);
            fee.setSyncStatus(2); // 同步成功
            fee.setSyncTime(new Date());
            log.info("6.开始同步旧系统附加费用,paidMoneyAddId: {}, ServiceOrdID: {}, DispatchOrdID: {}, feeId: {}", paidMoneyAddId, paidMoneyAdd.getToServiceOrdID(), paidMoneyAdd.getToDispatchOrdID(), fee.getId());
            // 6. 插入新系统附加费用记录
            int result = additionalFeeMapper.insert(fee);
            if (result > 0) {
                log.info("旧系统附加费用记录同步到新系统成功,paidMoneyAddId: {}, feeId: {}", paidMoneyAddId, fee.getId());
                log.info("6.旧系统附加费用记录同步到新系统成功,paidMoneyAddId: {}, feeId: {}", paidMoneyAddId, fee.getId());
                return true;
            } else {
                log.error("插入新系统附加费用记录失败,paidMoneyAddId: {}", paidMoneyAddId);
                log.error("6.插入新系统附加费用记录失败,paidMoneyAddId: {}", paidMoneyAddId);
                return false;
            }
            
        } catch (Exception e) {
            log.error("同步旧系统附加费用记录到新系统异常,paidMoneyAddId: {}", paidMoneyAddId, e);
            log.error("8.同步旧系统附加费用记录到新系统异常,paidMoneyAddId: {}", paidMoneyAdd.getId(), e);
            return false;
        }
    }
@@ -256,7 +248,7 @@
            
            for (SysTaskAdditionalFee fee : unsyncedFees) {
                try {
                    if (syncAdditionalFeeToLegacy(fee.getId())) {
                    if (syncAdditionalFeeToLegacy(fee)) {
                        successCount++;
                    }
                    // 每条记录间隔1秒,避免过于频繁
@@ -290,7 +282,7 @@
            
            for (PaidMoneyAdd paidMoneyAdd : recentRecords) {
                try {
                    if (syncAdditionalFeeFromLegacy(paidMoneyAdd.getId())) {
                    if (syncAdditionalFeeFromLegacy(paidMoneyAdd)) {
                        successCount++;
                    }
                    // 每条记录间隔1秒,避免过于频繁
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DepartmentSyncServiceImpl.java
@@ -88,11 +88,32 @@
                if (parts.length != 2)
                {
                    log.warn("部门名称格式不正确,跳过: {}", fullName);
                    continue;
                   parts= fullName.split("-");
                   if(parts.length != 2) {
                       continue;
                   }
                }
                String part="";
                //只要发现有(新)或(新)就在branchName中加个新 字
                if(fullName.contains("(新)") || fullName.contains("(新)")) {
                    part="(新)";
                }
                String branchName = parts[0].trim() + "分公司";  // 湛江 -> 湛江分公司
                String deptName = parts[1].trim();              // 护士
                String cityName=parts[0].trim();
                String namePart=parts[0].trim();
                if(namePart.contains("(新)") || namePart.contains("(新)")) {
                    branchName=namePart+"分公司";
                    cityName=namePart;
                    deptName=parts[1].trim();
                }
                else{
                    branchName=namePart+part+"分公司";
                    cityName=namePart+ part;
                    deptName=parts[1].trim();
                }
                // 获取或创建分公司
                Long branchDeptId = branchMap.get(branchName);
@@ -107,7 +128,7 @@
                        branchMap.put(branchName, branchDeptId);
                        
                        // 检查并更新编码
                        syncOrderClassCodes(existingBranch, parts[0].trim(), serviceOrderList, dispatchOrderList,addressList);
                        syncOrderClassCodes(existingBranch, cityName, serviceOrderList, dispatchOrderList,addressList);
//                        existingBranch.setDepartmentId(dto.getDepartmentId());
                        sysDeptMapper.updateDept(existingBranch);
                        log.info("更新分公司编码: {}, 服务单编码: {}, 调度单编码: {}", 
@@ -126,7 +147,7 @@
//                        newBranch.setDepartmentId(dto.getDepartmentId());
                        // 自动匹配并设置服务单和调度单编码
                        syncOrderClassCodes(newBranch, parts[0].trim(), serviceOrderList, dispatchOrderList,addressList);
                        syncOrderClassCodes(newBranch, cityName, serviceOrderList, dispatchOrderList,addressList);
                        sysDeptMapper.insertDept(newBranch);
                        branchDeptId = newBranch.getDeptId();
@@ -479,6 +500,7 @@
        // 遍历编码列表,查找包含城市名称的项
        for (OrderClassDTO dto : orderClassList)
        {
            //有些加了新 TODO
            if (dto.getVtext() != null && dto.getVtext().contains(cityName))
            {
                log.debug("城市名称匹配成功 - 城市: {}, vtext: {}, vOrder2: {}", 
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
@@ -525,7 +525,7 @@
        params.put("ServiceOrdCoName", StringUtils.nvl(emergency.getPatientContact(), ""));
        params.put("ServiceOrdCoPhone", StringUtils.nvl(emergency.getPatientPhone(), ""));
        params.put("ServiceOrdPtName", StringUtils.nvl(emergency.getPatientName(), ""));
        params.put("ServiceOrdTraStreet",StringUtils.nvl(task.getDepartureAddress(), StringUtils.nvl(emergency.getHospitalOutAddress(), "")));
        // 地址信息
        params.put("DispatchOrdTraStreet", StringUtils.nvl(task.getDepartureAddress(), StringUtils.nvl(emergency.getHospitalOutAddress(), "")));
        params.put("DispatchOrdTraEnd", StringUtils.nvl(task.getDestinationAddress(), StringUtils.nvl(emergency.getHospitalInAddress(), "")));
@@ -1188,20 +1188,21 @@
            params.put("DispatchOrdID", emergency.getLegacyDispatchOrdId().toString());
            params.put("ServiceOrdID", emergency.getLegacyServiceOrdId().toString());
            params.put("DispatchOrdState", "3");
            log.info("重新同步调度单到旧系统请求参数: {}", params);
            // 发送HTTP请求到旧系统(使用admin_save_25.asp接口)
            String response = sendHttpPost(legacyConfig.getDispatchUpdateUrl(), params);
            log.info("重新同步调度单到旧系统响应: ServiceOrdID:{},DispatchOrdId:{},Result: {}",emergency.getLegacyServiceOrdId(),emergency.getLegacyDispatchOrdId(), response);
            // 解析响应
            Long dispatchOrdId = parseResponse(response);
//            Long dispatchOrdId = parseResponse(response);
            
            if (dispatchOrdId != null && dispatchOrdId > 0) {
            if (response != null && response.equals("OK")) {
                // 重新同步成功,清除重新同步标记
                emergency.setNeedResync(0);
                emergency.setDispatchSyncTime(new Date());
                emergency.setDispatchSyncErrorMsg(null);
                sysTaskEmergencyService.updateSysTaskEmergency(emergency);
                
                log.info("调度单重新同步成功,任务ID: {}, DispatchOrdID: {}", taskId, dispatchOrdId);
                log.info("调度单重新同步成功,任务ID: {}, DispatchOrdID: {}", taskId, emergency.getLegacyDispatchOrdId());
                return true;
            } else {
                // 重新同步失败
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -1,15 +1,13 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.VehicleInfo;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.domain.vo.TaskUpdateVO;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.service.ILegacyTransferSyncService;
@@ -19,9 +17,9 @@
import com.ruoyi.system.mapper.VehicleInfoMapper;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import org.apache.poi.ss.usermodel.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -31,7 +29,6 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
 * 旧系统转运单同步Service业务层处理
@@ -67,7 +64,9 @@
    @Autowired
    private IWechatTaskNotifyService wechatTaskNotifyService;
    /**
     * 同步指定日期范围的旧系统转运单到新系统
     * 
@@ -121,6 +120,8 @@
                    // 检查是否已同步
                    if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) {
                        log.debug("转运单已同步,跳过: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                        //进行更新操作
                        updateTransferOrder(serviceOrdID, dispatchOrdID, order);
                        continue;
                    }
                    
@@ -194,7 +195,71 @@
            return false;
        }
    }
    private boolean updateTransferOrder(String serviceOrdID, String dispatchOrdID, Map<String, Object> order){
        log.info("开始同步单个转运单: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
        String sysTaskCode="";
        try {
            SysTaskEmergency emergency=sysTaskEmergencyMapper.selectByLegacyServiceOrdId(Long.parseLong(serviceOrdID));
            if(emergency.getNeedResync().equals(1)){
                log.info("新系统需要同步到旧系统那里,所以不要同步旧数据到新系统,serviceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                return false;
            }
            // 构造TaskCreateVO对象
            TaskCreateVO createTaskVo = buildCreateTaskVo(serviceOrdID, dispatchOrdID, order);
            sysTaskCode = createTaskVo.getTaskCode();
            if (createTaskVo == null) {
                log.error("构造TaskCreateVO失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                return false;
            }
            // 记录创建的任务信息
            log.debug("准备创建任务: ServiceOrdID={}, DispatchOrdID={}, 患者姓名={}, 转出医院={}, 转入医院={}",
                    serviceOrdID, dispatchOrdID,
                    createTaskVo.getPatient() != null ? createTaskVo.getPatient().getName() : "未知",
                    createTaskVo.getHospitalOut() != null ? createTaskVo.getHospitalOut().getName() : "未知",
                    createTaskVo.getHospitalIn() != null ? createTaskVo.getHospitalIn().getName() : "未知");
            /**
             * 开单时间
             */
            Date ServiceOrd_CC_Time= getDateValue(order, "ServiceOrd_CC_Time");
            // 调用sysTaskService创建任务
            String serviceOrdClass = getStringValue(order,"ServiceOrdClass");
            String serviceOrdNo = getStringValue(order,"ServiceOrdNo");
            Integer oauserId=getIntegerValue(order,"ServiceOrd_NS_ID");
            SysUser sysUser=sysUserService.selectUserByOaUserId(oauserId);
            Long taskCreatorId=sysUser==null?null:sysUser.getUserId();
            String createUserName=sysUser==null?"system":sysUser.getUserName();
            SysDept dept=sysDeptService.selectDeptByServiceClass(serviceOrdClass);
            Long deptId=dept==null?null:dept.getDeptId();
            TaskUpdateVO updateTaskVo = new TaskUpdateVO();
            BeanUtils.copyProperties(createTaskVo, updateTaskVo);
            int result = sysTaskService.updateTask(updateTaskVo,serviceOrdID,dispatchOrdID, serviceOrdNo, taskCreatorId,createUserName, deptId, ServiceOrd_CC_Time, ServiceOrd_CC_Time);
            if (result > 0) {
                log.info("转运单同步成功: ServiceOrdID={}, DispatchOrdID={}, 创建的任务ID={}", serviceOrdID, dispatchOrdID, result);
                try {
                    notifyTransferOrderByWechat((long) result, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
                } catch (Exception e) {
                    log.error("转运单同步成功后发送微信通知失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
                }
                return true;
            } else {
                log.error("转运单同步失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                return false;
            }
        } catch (Exception e) {
            log.error("同步单个转运单异常: ServiceOrdID={}, DispatchOrdID={},sysTaskCode:{}", serviceOrdID, dispatchOrdID,sysTaskCode, e);
            return false;
        }
    }
    /**
     * 同步单个旧系统转运单到新系统(带详细信息)
     * 
@@ -359,18 +424,10 @@
            
            // 设置区域类型
            String serviceOrdAreaType = getStringValue(order, "ServiceOrdAreaType");
            if (StringUtils.isNotEmpty(serviceOrdAreaType)) {
                // 可以根据需要将区域类型映射到TaskCreateVO的其他字段
                log.debug("区域类型: {}", serviceOrdAreaType);
            }
            // 设置用户ID
            Long serviceOrdUserID = getLongValue(order, "ServiceOrdUserID");
            if (serviceOrdUserID != null) {
                // 可以根据需要将用户ID映射到TaskCreateVO的其他字段
                log.debug("用户ID: {}", serviceOrdUserID);
            }
            // 设置患者信息
            TaskCreateVO.PatientInfo patientInfo = new TaskCreateVO.PatientInfo();
            patientInfo.setName(getStringValue(order, "ServiceOrdPtName"));
@@ -414,9 +471,6 @@
            String hospitalOutDeptId = getStringValue(order, "ServiceOrdPtServicesID");
            //转出床位
            String serviceOrdPtServices=getStringValue(order, "ServiceOrdPtServices");
            hospitalOutInfo.setDepartmentId(hospitalOutDeptId);
            if (StringUtils.isNotEmpty(hospitalOutDeptId)) {
                String hospitalOutDeptName = legacyTransferSyncMapper.selectDepartmentNameByDeptID(hospitalOutDeptId);
@@ -424,6 +478,8 @@
                    hospitalOutInfo.setDepartment(hospitalOutDeptName);
                }
            }
            //转出床位
            String serviceOrdPtServices=getStringValue(order, "ServiceOrdPtServices");
            if(serviceOrdPtServices!= null){
                hospitalOutInfo.setBedNumber(serviceOrdPtServices);
            }
@@ -461,7 +517,7 @@
            createTaskVo.setHospitalIn(hospitalInInfo);
            
            // 设置地址信息
            createTaskVo.setDepartureAddress(getStringValue(order, "ServiceOrdTraVia"));
            createTaskVo.setDepartureAddress(getStringValue(order, "ServiceOrdTraStreet"));
            createTaskVo.setDestinationAddress(getStringValue(order, "ServiceOrdTraEnd"));
            
            // 设置价格和距离信息
@@ -471,6 +527,9 @@
            // 设置执行人信息
            List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
            createTaskVo.setAssignees(assignees);
            if(!assignees.isEmpty()){
                createTaskVo.setAssigneeId(assignees.get(0).getUserId());
            }
            
            // 设置车辆信息
            // 车辆ID需要根据DispatchOrdCarID查询获取
@@ -602,7 +661,7 @@
            if (assigneeList != null && !assigneeList.isEmpty()) {
                for (Map<String, Object> assigneeMap : assigneeList) {
                    String entourageOAId = getStringValue(assigneeMap, "EntourageOAId");
                    String entourageState = getStringValue(assigneeMap, "EntourageState");
                    String entourageState = getStringValue(assigneeMap, "EntourageID");
                    
                    if (StringUtils.isNotEmpty(entourageOAId)) {
                        try {
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskPaymentServiceImpl.java
@@ -175,7 +175,7 @@
        
        // 异步同步到旧系统
        try {
            additionalFeeSyncService.syncAdditionalFeeToLegacy(fee.getId());
            additionalFeeSyncService.syncAdditionalFeeToLegacy(fee);
        } catch (Exception e) {
            log.error("同步附加费用到旧系统失败", e);
        }
@@ -262,6 +262,7 @@
            
            // 生成回调地址
            String callbackUrl = callbackBaseUrl + "/payment/callback/" + provider.toLowerCase();
            payment.setCallbackUrl(callbackUrl);
            
            try {
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -8,6 +8,7 @@
import java.net.URL;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.domain.vo.*;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.utils.TaskCodeGenerator;
import com.ruoyi.common.config.ImageUrlConfig;
@@ -28,10 +29,6 @@
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.SysTaskWelfare;
import com.ruoyi.system.domain.SysTaskAssignee;
import com.ruoyi.system.domain.vo.TaskQueryVO;
import com.ruoyi.system.domain.vo.TaskCreateVO;
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.domain.VehicleInfo;
import com.ruoyi.system.service.ISysTaskService;
@@ -259,11 +256,11 @@
                taskVehicle.setTaskId(task.getTaskId());
                taskVehicle.setVehicleId(vehicleId);
                taskVehicle.setAssignTime(DateUtils.getNowDate());
                taskVehicle.setAssignBy(SecurityUtils.getUsername());
                taskVehicle.setAssignBy(username);
                taskVehicle.setStatus("ASSIGNED");
                taskVehicle.setCreateBy(SecurityUtils.getUsername());
                taskVehicle.setCreateBy(username);
                taskVehicle.setCreateTime(DateUtils.getNowDate());
                taskVehicle.setUpdateBy(SecurityUtils.getUsername());
                taskVehicle.setUpdateBy(username);
                taskVehicle.setUpdateTime(DateUtils.getNowDate());
                
                sysTaskVehicleMapper.insertSysTaskVehicle(taskVehicle);
@@ -549,49 +546,70 @@
     */
    @Override
    @Transactional
    public int updateSysTask(TaskUpdateVO updateVO) {
    public int updateSysTask(TaskUpdateVO updateVO, Boolean updateFromLegacy) {
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(updateVO.getTaskId());
        if (oldTask == null) {
            throw new RuntimeException("任务不存在");
        }
        String userName = SecurityUtils.getUsername();
        
        SysTask task = new SysTask();
        task.setTaskId(updateVO.getTaskId());
        task.setTaskDescription(updateVO.getTaskDescription());
        task.setPlannedStartTime(updateVO.getPlannedStartTime());
        task.setPlannedEndTime(updateVO.getPlannedEndTime());
        task.setAssigneeId(updateVO.getAssigneeId());
        task.setUpdateBy(userName);
        task.setUpdateTime(updateVO.getUpdateTime() != null ? updateVO.getUpdateTime() : DateUtils.getNowDate());
        task.setRemark(updateVO.getRemark());
        // 设置通用地址和坐标信息
        task.setDepartureAddress(updateVO.getDepartureAddress());
        task.setDestinationAddress(updateVO.getDestinationAddress());
        task.setDepartureLongitude(updateVO.getDepartureLongitude());
        task.setDepartureLatitude(updateVO.getDepartureLatitude());
        task.setDestinationLongitude(updateVO.getDestinationLongitude());
        task.setDestinationLatitude(updateVO.getDestinationLatitude());
        task.setPlannedStartTime(updateVO.getPlannedStartTime());
        task.setPlannedEndTime(updateVO.getPlannedEndTime());
        task.setAssigneeId(updateVO.getAssigneeId());
        task.setUpdateBy(SecurityUtils.getUsername());
        task.setUpdateTime(DateUtils.getNowDate());
        task.setRemark(updateVO.getRemark());
        // 设置预计距离
        if (updateVO.getEstimatedDistance() != null) {
            task.setEstimatedDistance(updateVO.getEstimatedDistance());
        } else if (updateVO.getTransferDistance() != null) {
            // 兼容急救转运字段
            task.setEstimatedDistance(updateVO.getTransferDistance());
        } else if (updateVO.getDistance() != null) {
            // 兼容福祉车字段
            task.setEstimatedDistance(updateVO.getDistance());
        }
        
        // 如果更新了部门ID
        if (updateVO.getDeptId() != null) {
            task.setDeptId(updateVO.getDeptId());
        }
        
        // 如果更新了任务编号
        if (updateVO.getTaskCode() != null) {
            task.setTaskCode(updateVO.getTaskCode());
        }
        // 自动获取出发地GPS坐标(如果更新了地址但缺失坐标)
        if (updateVO.getDepartureAddress() != null && 
            (updateVO.getDepartureLongitude() == null || updateVO.getDepartureLatitude() == null) && 
            mapService != null) {
            try {
                Map<String, Double> coords = mapService.geocoding(
                    updateVO.getDepartureAddress(),
                    extractCityFromAddress(updateVO.getDepartureAddress())
                );
                if (coords != null) {
                    task.setDepartureLongitude(BigDecimal.valueOf(coords.get("lng")));
                    task.setDepartureLatitude(BigDecimal.valueOf(coords.get("lat")));
                    log.info("出发地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
            if (!updateVO.getDepartureAddress().equals(oldTask.getDepartureAddress())) {
                try {
                    Map<String, Double> coords = mapService.geocoding(
                        updateVO.getDepartureAddress(),
                        extractCityFromAddress(updateVO.getDepartureAddress())
                    );
                    if (coords != null) {
                        task.setDepartureLongitude(BigDecimal.valueOf(coords.get("lng")));
                        task.setDepartureLatitude(BigDecimal.valueOf(coords.get("lat")));
                        log.info("出发地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                    }
                } catch (Exception e) {
                    log.error("自动获取出发地GPS坐标失败", e);
                }
            } catch (Exception e) {
                log.error("自动获取出发地GPS坐标失败", e);
            }
        }
        
@@ -599,28 +617,27 @@
        if (updateVO.getDestinationAddress() != null && 
            (updateVO.getDestinationLongitude() == null || updateVO.getDestinationLatitude() == null) && 
            mapService != null) {
            try {
                Map<String, Double> coords = mapService.geocoding(
                    updateVO.getDestinationAddress(),
                    extractCityFromAddress(updateVO.getDestinationAddress())
                );
                if (coords != null) {
                    task.setDestinationLongitude(BigDecimal.valueOf(coords.get("lng")));
                    task.setDestinationLatitude(BigDecimal.valueOf(coords.get("lat")));
                    log.info("目的地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
            if (!updateVO.getDestinationAddress().equals(oldTask.getDestinationAddress())) {
                try {
                    Map<String, Double> coords = mapService.geocoding(
                        updateVO.getDestinationAddress(),
                        extractCityFromAddress(updateVO.getDestinationAddress())
                    );
                    if (coords != null) {
                        task.setDestinationLongitude(BigDecimal.valueOf(coords.get("lng")));
                        task.setDestinationLatitude(BigDecimal.valueOf(coords.get("lat")));
                        log.info("目的地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                    }
                } catch (Exception e) {
                    log.error("自动获取目的地GPS坐标失败", e);
                }
            } catch (Exception e) {
                log.error("自动获取目的地GPS坐标失败", e);
            }
        }
        // 重新计算预计公里数
        calculateEstimatedDistance(task);
        // 用于跟踪是否需要重新同步(车辆、人员、地址、成交价变更)
        boolean needResync = true;
        int result = sysTaskMapper.updateSysTask(task);
        
        // 用于跟踪是否需要重新同步(车辆、人员、地址、成交价变更)
        boolean needResync = false;
        
        // 更新车辆关联
        if (result > 0 && updateVO.getVehicleIds() != null && !updateVO.getVehicleIds().isEmpty()) {
@@ -640,7 +657,7 @@
                
                // 添加新的车辆关联
                Date now = DateUtils.getNowDate();
                String currentUser = SecurityUtils.getUsername();
                String currentUser = userName;
                for (Long vehicleId : updateVO.getVehicleIds()) {
                    SysTaskVehicle taskVehicle = new SysTaskVehicle();
                    taskVehicle.setTaskId(updateVO.getTaskId());
@@ -665,7 +682,7 @@
                .collect(Collectors.toList());
            
            List<Long> newAssigneeIds = updateVO.getAssignees().stream()
                .map(TaskUpdateVO.AssigneeInfo::getUserId)
                .map(TaskCreateVO.AssigneeInfo::getUserId)
                .collect(Collectors.toList());
            
            // 比较新旧执行人员ID列表,判断是否有变化
@@ -678,17 +695,7 @@
                
                // 添加新的执行人员关联
                if (!updateVO.getAssignees().isEmpty()) {
                    // 将 TaskUpdateVO.AssigneeInfo 转换为 TaskCreateVO.AssigneeInfo
                    List<TaskCreateVO.AssigneeInfo> createAssignees = updateVO.getAssignees().stream()
                        .map(assignee -> {
                            TaskCreateVO.AssigneeInfo createAssignee = new TaskCreateVO.AssigneeInfo();
                            createAssignee.setUserId(assignee.getUserId());
                            createAssignee.setUserName(assignee.getUserName());
                            createAssignee.setUserType(assignee.getUserType());
                            return createAssignee;
                        })
                        .collect(Collectors.toList());
                    saveTaskAssignees(updateVO.getTaskId(), createAssignees, SecurityUtils.getUsername());
                    saveTaskAssignees(updateVO.getTaskId(), updateVO.getAssignees(), userName);
                }
                
                // 标记需要重新同步(人员变更)
@@ -697,37 +704,41 @@
        }
        
        // 更新急救转运扩展信息(检测地址和成交价变更)
        if (result > 0 && "EMERGENCY_TRANSFER".equals(oldTask.getTaskType()) && updateVO.getEmergencyInfo() != null) {
        if (result > 0 && "EMERGENCY_TRANSFER".equals(oldTask.getTaskType())) {
            // 获取旧的急救转运信息
            SysTaskEmergency oldEmergency = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(updateVO.getTaskId());
            
            // 检测转出医院地址变更
            boolean hospitalOutAddressChanged = false;
            if (updateVO.getEmergencyInfo().getHospitalOutAddress() != null
                && oldEmergency != null
                && !updateVO.getEmergencyInfo().getHospitalOutAddress().equals(oldEmergency.getHospitalOutAddress())) {
            if (updateVO.getHospitalOut() != null && updateVO.getHospitalOut().getAddress() != null
                && oldEmergency != null
                && !updateVO.getHospitalOut().getAddress().equals(oldEmergency.getHospitalOutAddress())) {
                hospitalOutAddressChanged = true;
            }
            
            // 检测转入医院地址变更
            boolean hospitalInAddressChanged = false;
            if (updateVO.getEmergencyInfo().getHospitalInAddress() != null
                && oldEmergency != null
                && !updateVO.getEmergencyInfo().getHospitalInAddress().equals(oldEmergency.getHospitalInAddress())) {
            if (updateVO.getHospitalIn() != null && updateVO.getHospitalIn().getAddress() != null
                && oldEmergency != null
                && !updateVO.getHospitalIn().getAddress().equals(oldEmergency.getHospitalInAddress())) {
                hospitalInAddressChanged = true;
            }
            
            // 检测成交价变更
            boolean transferPriceChanged = false;
            if (updateVO.getEmergencyInfo().getTransferPrice() != null
                && oldEmergency != null
            if (updateVO.getPrice() != null
                && oldEmergency != null
                && oldEmergency.getTransferPrice() != null
                && updateVO.getEmergencyInfo().getTransferPrice().compareTo(oldEmergency.getTransferPrice()) != 0) {
                && updateVO.getPrice().compareTo(oldEmergency.getTransferPrice()) != 0) {
                transferPriceChanged = true;
            }
            ;
            
            // 更新急救转运信息
            updateEmergencyInfo(updateVO.getTaskId(), updateVO);
            if (updateVO.getHospitalOut() != null || updateVO.getHospitalIn() != null || updateVO.getPatient() != null) {
                updateEmergencyInfoFromCreateVO(updateVO.getTaskId(), updateVO, userName);
            }
            
            // 如果地址或成交价发生变更,标记需要重新同步
            if (hospitalOutAddressChanged || hospitalInAddressChanged || transferPriceChanged) {
@@ -735,8 +746,15 @@
            }
        }
        
        // 更新福祉车扩展信息
        if (result > 0 && "WELFARE".equals(oldTask.getTaskType())) {
            if (updateVO.getPassenger() != null || updateVO.getStartAddress() != null || updateVO.getEndAddress() != null) {
                updateWelfareInfoFromCreateVO(updateVO.getTaskId(), updateVO, userName);
            }
        }
        // 如果是急救转运任务且有变更,标记需要重新同步
        if (result > 0 && "EMERGENCY_TRANSFER".equals(oldTask.getTaskType()) && needResync) {
        if (result > 0 && "EMERGENCY_TRANSFER".equals(oldTask.getTaskType()) && needResync && !updateFromLegacy) {
            try {
                sysTaskEmergencyService.markNeedResync(updateVO.getTaskId());
            } catch (Exception e) {
@@ -746,9 +764,183 @@
        
        // 记录操作日志
        if (result > 0) {
            recordTaskLog(updateVO.getTaskId(), "UPDATE", "更新任务",
                         buildTaskDescription(oldTask), buildTaskDescription(task),
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
            recordTaskLog(updateVO.getTaskId(), "UPDATE", "更新任务",
                buildTaskDescription(oldTask), buildTaskDescription(task),
                SecurityUtils.getUserId(), userName);
        }
        return result;
    }
    /**
     * 更新任务(用于旧系统同步)
     *
     * @param updateVO 任务更新对象
     * @param serviceOrderId 旧系统服务单ID
     * @param dispatchOrderId 旧系统调度单ID
     * @param serviceOrdNo 旧系统服务单编号
     * @param userId 用户ID
     * @param userName 用户名
     * @param deptId 部门ID
     * @param createTime 创建时间
     * @param updateTime 更新时间
     * @return 结果
     */
    @Override
    public int updateTask(TaskUpdateVO updateVO, String serviceOrderId, String dispatchOrderId, String serviceOrdNo,
                         Long userId, String userName, Long deptId, Date createTime, Date updateTime) {
        // 通过旧系统服务单ID查找任务
        SysTaskEmergency taskEmergency = sysTaskEmergencyMapper.selectByLegacyServiceOrdId(Long.parseLong(serviceOrderId));
        Long taskId = taskEmergency.getTaskId();
        updateVO.setTaskId(taskId);
        SysTask task = new SysTask();
        task.setTaskId(taskId);
        task.setTaskDescription(updateVO.getTaskDescription());
        task.setPlannedStartTime(updateVO.getPlannedStartTime());
        task.setPlannedEndTime(updateVO.getPlannedEndTime());
        task.setAssigneeId(updateVO.getAssigneeId());
        task.setUpdateBy(userName);
        task.setUpdateTime(DateUtils.getNowDate());
        task.setRemark(updateVO.getRemark());
        // 设置地址和坐标信息
        task.setDepartureAddress(updateVO.getDepartureAddress());
        task.setDestinationAddress(updateVO.getDestinationAddress());
        task.setDepartureLongitude(updateVO.getDepartureLongitude());
        task.setDepartureLatitude(updateVO.getDepartureLatitude());
        task.setDestinationLongitude(updateVO.getDestinationLongitude());
        task.setDestinationLatitude(updateVO.getDestinationLatitude());
        // 如果更新了部门ID
        if (updateVO.getDeptId() != null) {
            task.setDeptId(updateVO.getDeptId());
        }
        // 如果更新了任务编号
        if (updateVO.getTaskCode() != null) {
            task.setTaskCode(updateVO.getTaskCode());
        }
        // 获取旧任务信息,用于判断地址是否变更
        SysTask oldTask = sysTaskMapper.selectSysTaskByTaskId(taskId);
        // 自动获取出发地GPS坐标(如果地址变更且缺失坐标)
        if (oldTask != null && updateVO.getDepartureAddress() != null
            && !updateVO.getDepartureAddress().equals(oldTask.getDepartureAddress())
            && (updateVO.getDepartureLongitude() == null || updateVO.getDepartureLatitude() == null)
            && mapService != null) {
            try {
                Map<String, Double> coords = mapService.geocoding(
                    updateVO.getDepartureAddress(),
                    extractCityFromAddress(updateVO.getDepartureAddress())
                );
                if (coords != null) {
                    task.setDepartureLongitude(BigDecimal.valueOf(coords.get("lng")));
                    task.setDepartureLatitude(BigDecimal.valueOf(coords.get("lat")));
                    log.info("出发地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                }
            } catch (Exception e) {
                log.error("自动获取出发地GPS坐标失败", e);
            }
        }
        // 自动获取目的地GPS坐标(如果地址变更且缺失坐标)
        if (oldTask != null && updateVO.getDestinationAddress() != null
            && !updateVO.getDestinationAddress().equals(oldTask.getDestinationAddress())
            && (updateVO.getDestinationLongitude() == null || updateVO.getDestinationLatitude() == null)
            && mapService != null) {
            try {
                Map<String, Double> coords = mapService.geocoding(
                    updateVO.getDestinationAddress(),
                    extractCityFromAddress(updateVO.getDestinationAddress())
                );
                if (coords != null) {
                    task.setDestinationLongitude(BigDecimal.valueOf(coords.get("lng")));
                    task.setDestinationLatitude(BigDecimal.valueOf(coords.get("lat")));
                    log.info("目的地GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                }
            } catch (Exception e) {
                log.error("自动获取目的地GPS坐标失败", e);
            }
        }
        int result = sysTaskMapper.updateSysTask(task);
        // 更新车辆关联
        if (result > 0 && updateVO.getVehicleIds() != null && !updateVO.getVehicleIds().isEmpty()) {
            // 查询现有的车辆关联
            List<SysTaskVehicle> existingVehicles = sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId);
            List<Long> existingVehicleIds = existingVehicles.stream()
                .map(SysTaskVehicle::getVehicleId)
                .collect(Collectors.toList());
            // 比较新旧车辆ID列表,判断是否有变化
            boolean vehiclesChanged = !new HashSet<>(existingVehicleIds).equals(new HashSet<>(updateVO.getVehicleIds()));
            // 只有车辆发生变化时才更新
            if (vehiclesChanged) {
                // 删除旧的车辆关联
                sysTaskVehicleMapper.deleteSysTaskVehicleByTaskId(taskId);
                // 添加新的车辆关联
                Date now = DateUtils.getNowDate();
                for (Long vehicleId : updateVO.getVehicleIds()) {
                    SysTaskVehicle taskVehicle = new SysTaskVehicle();
                    taskVehicle.setTaskId(taskId);
                    taskVehicle.setVehicleId(vehicleId);
                    taskVehicle.setAssignTime(now);
                    taskVehicle.setAssignBy(userName);
                    taskVehicle.setCreateTime(now);
                    sysTaskVehicleMapper.insertSysTaskVehicle(taskVehicle);
                }
            }
        }
        // 更新执行人员(检测人员变更)
        if (result > 0 && updateVO.getAssignees() != null) {
            // 查询现有的执行人员
            List<SysTaskAssignee> existingAssignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId);
            List<Long> existingAssigneeIds = existingAssignees.stream()
                .map(SysTaskAssignee::getUserId)
                .collect(Collectors.toList());
            List<Long> newAssigneeIds = updateVO.getAssignees().stream()
                .map(TaskCreateVO.AssigneeInfo::getUserId)
                .collect(Collectors.toList());
            // 比较新旧执行人员ID列表,判断是否有变化
            boolean assigneesChanged = !new HashSet<>(existingAssigneeIds).equals(new HashSet<>(newAssigneeIds));
            // 只有执行人员发生变化时才更新
            if (assigneesChanged) {
                // 删除旧的执行人员关联
                sysTaskAssigneeMapper.deleteSysTaskAssigneeByTaskId(taskId);
                // 添加新的执行人员关联
                if (!updateVO.getAssignees().isEmpty()) {
                    saveTaskAssignees(taskId, updateVO.getAssignees(), userName);
                }
            }
        }
        // 更新急救转运扩展信息
        if (result > 0) {
            // 更新旧系统ID
            if (serviceOrderId != null) {
                taskEmergency.setLegacyServiceOrdId(Long.parseLong(serviceOrderId));
            }
            if (dispatchOrderId != null) {
                taskEmergency.setLegacyDispatchOrdId(Long.parseLong(dispatchOrderId));
            }
            if (serviceOrdNo != null) {
                taskEmergency.setLegacyServiceOrdNo(serviceOrdNo);
            }
            // 使用TaskCreateVO的字段来更新急救转运信息
            if (updateVO.getHospitalOut() != null || updateVO.getHospitalIn() != null || updateVO.getPatient() != null) {
                updateEmergencyInfoFromCreateVO(taskId, updateVO, userName);
            }
        }
        
        return result;
@@ -1165,7 +1357,7 @@
     */
    @Override
    @Transactional
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark) {
    public int assignVehicleToTask(Long taskId, Long vehicleId, String remark,Long userId,String userName) {
        // 检查是否已经分配
        int exists = sysTaskVehicleMapper.checkTaskVehicleExists(taskId, vehicleId);
        if (exists > 0) {
@@ -1176,7 +1368,7 @@
        taskVehicle.setTaskId(taskId);
        taskVehicle.setVehicleId(vehicleId);
        taskVehicle.setAssignTime(DateUtils.getNowDate());
        taskVehicle.setAssignBy(SecurityUtils.getUsername());
        taskVehicle.setAssignBy(userName);
        taskVehicle.setStatus("ASSIGNED");
        taskVehicle.setRemark(remark);
        
@@ -1186,7 +1378,7 @@
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "分配车辆", null, 
                         "分配车辆ID:" + vehicleId + ",备注:" + remark, 
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
                         userId, userName);
        }
        
        return result;
@@ -1224,10 +1416,10 @@
     */
    @Override
    @Transactional
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark) {
    public int assignMultipleVehiclesToTask(Long taskId, List<Long> vehicleIds, String remark,Long userId,String userName) {
        List<SysTaskVehicle> taskVehicles = new ArrayList<>();
        Date now = DateUtils.getNowDate();
        String assignBy = SecurityUtils.getUsername();
        String assignBy = userName;
        
        for (Long vehicleId : vehicleIds) {
            // 检查是否已经分配
@@ -1253,7 +1445,7 @@
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "批量分配车辆", null, 
                         "分配车辆数量:" + result + ",备注:" + remark, 
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
                         userId,userName);
        }
        
        return result;
@@ -1768,12 +1960,13 @@
    }
    /**
     * 更新急救转运任务扩展信息
     * 从 TaskCreateVO 更新急救转运任务扩展信息
     * 
     * @param taskId 任务ID
     * @param updateVO 任务更新对象
     * @param createVO 任务创建/更新对象
     * @param userName 操作人名
     */
    private void updateEmergencyInfo(Long taskId, TaskUpdateVO updateVO) {
    private void updateEmergencyInfoFromCreateVO(Long taskId, TaskCreateVO createVO, String userName) {
        // 查询现有的扩展信息
        SysTaskEmergency existingInfo = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
        if (existingInfo == null) {
@@ -1781,143 +1974,223 @@
            existingInfo = new SysTaskEmergency();
            existingInfo.setTaskId(taskId);
            existingInfo.setCreateTime(DateUtils.getNowDate());
            existingInfo.setCreateBy(SecurityUtils.getUsername());
            existingInfo.setCreateBy(userName);
        }
        TaskUpdateVO.EmergencyInfoVO emergencyInfo = updateVO.getEmergencyInfo();
        
        // 更新患者信息
        if (emergencyInfo.getPatientContact() != null) {
            existingInfo.setPatientContact(emergencyInfo.getPatientContact());
        }
        if (emergencyInfo.getPatientPhone() != null) {
            existingInfo.setPatientPhone(emergencyInfo.getPatientPhone());
        }
        if (emergencyInfo.getPatientName() != null) {
            existingInfo.setPatientName(emergencyInfo.getPatientName());
        }
        if (emergencyInfo.getPatientGender() != null) {
            existingInfo.setPatientGender(emergencyInfo.getPatientGender());
        }
        if (emergencyInfo.getPatientIdCard() != null) {
            existingInfo.setPatientIdCard(emergencyInfo.getPatientIdCard());
        }
        if (emergencyInfo.getPatientCondition() != null) {
            existingInfo.setPatientCondition(emergencyInfo.getPatientCondition());
        if (createVO.getPatient() != null) {
            if (createVO.getPatient().getContact() != null) {
                existingInfo.setPatientContact(createVO.getPatient().getContact());
            }
            if (createVO.getPatient().getPhone() != null) {
                existingInfo.setPatientPhone(createVO.getPatient().getPhone());
            }
            if (createVO.getPatient().getName() != null) {
                existingInfo.setPatientName(createVO.getPatient().getName());
            }
            if (createVO.getPatient().getGender() != null) {
                existingInfo.setPatientGender(createVO.getPatient().getGender());
            }
            if (createVO.getPatient().getIdCard() != null) {
                existingInfo.setPatientIdCard(createVO.getPatient().getIdCard());
            }
            if (createVO.getPatient().getCondition() != null) {
                existingInfo.setPatientCondition(createVO.getPatient().getCondition());
            }
        }
        
        // 更新转出医院信息
        if (emergencyInfo.getHospitalOutId() != null) {
            existingInfo.setHospitalOutId(emergencyInfo.getHospitalOutId());
        }
        if (emergencyInfo.getHospitalOutName() != null) {
            existingInfo.setHospitalOutName(emergencyInfo.getHospitalOutName());
        }
        if (emergencyInfo.getHospitalOutDepartment() != null) {
            existingInfo.setHospitalOutDepartment(emergencyInfo.getHospitalOutDepartment());
        }
        if (emergencyInfo.getHospitalOutDepartmentId() != null) {
            existingInfo.setHospitalOutDepartmentId(emergencyInfo.getHospitalOutDepartmentId());
        }
        if (emergencyInfo.getHospitalOutBedNumber() != null) {
            existingInfo.setHospitalOutBedNumber(emergencyInfo.getHospitalOutBedNumber());
        }
        if (emergencyInfo.getHospitalOutAddress() != null) {
            existingInfo.setHospitalOutAddress(emergencyInfo.getHospitalOutAddress());
            // 如果更新了地址但没有GPS坐标,后端自动获取
            if (emergencyInfo.getHospitalOutLongitude() == null && emergencyInfo.getHospitalOutLatitude() == null && mapService != null) {
                try {
                    Map<String, Double> coords = mapService.geocoding(
                        emergencyInfo.getHospitalOutAddress(),
                        extractCityFromAddress(emergencyInfo.getHospitalOutAddress())
                    );
                    if (coords != null) {
                        existingInfo.setHospitalOutLongitude(BigDecimal.valueOf(coords.get("lng")));
                        existingInfo.setHospitalOutLatitude(BigDecimal.valueOf(coords.get("lat")));
                        log.info("转出医院GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
        if (createVO.getHospitalOut() != null) {
            if (createVO.getHospitalOut().getId() != null) {
                existingInfo.setHospitalOutId(createVO.getHospitalOut().getId());
            }
            if (createVO.getHospitalOut().getName() != null) {
                existingInfo.setHospitalOutName(createVO.getHospitalOut().getName());
            }
            if (createVO.getHospitalOut().getDepartment() != null) {
                existingInfo.setHospitalOutDepartment(createVO.getHospitalOut().getDepartment());
            }
            if (createVO.getHospitalOut().getDepartmentId() != null) {
                existingInfo.setHospitalOutDepartmentId(createVO.getHospitalOut().getDepartmentId());
            }
            if (createVO.getHospitalOut().getBedNumber() != null) {
                existingInfo.setHospitalOutBedNumber(createVO.getHospitalOut().getBedNumber());
            }
            if (createVO.getHospitalOut().getAddress() != null && !createVO.getHospitalOut().getAddress().equals(existingInfo.getHospitalOutAddress())) {
                existingInfo.setHospitalOutAddress(createVO.getHospitalOut().getAddress());
                // 如枟更新了地址但没有GPS坐标,后端自动获取
                if (createVO.getHospitalOut().getLongitude() == null && createVO.getHospitalOut().getLatitude() == null && mapService != null) {
                    try {
                        Map<String, Double> coords = mapService.geocoding(
                            createVO.getHospitalOut().getAddress(),
                            extractCityFromAddress(createVO.getHospitalOut().getAddress())
                        );
                        if (coords != null) {
                            existingInfo.setHospitalOutLongitude(BigDecimal.valueOf(coords.get("lng")));
                            existingInfo.setHospitalOutLatitude(BigDecimal.valueOf(coords.get("lat")));
                            log.info("转出医院GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                        }
                    } catch (Exception e) {
                        log.error("自动获取转出医院GPS坐标失败", e);
                    }
                } catch (Exception e) {
                    log.error("自动获取转出医院GPS坐标失败", e);
                }
            }
        }
        if (emergencyInfo.getHospitalOutLongitude() != null) {
            existingInfo.setHospitalOutLongitude(emergencyInfo.getHospitalOutLongitude());
        }
        if (emergencyInfo.getHospitalOutLatitude() != null) {
            existingInfo.setHospitalOutLatitude(emergencyInfo.getHospitalOutLatitude());
            if (createVO.getHospitalOut().getLongitude() != null) {
                existingInfo.setHospitalOutLongitude(createVO.getHospitalOut().getLongitude());
            }
            if (createVO.getHospitalOut().getLatitude() != null) {
                existingInfo.setHospitalOutLatitude(createVO.getHospitalOut().getLatitude());
            }
        }
        
        // 更新转入医院信息
        if (emergencyInfo.getHospitalInId() != null) {
            existingInfo.setHospitalInId(emergencyInfo.getHospitalInId());
        }
        if (emergencyInfo.getHospitalInName() != null) {
            existingInfo.setHospitalInName(emergencyInfo.getHospitalInName());
        }
        if (emergencyInfo.getHospitalInDepartment() != null) {
            existingInfo.setHospitalInDepartment(emergencyInfo.getHospitalInDepartment());
        }
        if (emergencyInfo.getHospitalInDepartmentId() != null) {
            existingInfo.setHospitalInDepartmentId(emergencyInfo.getHospitalInDepartmentId());
        }
        if (emergencyInfo.getHospitalInBedNumber() != null) {
            existingInfo.setHospitalInBedNumber(emergencyInfo.getHospitalInBedNumber());
        }
        if (emergencyInfo.getHospitalInAddress() != null) {
            existingInfo.setHospitalInAddress(emergencyInfo.getHospitalInAddress());
            // 如果更新了地址但没有GPS坐标,后端自动获取
            if (emergencyInfo.getHospitalInLongitude() == null && emergencyInfo.getHospitalInLatitude() == null && mapService != null) {
                try {
                    Map<String, Double> coords = mapService.geocoding(
                        emergencyInfo.getHospitalInAddress(),
                        extractCityFromAddress(emergencyInfo.getHospitalInAddress())
                    );
                    if (coords != null) {
                        existingInfo.setHospitalInLongitude(BigDecimal.valueOf(coords.get("lng")));
                        existingInfo.setHospitalInLatitude(BigDecimal.valueOf(coords.get("lat")));
                        log.info("转入医院GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
        if (createVO.getHospitalIn() != null) {
            if (createVO.getHospitalIn().getId() != null) {
                existingInfo.setHospitalInId(createVO.getHospitalIn().getId());
            }
            if (createVO.getHospitalIn().getName() != null) {
                existingInfo.setHospitalInName(createVO.getHospitalIn().getName());
            }
            if (createVO.getHospitalIn().getDepartment() != null) {
                existingInfo.setHospitalInDepartment(createVO.getHospitalIn().getDepartment());
            }
            if (createVO.getHospitalIn().getDepartmentId() != null) {
                existingInfo.setHospitalInDepartmentId(createVO.getHospitalIn().getDepartmentId());
            }
            if (createVO.getHospitalIn().getBedNumber() != null) {
                existingInfo.setHospitalInBedNumber(createVO.getHospitalIn().getBedNumber());
            }
            if (createVO.getHospitalIn().getAddress() != null && !createVO.getHospitalIn().getAddress().equals(existingInfo.getHospitalInAddress())) {
                existingInfo.setHospitalInAddress(createVO.getHospitalIn().getAddress());
                // 如果更新了地址但没有GPS坐标,后端自动获取
                if (createVO.getHospitalIn().getLongitude() == null && createVO.getHospitalIn().getLatitude() == null && mapService != null) {
                    try {
                        Map<String, Double> coords = mapService.geocoding(
                            createVO.getHospitalIn().getAddress(),
                            extractCityFromAddress(createVO.getHospitalIn().getAddress())
                        );
                        if (coords != null) {
                            existingInfo.setHospitalInLongitude(BigDecimal.valueOf(coords.get("lng")));
                            existingInfo.setHospitalInLatitude(BigDecimal.valueOf(coords.get("lat")));
                            log.info("转入医院GPS坐标自动获取成功: {}, {}", coords.get("lng"), coords.get("lat"));
                        }
                    } catch (Exception e) {
                        log.error("自动获取转入医院GPS坐标失败", e);
                    }
                } catch (Exception e) {
                    log.error("自动获取转入医院GPS坐标失败", e);
                }
            }
        }
        if (emergencyInfo.getHospitalInLongitude() != null) {
            existingInfo.setHospitalInLongitude(emergencyInfo.getHospitalInLongitude());
        }
        if (emergencyInfo.getHospitalInLatitude() != null) {
            existingInfo.setHospitalInLatitude(emergencyInfo.getHospitalInLatitude());
            if (createVO.getHospitalIn().getLongitude() != null) {
                existingInfo.setHospitalInLongitude(createVO.getHospitalIn().getLongitude());
            }
            if (createVO.getHospitalIn().getLatitude() != null) {
                existingInfo.setHospitalInLatitude(createVO.getHospitalIn().getLatitude());
            }
        }
        
        // 更新费用信息
        if (emergencyInfo.getTransferDistance() != null) {
            existingInfo.setTransferDistance(emergencyInfo.getTransferDistance());
        if (createVO.getTransferDistance() != null) {
            existingInfo.setTransferDistance(createVO.getTransferDistance());
        }
        if (emergencyInfo.getTransferPrice() != null) {
            existingInfo.setTransferPrice(emergencyInfo.getTransferPrice());
        if (createVO.getPrice() != null) {
            existingInfo.setTransferPrice(createVO.getPrice());
        }
        // 更新单据类型ID
        if (createVO.getDocumentTypeId() != null) {
            existingInfo.setDocumentTypeId(createVO.getDocumentTypeId());
        }
        // 更新任务类型ID
        if (createVO.getTaskTypeId() != null) {
            existingInfo.setTaskTypeId(createVO.getTaskTypeId());
        }
        
        // 更新病情ID列表
        if (updateVO.getDiseaseIds() != null && !updateVO.getDiseaseIds().isEmpty()) {
            String diseaseIdsStr = updateVO.getDiseaseIds().stream()
        if (createVO.getDiseaseIds() != null && !createVO.getDiseaseIds().isEmpty()) {
            String diseaseIdsStr = createVO.getDiseaseIds().stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
            existingInfo.setDiseaseIds(diseaseIdsStr);
        } else {
            // 如果病情ID列表为空,清空该字段
            existingInfo.setDiseaseIds(null);
        }
        
        // 系统字段
        existingInfo.setUpdateTime(DateUtils.getNowDate());
        existingInfo.setUpdateBy(SecurityUtils.getUsername());
        existingInfo.setUpdateBy(userName);
        
        // 执行更新
        sysTaskEmergencyMapper.updateSysTaskEmergency(existingInfo);
    }
    /**
     * 从 TaskCreateVO 更新福祉车任务扩展信息
     *
     * @param taskId 任务ID
     * @param createVO 任务创建/更新对象
     * @param userName 操作人名
     */
    private void updateWelfareInfoFromCreateVO(Long taskId, TaskCreateVO createVO, String userName) {
        // 查询现有的扩展信息
        SysTaskWelfare existingInfo = sysTaskWelfareMapper.selectSysTaskWelfareByTaskId(taskId);
        if (existingInfo == null) {
            // 如果不存在,则创建新的
            existingInfo = new SysTaskWelfare();
            existingInfo.setTaskId(taskId);
            existingInfo.setCreateTime(DateUtils.getNowDate());
            existingInfo.setCreateBy(userName);
        }
        // 更新乘客信息
        if (createVO.getPassenger() != null) {
            if (createVO.getPassenger().getContact() != null) {
                existingInfo.setPassengerContact(createVO.getPassenger().getContact());
            }
            if (createVO.getPassenger().getPhone() != null) {
                existingInfo.setPassengerPhone(createVO.getPassenger().getPhone());
            }
        }
        // 更新地址信息
        if (createVO.getStartAddress() != null) {
            existingInfo.setPickupAddress(createVO.getStartAddress());
        }
        if (createVO.getEndAddress() != null) {
            existingInfo.setDestinationAddress(createVO.getEndAddress());
        }
        // 更新GPS坐标
        if (createVO.getDepartureLongitude() != null) {
            existingInfo.setPickupLongitude(createVO.getDepartureLongitude());
        }
        if (createVO.getDepartureLatitude() != null) {
            existingInfo.setPickupLatitude(createVO.getDepartureLatitude());
        }
        if (createVO.getDestinationLongitude() != null) {
            existingInfo.setDestinationLongitude(createVO.getDestinationLongitude());
        }
        if (createVO.getDestinationLatitude() != null) {
            existingInfo.setDestinationLatitude(createVO.getDestinationLatitude());
        }
        // 更新距离和费用
        if (createVO.getDistance() != null) {
            existingInfo.setServiceDistance(createVO.getDistance());
        } else if (createVO.getEstimatedDistance() != null) {
            existingInfo.setServiceDistance(createVO.getEstimatedDistance());
        }
        if (createVO.getPrice() != null) {
            existingInfo.setServicePrice(createVO.getPrice());
        }
        // 系统字段
        existingInfo.setUpdateTime(DateUtils.getNowDate());
        existingInfo.setUpdateBy(userName);
        // 执行更新
        sysTaskWelfareMapper.updateSysTaskWelfare(existingInfo);
    }
    /**
     * 保存福祉车任务扩展信息
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml
@@ -48,6 +48,7 @@
    <!-- 执行人结果映射 -->
    <resultMap type="java.util.HashMap" id="AssigneeResult">
        <result property="EntourageOAId" column="EntourageOAId" />
        <result property="EntourageID" column="EntourageID" />
        <result property="EntourageState" column="EntourageState" />
    </resultMap>
    
@@ -156,6 +157,7 @@
    <select id="selectAssigneesByDispatchOrdID" resultMap="AssigneeResult">
        SELECT 
            EntourageOAId,
            EntourageID,
            EntourageState
        FROM DispatchOrd_Entourage 
        WHERE DispatchOrdIDDt = #{dispatchOrdID}
ruoyi-system/src/main/resources/mapper/system/SysTaskAdditionalFeeMapper.xml
@@ -41,6 +41,9 @@
            <if test="quantity != null">quantity,</if>
            <if test="totalAmount != null">total_amount,</if>
            <if test="remark != null">remark,</if>
            <if test="pid != null">pid,</if>
            <if test="syncStatus != null">sync_status,</if>
            <if test="syncTime != null">sync_time,</if>
            <if test="createdBy != null and createdBy != ''">created_by,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
@@ -51,6 +54,9 @@
            <if test="quantity != null">#{quantity},</if>
            <if test="totalAmount != null">#{totalAmount},</if>
            <if test="remark != null">#{remark},</if>
            <if test="pid != null">#{pid},</if>
            <if test="syncStatus != null">#{syncStatus},</if>
            <if test="syncTime != null">#{syncTime},</if>
            <if test="createdBy != null and createdBy != ''">#{createdBy},</if>
        </trim>
    </insert>
ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
@@ -251,6 +251,7 @@
            <if test="actualEndTime != null">actual_end_time = #{actualEndTime},</if>
            <if test="creatorId != null">creator_id = #{creatorId},</if>
            <if test="assigneeId != null">assignee_id = #{assigneeId},</if>
            <if test="deptId != null">dept_id = #{deptId},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
ruoyi-ui/src/views/task/general/detail.vue
@@ -33,11 +33,11 @@
      <!-- 急救转运任务扩展信息 -->
      <el-descriptions v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && taskDetail.emergencyInfo" title="急救转运信息" :column="2" border style="margin-top: 20px;">
        <el-descriptions-item label="联系人">
          <span v-if="taskDetail.emergencyInfo.contactPerson">{{ taskDetail.emergencyInfo.contactPerson }}</span>
          <span v-if="taskDetail.emergencyInfo.patientContact">{{ taskDetail.emergencyInfo.patientContact }}</span>
          <span v-else style="color: #C0C4CC;">--</span>
        </el-descriptions-item>
        <el-descriptions-item label="联系电话">
          <span v-if="taskDetail.emergencyInfo.contactPhone">{{ taskDetail.emergencyInfo.contactPhone }}</span>
          <span v-if="taskDetail.emergencyInfo.patientPhone">{{ taskDetail.emergencyInfo.patientPhone }}</span>
          <span v-else style="color: #C0C4CC;">--</span>
        </el-descriptions-item>
        <el-descriptions-item label="患者姓名">
@@ -53,7 +53,7 @@
          <span v-else style="color: #C0C4CC;">--</span>
        </el-descriptions-item>
        <el-descriptions-item label="病情描述" :span="2">
          <span v-if="taskDetail.emergencyInfo.illnessDescription">{{ taskDetail.emergencyInfo.illnessDescription }}</span>
          <span v-if="taskDetail.emergencyInfo.patientCondition">{{ taskDetail.emergencyInfo.patientCondition }}</span>
          <span v-else style="color: #C0C4CC;">--</span>
        </el-descriptions-item>
      </el-descriptions>
@@ -120,6 +120,166 @@
        </el-descriptions-item>
      </el-descriptions>
      <!-- 支付信息(仅急救转运任务显示) -->
      <el-card v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && paymentInfo" class="box-card" style="margin-top: 20px;">
        <div slot="header" class="clearfix">
          <span>支付信息</span>
        </div>
        <!-- 支付概览 -->
        <el-descriptions :column="3" border>
          <el-descriptions-item label="基本价格">
            <span style="color: #409EFF; font-weight: bold;">¥{{ paymentInfo.transferPrice || '0.00' }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="附加费用总额">
            <span style="color: #E6A23C; font-weight: bold;">¥{{ paymentInfo.additionalAmount || '0.00' }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="总金额">
            <span style="color: #67C23A; font-weight: bold;">¥{{ paymentInfo.totalAmount || '0.00' }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="已支付金额">
            <span style="color: #67C23A; font-weight: bold;">¥{{ paymentInfo.paidAmount || '0.00' }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="剩余未付金额">
            <span style="color: #F56C6C; font-weight: bold;">¥{{ getRemainingAmount() }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="支付状态">
            <el-tag v-if="getRemainingAmount() <= 0" type="success" size="small">
              <i class="el-icon-success"></i> 已结清
            </el-tag>
            <el-tag v-else-if="paymentInfo.paidAmount > 0" type="warning" size="small">
              <i class="el-icon-warning"></i> 部分支付
            </el-tag>
            <el-tag v-else type="info" size="small">
              <i class="el-icon-warning"></i> 未支付
            </el-tag>
          </el-descriptions-item>
        </el-descriptions>
        <!-- 附加费用明细 -->
        <div style="margin-top: 20px;">
          <h4 style="margin-bottom: 10px;">附加费用明细</h4>
          <el-table :data="paymentInfo.additionalFees" border>
            <el-table-column label="费用类型" align="center" prop="feeType" width="120">
              <template slot-scope="scope">
                <dict-tag :options="dict.type.task_additional_fee_type" :value="scope.row.feeType"/>
              </template>
            </el-table-column>
            <el-table-column label="费用名称" align="center" prop="feeName" />
            <el-table-column label="单价" align="center" prop="unitAmount" width="120">
              <template slot-scope="scope">
                <span>¥{{ scope.row.unitAmount }}</span>
              </template>
            </el-table-column>
            <el-table-column label="数量" align="center" prop="quantity" width="80" />
            <el-table-column label="小计" align="center" prop="totalAmount" width="120">
              <template slot-scope="scope">
                <span style="color: #E6A23C; font-weight: bold;">¥{{ scope.row.totalAmount }}</span>
              </template>
            </el-table-column>
            <el-table-column label="同步状态" align="center" width="120">
              <template slot-scope="scope">
                <el-tag v-if="scope.row.syncStatus === 0" type="info" size="small">
                  <i class="el-icon-warning"></i> 未同步
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 1" type="warning" size="small">
                  <i class="el-icon-loading"></i> 同步中
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 2" type="success" size="small">
                  <i class="el-icon-success"></i> 同步成功
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 3" type="danger" size="small">
                  <i class="el-icon-error"></i> 同步失败
                </el-tag>
                <span v-else style="color: #C0C4CC;">--</span>
              </template>
            </el-table-column>
            <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip>
              <template slot-scope="scope">
                <span v-if="scope.row.remark">{{ scope.row.remark }}</span>
                <span v-else style="color: #C0C4CC;">--</span>
              </template>
            </el-table-column>
          </el-table>
          <!-- 空状态提示 -->
          <div v-if="!paymentInfo.additionalFees || paymentInfo.additionalFees.length === 0" style="text-align: center; padding: 40px 0; color: #909399;">
            <i class="el-icon-document" style="font-size: 48px; display: block; margin-bottom: 12px;"></i>
            <span>暂无附加费用</span>
          </div>
        </div>
        <!-- 已支付记录 -->
        <div style="margin-top: 20px;">
          <h4 style="margin-bottom: 10px;">已支付记录</h4>
          <el-table :data="paymentInfo.paidPayments" border>
            <el-table-column label="商户订单号" align="center" prop="outTradeNo" width="200" show-overflow-tooltip />
            <el-table-column label="支付金额" align="center" prop="settlementAmount" width="120">
              <template slot-scope="scope">
                <span style="color: #67C23A; font-weight: bold;">¥{{ scope.row.settlementAmount }}</span>
              </template>
            </el-table-column>
            <el-table-column label="支付方式" align="center" prop="paymentMethod" width="120">
              <template slot-scope="scope">
                <dict-tag :options="dict.type.task_payment_method" :value="scope.row.paymentMethod"/>
              </template>
            </el-table-column>
            <el-table-column label="支付状态" align="center" prop="payStatus" width="120">
              <template slot-scope="scope">
                <el-tag v-if="scope.row.payStatus === 'PAID'" type="success" size="small">
                  <i class="el-icon-success"></i> 已支付
                </el-tag>
                <el-tag v-else-if="scope.row.payStatus === 'PENDING'" type="warning" size="small">
                  <i class="el-icon-time"></i> 待支付
                </el-tag>
                <el-tag v-else-if="scope.row.payStatus === 'FAILED'" type="danger" size="small">
                  <i class="el-icon-error"></i> 失败
                </el-tag>
                <el-tag v-else type="info" size="small">
                  {{ scope.row.payStatus }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="交易号" align="center" prop="tradeNo" width="150" show-overflow-tooltip />
            <el-table-column label="支付时间" align="center" prop="payTime" width="180">
              <template slot-scope="scope">
                <span v-if="scope.row.payTime">{{ parseTime(scope.row.payTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
                <span v-else style="color: #C0C4CC;">--</span>
              </template>
            </el-table-column>
            <el-table-column label="同步状态" align="center" width="120">
              <template slot-scope="scope">
                <el-tag v-if="scope.row.syncStatus === 0" type="info" size="small">
                  <i class="el-icon-warning"></i> 未同步
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 1" type="warning" size="small">
                  <i class="el-icon-loading"></i> 同步中
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 2" type="success" size="small">
                  <i class="el-icon-success"></i> 同步成功
                </el-tag>
                <el-tag v-else-if="scope.row.syncStatus === 3" type="danger" size="small">
                  <i class="el-icon-error"></i> 同步失败
                </el-tag>
                <span v-else style="color: #C0C4CC;">--</span>
              </template>
            </el-table-column>
            <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip>
              <template slot-scope="scope">
                <span v-if="scope.row.remark">{{ scope.row.remark }}</span>
                <span v-else style="color: #C0C4CC;">--</span>
              </template>
            </el-table-column>
          </el-table>
          <!-- 空状态提示 -->
          <div v-if="!paymentInfo.paidPayments || paymentInfo.paidPayments.length === 0" style="text-align: center; padding: 40px 0; color: #909399;">
            <i class="el-icon-document" style="font-size: 48px; display: block; margin-bottom: 12px;"></i>
            <span>暂无支付记录</span>
          </div>
        </div>
      </el-card>
      <!-- 福祉车任务扩展信息 -->
      <el-descriptions v-if="taskDetail.taskType === 'WELFARE' && taskDetail.welfareInfo" title="福祉车服务信息" :column="2" border style="margin-top: 20px;">
        <el-descriptions-item label="乘客姓名">{{ taskDetail.welfareInfo.passengerName }}</el-descriptions-item>
@@ -151,6 +311,48 @@
        <el-button type="success" @click="handleAssign" v-hasPermi="['task:general:assign']">分配任务</el-button>
        <el-button type="warning" @click="handleStatusChange" v-hasPermi="['task:general:status']">状态变更</el-button>
        <el-button type="info" @click="handleVehicleAssign" v-hasPermi="['task:general:assign']">分配车辆</el-button>
      </div>
    </el-card>
    <!-- 执行人员列表 -->
    <el-card class="box-card" style="margin-top: 20px;">
      <div slot="header" class="clearfix">
        <span>执行人员</span>
      </div>
      <el-table :data="taskDetail.assignees" v-loading="assigneeLoading">
        <el-table-column label="序号" align="center" width="60">
          <template slot-scope="scope">
            <span>{{ scope.$index + 1 }}</span>
          </template>
        </el-table-column>
        <el-table-column label="姓名" align="center" prop="userName">
          <template slot-scope="scope">
            <span>{{ scope.row.userName }}</span>
            <el-tag v-if="scope.row.isPrimary === '1'" type="warning" size="mini" style="margin-left: 8px;">
              <i class="el-icon-star-on"></i> 负责人
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="角色类型" align="center" prop="userType" width="120">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.userType === 'driver'" type="primary" size="small">司机</el-tag>
            <el-tag v-else-if="scope.row.userType === 'doctor'" type="success" size="small">医生</el-tag>
            <el-tag v-else-if="scope.row.userType === 'nurse'" type="info" size="small">护士</el-tag>
            <el-tag v-else type="" size="small">{{ scope.row.userType }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
          </template>
        </el-table-column>
      </el-table>
      <!-- 空状态提示 -->
      <div v-if="!taskDetail.assignees || taskDetail.assignees.length === 0" style="text-align: center; padding: 40px 0; color: #909399;">
        <i class="el-icon-user" style="font-size: 48px; display: block; margin-bottom: 12px;"></i>
        <span>暂无执行人员</span>
      </div>
    </el-card>
@@ -261,6 +463,74 @@
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- 附加费用列表 -->
    <el-card class="box-card" style="margin-top: 20px;">
      <div slot="header" class="clearfix">
        <span>附加费用列表</span>
      </div>
      <el-table :data="additionalFeeList" v-loading="feeLoading">
        <el-table-column label="费用类型" align="center" prop="feeType" width="120">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.task_additional_fee_type" :value="scope.row.feeType"/>
          </template>
        </el-table-column>
        <el-table-column label="费用名称" align="center" prop="feeName" />
        <el-table-column label="单价" align="center" prop="unitAmount" width="120">
          <template slot-scope="scope">
            <span>¥{{ scope.row.unitAmount }}</span>
          </template>
        </el-table-column>
        <el-table-column label="数量" align="center" prop="quantity" width="80" />
        <el-table-column label="总金额" align="center" prop="totalAmount" width="120">
          <template slot-scope="scope">
            <span style="color: #E6A23C; font-weight: bold;">¥{{ scope.row.totalAmount }}</span>
          </template>
        </el-table-column>
        <el-table-column label="同步状态" align="center" width="120">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.syncStatus === 0" type="info" size="small">
              <i class="el-icon-warning"></i> 未同步
            </el-tag>
            <el-tag v-else-if="scope.row.syncStatus === 1" type="warning" size="small">
              <i class="el-icon-loading"></i> 同步中
            </el-tag>
            <el-tag v-else-if="scope.row.syncStatus === 2" type="success" size="small">
              <i class="el-icon-success"></i> 同步成功
            </el-tag>
            <el-tag v-else-if="scope.row.syncStatus === 3" type="danger" size="small">
              <i class="el-icon-error"></i> 同步失败
            </el-tag>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
        <el-table-column label="同步时间" align="center" prop="syncTime" width="180">
          <template slot-scope="scope">
            <span v-if="scope.row.syncTime">{{ parseTime(scope.row.syncTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createdTime" width="180">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.createdTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
          </template>
        </el-table-column>
        <el-table-column label="创建者" align="center" prop="createdBy" width="100" />
        <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip>
          <template slot-scope="scope">
            <span v-if="scope.row.remark">{{ scope.row.remark }}</span>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
      </el-table>
      <!-- 空状态提示 -->
      <div v-if="!additionalFeeList || additionalFeeList.length === 0" style="text-align: center; padding: 40px 0; color: #909399;">
        <i class="el-icon-document" style="font-size: 48px; display: block; margin-bottom: 12px;"></i>
        <span>暂无附加费用</span>
      </div>
    </el-card>
    <!-- 操作日志 -->
@@ -456,13 +726,13 @@
</template>
<script>
import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask } from "@/api/task";
import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask, getPaymentInfo } from "@/api/task";
import { listUser } from "@/api/system/user";
import { getToken } from "@/utils/auth";
export default {
  name: "TaskDetail",
  dicts: ['sys_task_type', 'sys_task_status', 'sys_vehicle_type', 'sys_task_vehicle_status', 'sys_user_sex', 'hospital_department', 'sys_attachment_category'],
  dicts: ['sys_task_type', 'sys_task_status', 'sys_vehicle_type', 'sys_task_vehicle_status', 'sys_user_sex', 'hospital_department', 'sys_attachment_category', 'task_additional_fee_type', 'task_payment_method'],
  data() {
    return {
      // 任务详情
@@ -502,6 +772,12 @@
      // 加载状态
      vehicleLoading: false,
      attachmentLoading: false,
      feeLoading: false,
      assigneeLoading: false,
      // 附加费用列表
      additionalFeeList: [],
      // 支付信息
      paymentInfo: null,
      // 上传相关
      uploadUrl: process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + (new URLSearchParams(window.location.search).get('taskId') || ''),
      uploadHeaders: {
@@ -545,6 +821,7 @@
  created() {
    this.getTaskDetail();
    this.getUserList();
    this.getAdditionalFeeList();
    // 初始化上传URL
    this.uploadUrl = process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId;
  },
@@ -553,8 +830,41 @@
    getTaskDetail() {
      getTask(this.$route.params.taskId).then(response => {
        this.taskDetail = response.data;
        // 任务详情加载完成后,加载支付信息
        // console.log("TaskDetail", this.taskDetail);
        this.loadPaymentInfo();
      });
    },
    /** 获取附加费用列表 */
    getAdditionalFeeList() {
      this.feeLoading = true;
      getPaymentInfo(this.$route.params.taskId).then(response => {
        this.additionalFeeList = response.data.additionalFees || [];
        this.feeLoading = false;
      }).catch(() => {
        this.feeLoading = false;
      });
    },
    /** 加载支付信息 */
    loadPaymentInfo() {
      //EMERGENCY_TRANSFER
      if (this.taskDetail.taskType === 'EMERGENCY_TRANSFER') {
        getPaymentInfo(this.$route.params.taskId).then(response => {
          // console.log("PaymentInfo", response.data);
          this.paymentInfo = response.data;
        }).catch(() => {
          this.paymentInfo = null;
        });
      }
    },
    /** 计算剩余未付金额 */
    getRemainingAmount() {
      if (!this.paymentInfo) return '0.00';
      const total = parseFloat(this.paymentInfo.totalAmount || 0);
      const paid = parseFloat(this.paymentInfo.paidAmount || 0);
      const remaining = total - paid;
      return remaining > 0 ? remaining.toFixed(2) : '0.00';
    },
    /** 获取用户列表 */
    getUserList() {
      listUser().then(response => {