wlzboy
2025-11-08 cdcc529ce5fb9aa0dd1dea5bb2a620fc09b7d25c
feat: 上传附件
9个文件已添加
13个文件已修改
1915 ■■■■ 已修改文件
app/components/AttachmentUpload.vue 629 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/task/detail.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/任务附件同步ImageData功能说明.md 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskAttachmentSyncController.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/ImageUrlConfig.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAttachment.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/file/FileUploadServiceImpl.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/imagedata/ImageDataServiceImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java 64 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/task/ITaskAttachmentService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/task/TaskAttachmentServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/update_sys_task_attachment_sync_fields.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/components/AttachmentUpload.vue
New file
@@ -0,0 +1,629 @@
<template>
  <view class="attachment-upload-container">
    <view class="detail-section">
      <view class="section-title">
        {{ title }}
        <button class="upload-btn" @click="showUploadDialog">上传附件</button>
      </view>
      <view v-if="attachmentList && attachmentList.length > 0">
        <view class="attachment-item" v-for="(item, index) in attachmentList" :key="item.attachmentId">
          <view class="attachment-thumbnail" @click="viewAttachment(item)">
            <image :src="getImageUrl(item)" mode="aspectFill" class="thumbnail-image"></image>
          </view>
          <view class="attachment-info">
            <view class="attachment-category">
              <text class="category-tag">{{ getCategoryName(item.attachmentCategory) }}</text>
            </view>
            <view class="attachment-meta">
              <text class="upload-time">{{ formatTime(item.uploadTime) }}</text>
            </view>
          </view>
          <view class="attachment-actions">
            <button class="action-btn view-btn" @click="viewAttachment(item)">查看</button>
            <button class="action-btn delete-btn" @click="deleteAttachment(item.attachmentId, index)">删除</button>
          </view>
        </view>
      </view>
      <view v-else class="no-attachment">
        <text>暂无附件</text>
      </view>
    </view>
    <!-- é™„件上传对话框 -->
    <uni-popup ref="uploadPopup" type="bottom">
      <view class="upload-dialog">
        <view class="dialog-header">
          <text class="dialog-title">上传附件</text>
          <uni-icons type="closeempty" size="24" @click="closeUploadDialog"></uni-icons>
        </view>
        <view class="dialog-content">
          <view class="form-item">
            <view class="form-label">附件分类</view>
            <picker @change="onCategoryChange" :value="selectedCategoryIndex" :range="categoryList" range-key="label">
              <view class="picker-value">
                {{ categoryList[selectedCategoryIndex].label }}
                <uni-icons type="arrowdown" size="16"></uni-icons>
              </view>
            </picker>
          </view>
          <view class="form-item">
            <view class="form-label">选择图片</view>
            <button class="choose-image-btn" @click="chooseImage">
              <uni-icons type="image" size="20"></uni-icons>
              <text>点击选择</text>
            </button>
          </view>
          <view class="preview-area" v-if="tempImagePath">
            <image :src="tempImagePath" mode="aspectFit" class="preview-image"></image>
          </view>
        </view>
        <view class="dialog-footer">
          <button class="cancel-btn" @click="closeUploadDialog">取消</button>
          <button class="confirm-btn" @click="confirmUpload" :disabled="!tempImagePath">确定上传</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
<script>
  import { getAttachmentList, uploadAttachmentFromWechat, deleteAttachment, getWechatAccessToken } from '@/api/task'
  import { formatDateTime } from '@/utils/common'
  export default {
    name: 'AttachmentUpload',
    props: {
      // ä»»åŠ¡ID
      taskId: {
        type: [String, Number],
        required: true
      },
      // æ ‡é¢˜
      title: {
        type: String,
        default: '任务附件'
      },
      // æ˜¯å¦è‡ªåŠ¨åŠ è½½é™„ä»¶åˆ—è¡¨
      autoLoad: {
        type: Boolean,
        default: true
      }
    },
    data() {
      return {
        attachmentList: [],
        categoryList: [
          { label: '知情同意书', value: '1' },
          { label: '病人资料', value: '2' },
          { label: '操作记录', value: '3' },
          { label: '出车前', value: '4' },
          { label: '出车后', value: '5' },
          { label: '系安全带', value: '6' }
        ],
        selectedCategoryIndex: 0,
        tempImagePath: null,
        isWechatMiniProgram: false
      }
    },
    mounted() {
      // æ£€æµ‹æ˜¯å¦æ˜¯å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒ
      // #ifdef MP-WEIXIN
      this.isWechatMiniProgram = true
      // #endif
      // è‡ªåŠ¨åŠ è½½é™„ä»¶åˆ—è¡¨
      if (this.autoLoad && this.taskId) {
        this.loadAttachmentList()
      }
    },
    watch: {
      // ç›‘听taskId变化,重新加载附件列表
      taskId(newVal) {
        if (newVal && this.autoLoad) {
          this.loadAttachmentList()
        }
      }
    },
    methods: {
      // åŠ è½½é™„ä»¶åˆ—è¡¨
      loadAttachmentList() {
        if (!this.taskId) {
          return
        }
        getAttachmentList(this.taskId).then(response => {
          this.attachmentList = response.data || response || []
          this.$emit('loaded', this.attachmentList)
        }).catch(error => {
          console.error('加载附件列表失败:', error)
          this.$emit('error', error)
        })
      },
      // æ˜¾ç¤ºä¸Šä¼ å¯¹è¯æ¡†
      showUploadDialog() {
        this.selectedCategoryIndex = 0
        this.tempImagePath = null
        this.$refs.uploadPopup.open()
      },
      // å…³é—­ä¸Šä¼ å¯¹è¯æ¡†
      closeUploadDialog() {
        this.$refs.uploadPopup.close()
      },
      // åˆ†ç±»é€‰æ‹©å˜åŒ–
      onCategoryChange(e) {
        this.selectedCategoryIndex = e.detail.value
      },
      // é€‰æ‹©å›¾ç‰‡
      chooseImage() {
        const that = this
        uni.chooseImage({
          count: 1,
          sizeType: ['compressed'],
          sourceType: ['album', 'camera'],
          success: function(res) {
            that.tempImagePath = res.tempFilePaths[0]
          },
          fail: function(err) {
            console.error('选择图片失败:', err)
            that.$modal.showToast('选择图片失败')
          }
        })
      },
      // ç¡®è®¤ä¸Šä¼ 
      confirmUpload() {
        if (!this.tempImagePath) {
          this.$modal.showToast('请先选择图片')
          return
        }
        const that = this
        const category = this.categoryList[this.selectedCategoryIndex].value
        // å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒï¼šå…ˆèŽ·å–AccessToken,再上传到微信服务器,最后提交mediaId到后端
        // #ifdef MP-WEIXIN
        if (this.isWechatMiniProgram) {
          uni.showLoading({
            title: '上传中...'
          })
          // ç¬¬ä¸€æ­¥ï¼šä»ŽåŽç«¯èŽ·å–AccessToken
          getWechatAccessToken().then(tokenResponse => {
            // æŽ¥å£è¿”回格式:{"msg":"token值","code":200}
            console.log('获取AccessToken成功:', tokenResponse)
            const accessToken = tokenResponse.msg || tokenResponse.data || tokenResponse
            if (!accessToken) {
              uni.hideLoading()
              that.$modal.showToast('获取AccessToken失败')
              console.error('获取AccessToken失败,响应数据:', tokenResponse)
              return
            }
            console.log('获取到AccessToken:', accessToken)
            // ç¬¬äºŒæ­¥ï¼šä¸Šä¼ åˆ°å¾®ä¿¡æœåС噍
            const uploadUrl = `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${accessToken}&type=image`
            uni.uploadFile({
              url: uploadUrl,
              filePath: that.tempImagePath,
              name: 'media',
              success: function(res) {
                console.log('微信上传响应:', res)
                try {
                  const data = JSON.parse(res.data)
                  if (data.media_id) {
                    // ç¬¬ä¸‰æ­¥ï¼šæäº¤mediaId到后端
                    uploadAttachmentFromWechat(that.taskId, data.media_id, category).then(response => {
                      uni.hideLoading()
                      that.$modal.showToast('上传成功')
                      that.closeUploadDialog()
                      that.loadAttachmentList()
                      that.$emit('uploaded', response)
                    }).catch(error => {
                      uni.hideLoading()
                      console.error('提交mediaId失败:', error)
                      that.$modal.showToast('上传失败:' + (error.msg || '请重试'))
                      that.$emit('error', error)
                    })
                  } else {
                    uni.hideLoading()
                    const errMsg = data.errmsg || '未知错误'
                    console.error('微信返回错误:', data)
                    that.$modal.showToast('微信上传失败:' + errMsg)
                  }
                } catch (e) {
                  uni.hideLoading()
                  console.error('解析微信响应失败:', e, res.data)
                  that.$modal.showToast('上传失败:响应解析错误')
                }
              },
              fail: function(err) {
                uni.hideLoading()
                console.error('上传到微信失败:', err)
                that.$modal.showToast('上传失败:' + (err.errMsg || '请检查网络'))
                that.$emit('error', err)
              }
            })
          }).catch(error => {
            uni.hideLoading()
            console.error('获取AccessToken失败:', error)
            that.$modal.showToast('获取AccessToken失败')
            that.$emit('error', error)
          })
          return
        }
        // #endif
        // éžå¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒï¼šç›´æŽ¥ä¸Šä¼ åˆ°åŽç«¯æœåŠ¡å™¨
        uni.showLoading({
          title: '上传中...'
        })
        uni.uploadFile({
          url: that.$baseUrl + '/task/attachment/upload/' + that.taskId,
          filePath: that.tempImagePath,
          name: 'file',
          formData: {
            'category': category
          },
          header: {
            'Authorization': 'Bearer ' + uni.getStorageSync('token')
          },
          success: function(uploadRes) {
            uni.hideLoading()
            if (uploadRes.statusCode === 200) {
              const result = JSON.parse(uploadRes.data)
              if (result.code === 200) {
                that.$modal.showToast('上传成功')
                that.closeUploadDialog()
                that.loadAttachmentList()
                that.$emit('uploaded', result)
              } else {
                that.$modal.showToast(result.msg || '上传失败')
                that.$emit('error', result)
              }
            } else {
              that.$modal.showToast('上传失败')
              that.$emit('error', uploadRes)
            }
          },
          fail: function(err) {
            uni.hideLoading()
            console.error('上传失败:', err)
            that.$modal.showToast('上传失败')
            that.$emit('error', err)
          }
        })
      },
      // æŸ¥çœ‹é™„ä»¶
      viewAttachment(item) {
        // å¦‚果是图片,使用图片预览
        const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp']
        const fileExt = item.fileName.split('.').pop().toLowerCase()
        if (imageTypes.includes(fileExt)) {
          // ä¼˜å…ˆä½¿ç”¨fileUrl字段(后端拼接好的完整URL)
          let imageUrl = item.fileUrl
          // å¦‚果没有fileUrl,则使用下载接口
          if (!imageUrl) {
            imageUrl = this.$baseUrl + '/task/attachment/download/' + item.attachmentId
          }
          // å¾®ä¿¡å°ç¨‹åºä¸­é¢„览图片
          // #ifdef MP-WEIXIN
          // å¾®ä¿¡å°ç¨‹åºéœ€è¦å…ˆä¸‹è½½åˆ°æœ¬åœ°å†é¢„览
          uni.showLoading({ title: '加载中...' })
          uni.downloadFile({
            url: imageUrl,
            success: function(res) {
              uni.hideLoading()
              if (res.statusCode === 200) {
                uni.previewImage({
                  urls: [res.tempFilePath],
                  current: res.tempFilePath
                })
              } else {
                uni.showToast({ title: '加载图片失败', icon: 'none' })
              }
            },
            fail: function() {
              uni.hideLoading()
              uni.showToast({ title: '下载失败', icon: 'none' })
            }
          })
          // #endif
          // éžå¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒï¼Œç›´æŽ¥é¢„è§ˆ
          // #ifndef MP-WEIXIN
          uni.previewImage({
            urls: [imageUrl],
            current: imageUrl
          })
          // #endif
        } else {
          this.$modal.showToast('仅支持预览图片')
        }
        this.$emit('view', item)
      },
      // åˆ é™¤é™„ä»¶
      deleteAttachment(attachmentId, index) {
        const that = this
        this.$modal.confirm('确定要删除该附件吗?').then(() => {
          deleteAttachment(attachmentId).then(response => {
            that.$modal.showToast('删除成功')
            that.attachmentList.splice(index, 1)
            that.$emit('deleted', attachmentId)
          }).catch(error => {
            console.error('删除附件失败:', error)
            that.$modal.showToast('删除失败')
            that.$emit('error', error)
          })
        }).catch(() => {})
      },
      // èŽ·å–åˆ†ç±»åç§°
      getCategoryName(category) {
        console.log('category', category)
        const item = this.categoryList.find(c => c.value === category)
        return item ? item.label : '未分类'
      },
      // æ ¼å¼åŒ–æ—¶é—´
      formatTime(time) {
        if (!time) return ''
        return formatDateTime(time, 'YYYY-MM-DD HH:mm')
      },
      // æ ¼å¼åŒ–文件大小
      formatFileSize(size) {
        if (!size) return '0B'
        if (size < 1024) return size + 'B'
        if (size < 1024 * 1024) return (size / 1024).toFixed(2) + 'KB'
        return (size / 1024 / 1024).toFixed(2) + 'MB'
      },
      // èŽ·å–å›¾ç‰‡URL
      getImageUrl(item) {
        // ä¼˜å…ˆä½¿ç”¨fileUrl字段(后端拼接好的完整URL)
        if (item.fileUrl) {
          return item.fileUrl
        }
        // ä½¿ç”¨ä¸‹è½½æŽ¥å£
        if (item.attachmentId) {
          return this.$baseUrl + '/task/attachment/download/' + item.attachmentId
        }
        // é»˜è®¤å ä½å›¾
        return '/static/images/default-image.png'
      },
      // åˆ·æ–°é™„件列表(供外部调用)
      refresh() {
        this.loadAttachmentList()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .attachment-upload-container {
    .detail-section {
      background-color: white;
      border-radius: 15rpx;
      padding: 30rpx;
      margin-bottom: 20rpx;
      box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
      .section-title {
        font-size: 32rpx;
        font-weight: bold;
        margin-bottom: 20rpx;
        color: #333;
        border-bottom: 1rpx solid #f0f0f0;
        padding-bottom: 10rpx;
        display: flex;
        justify-content: space-between;
        align-items: center;
        .upload-btn {
          font-size: 24rpx;
          padding: 8rpx 20rpx;
          background-color: #007AFF;
          color: white;
          border-radius: 8rpx;
          border: none;
        }
      }
      .no-attachment {
        text-align: center;
        padding: 40rpx 0;
        color: #999;
        font-size: 28rpx;
      }
      .attachment-item {
        display: flex;
        align-items: center;
        padding: 20rpx;
        margin-bottom: 15rpx;
        background-color: #f9f9f9;
        border-radius: 10rpx;
        &:last-child {
          margin-bottom: 0;
        }
        .attachment-thumbnail {
          width: 120rpx;
          height: 120rpx;
          border-radius: 8rpx;
          overflow: hidden;
          margin-right: 20rpx;
          flex-shrink: 0;
          background-color: #f0f0f0;
          .thumbnail-image {
            width: 100%;
            height: 100%;
          }
        }
        .attachment-info {
          flex: 1;
          margin-right: 20rpx;
          .attachment-category {
            margin-bottom: 8rpx;
            .category-tag {
              display: inline-block;
              padding: 4rpx 12rpx;
              background-color: #007AFF;
              color: white;
              font-size: 22rpx;
              border-radius: 4rpx;
            }
          }
          .attachment-meta {
            font-size: 24rpx;
            color: #999;
            .upload-time {
              margin-right: 20rpx;
            }
          }
        }
        .attachment-actions {
          display: flex;
          flex-direction: column;
          gap: 10rpx;
          .action-btn {
            padding: 8rpx 20rpx;
            font-size: 24rpx;
            border-radius: 6rpx;
            border: none;
            &.view-btn {
              background-color: #007AFF;
              color: white;
            }
            &.delete-btn {
              background-color: #ff3b30;
              color: white;
            }
          }
        }
      }
    }
    .upload-dialog {
      background-color: white;
      border-radius: 20rpx 20rpx 0 0;
      padding: 30rpx;
      .dialog-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 30rpx;
        .dialog-title {
          font-size: 32rpx;
          font-weight: bold;
          color: #333;
        }
      }
      .dialog-content {
        .form-item {
          margin-bottom: 30rpx;
          .form-label {
            font-size: 28rpx;
            color: #333;
            margin-bottom: 15rpx;
          }
          .picker-value {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            font-size: 28rpx;
            color: #333;
          }
          .choose-image-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 30rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            border: 2rpx dashed #ccc;
            color: #666;
            font-size: 28rpx;
            text {
              margin-left: 10rpx;
            }
          }
        }
        .preview-area {
          margin-top: 20rpx;
          .preview-image {
            width: 100%;
            height: 400rpx;
            border-radius: 10rpx;
          }
        }
      }
      .dialog-footer {
        display: flex;
        gap: 20rpx;
        margin-top: 30rpx;
        button {
          flex: 1;
          height: 80rpx;
          border-radius: 10rpx;
          font-size: 30rpx;
          border: none;
        }
        .cancel-btn {
          background-color: #f5f5f5;
          color: #666;
        }
        .confirm-btn {
          background-color: #007AFF;
          color: white;
          &:disabled {
            background-color: #ccc;
          }
        }
      }
    }
  }
</style>
app/pages/task/detail.vue
@@ -195,33 +195,12 @@
      </view>
      
      <!-- é™„件信息 -->
      <view class="detail-section">
        <view class="section-title">
          ä»»åС附件
          <button class="upload-btn" @click="showUploadDialog">上传附件</button>
        </view>
        <view v-if="attachmentList && attachmentList.length > 0">
          <view class="attachment-item" v-for="(item, index) in attachmentList" :key="item.attachmentId">
            <view class="attachment-info">
              <view class="attachment-category">
                <text class="category-tag">{{ getCategoryName(item.attachmentCategory) }}</text>
              </view>
              <view class="attachment-name">{{ item.fileName }}</view>
              <view class="attachment-meta">
                <text class="upload-time">{{ formatTime(item.uploadTime) }}</text>
                <text class="file-size">{{ formatFileSize(item.fileSize) }}</text>
              </view>
            </view>
            <view class="attachment-actions">
              <button class="action-btn view-btn" @click="viewAttachment(item)">查看</button>
              <button class="action-btn delete-btn" @click="deleteAttachment(item.attachmentId, index)">删除</button>
            </view>
          </view>
        </view>
        <view v-else class="no-attachment">
          <text>暂无附件</text>
        </view>
      </view>
      <AttachmentUpload
        :taskId="taskId"
        title="任务附件"
        @uploaded="onAttachmentUploaded"
        @deleted="onAttachmentDeleted"
      />
      
      <!-- ç¦ç¥‰è½¦ä»»åŠ¡ç‰¹æœ‰ä¿¡æ¯ -->
      <view class="detail-section" v-if="taskDetail.taskType === 'WELFARE' && taskDetail.welfareInfo">
@@ -343,66 +322,22 @@
      
      <!-- å·²å®Œæˆ/已取消: ä¸æ˜¾ç¤ºæŒ‰é’® -->
    </view>
    <!-- é™„件上传对话框 -->
    <uni-popup ref="uploadPopup" type="bottom">
      <view class="upload-dialog">
        <view class="dialog-header">
          <text class="dialog-title">上传附件</text>
          <uni-icons type="closeempty" size="24" @click="closeUploadDialog"></uni-icons>
        </view>
        <view class="dialog-content">
          <view class="form-item">
            <view class="form-label">附件分类</view>
            <picker @change="onCategoryChange" :value="selectedCategoryIndex" :range="categoryList" range-key="label">
              <view class="picker-value">
                {{ categoryList[selectedCategoryIndex].label }}
                <uni-icons type="arrowdown" size="16"></uni-icons>
              </view>
            </picker>
          </view>
          <view class="form-item">
            <view class="form-label">选择图片</view>
            <button class="choose-image-btn" @click="chooseImage">
              <uni-icons type="image" size="20"></uni-icons>
              <text>点击选择</text>
            </button>
          </view>
          <view class="preview-area" v-if="tempImagePath">
            <image :src="tempImagePath" mode="aspectFit" class="preview-image"></image>
          </view>
        </view>
        <view class="dialog-footer">
          <button class="cancel-btn" @click="closeUploadDialog">取消</button>
          <button class="confirm-btn" @click="confirmUpload" :disabled="!tempImagePath">确定上传</button>
        </view>
      </view>
    </uni-popup>
  </view>
</template>
<script>
  import { getTask, changeTaskStatus } from '@/api/task'
  import { getAttachmentList, uploadAttachmentFromWechat, deleteAttachment, getWechatAccessToken } from '@/api/task'
  import { formatDateTime } from '@/utils/common'
  import AttachmentUpload from '@/components/AttachmentUpload.vue'
  
  export default {
    components: {
      AttachmentUpload
    },
    data() {
      return {
        taskDetail: null,
        taskId: null,
        attachmentList: [],
        categoryList: [
          { label: '知情同意书', value: '1' },
          { label: '病人资料', value: '2' },
          { label: '操作记录', value: '3' },
          { label: '出车前', value: '4' },
          { label: '出车后', value: '5' },
          { label: '系安全带', value: '6' }
        ],
        selectedCategoryIndex: 0,
        tempImagePath: null,
        isWechatMiniProgram: false // æ˜¯å¦æ˜¯å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒ
        taskId: null
      }
    },
    computed: {
@@ -464,12 +399,6 @@
    onLoad(options) {
      this.taskId = options.id
      this.loadTaskDetail()
      this.loadAttachmentList()
      // æ£€æµ‹æ˜¯å¦æ˜¯å¾®ä¿¡å°ç¨‹åºçŽ¯å¢ƒ
      // #ifdef MP-WEIXIN
      this.isWechatMiniProgram = true
      // #endif
    },
    methods: {
      // åŠ è½½ä»»åŠ¡è¯¦æƒ…
@@ -923,6 +852,16 @@
        if (size < 1024) return size + 'B'
        if (size < 1024 * 1024) return (size / 1024).toFixed(2) + 'KB'
        return (size / 1024 / 1024).toFixed(2) + 'MB'
      },
      // é™„件上传成功回调
      onAttachmentUploaded(response) {
        console.log('附件上传成功:', response)
      },
      // é™„件删除成功回调
      onAttachmentDeleted(attachmentId) {
        console.log('附件删除成功:', attachmentId)
      }
    }
  }
@@ -1038,84 +977,6 @@
        padding: 20rpx;
        border-radius: 10rpx;
      }
      .no-attachment {
        text-align: center;
        padding: 40rpx 0;
        color: #999;
        font-size: 28rpx;
      }
      .attachment-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx;
        margin-bottom: 15rpx;
        background-color: #f9f9f9;
        border-radius: 10rpx;
        &:last-child {
          margin-bottom: 0;
        }
        .attachment-info {
          flex: 1;
          margin-right: 20rpx;
          .attachment-category {
            margin-bottom: 8rpx;
            .category-tag {
              display: inline-block;
              padding: 4rpx 12rpx;
              background-color: #007AFF;
              color: white;
              font-size: 22rpx;
              border-radius: 4rpx;
            }
          }
          .attachment-name {
            font-size: 28rpx;
            color: #333;
            margin-bottom: 8rpx;
            word-break: break-all;
          }
          .attachment-meta {
            font-size: 24rpx;
            color: #999;
            .upload-time {
              margin-right: 20rpx;
            }
          }
        }
        .attachment-actions {
          display: flex;
          flex-direction: column;
          gap: 10rpx;
          .action-btn {
            padding: 8rpx 20rpx;
            font-size: 24rpx;
            border-radius: 6rpx;
            border: none;
            &.view-btn {
              background-color: #007AFF;
              color: white;
            }
            &.delete-btn {
              background-color: #ff3b30;
              color: white;
            }
          }
        }
      }
    }
    
    .loading {
@@ -1167,102 +1028,6 @@
        
        &:last-child {
          margin-right: 0;
        }
      }
    }
    .upload-dialog {
      background-color: white;
      border-radius: 20rpx 20rpx 0 0;
      padding: 30rpx;
      .dialog-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 30rpx;
        .dialog-title {
          font-size: 32rpx;
          font-weight: bold;
          color: #333;
        }
      }
      .dialog-content {
        .form-item {
          margin-bottom: 30rpx;
          .form-label {
            font-size: 28rpx;
            color: #333;
            margin-bottom: 15rpx;
          }
          .picker-value {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            font-size: 28rpx;
            color: #333;
          }
          .choose-image-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 30rpx;
            background-color: #f5f5f5;
            border-radius: 10rpx;
            border: 2rpx dashed #ccc;
            color: #666;
            font-size: 28rpx;
            text {
              margin-left: 10rpx;
            }
          }
        }
        .preview-area {
          margin-top: 20rpx;
          .preview-image {
            width: 100%;
            height: 400rpx;
            border-radius: 10rpx;
          }
        }
      }
      .dialog-footer {
        display: flex;
        gap: 20rpx;
        margin-top: 30rpx;
        button {
          flex: 1;
          height: 80rpx;
          border-radius: 10rpx;
          font-size: 30rpx;
          border: none;
        }
        .cancel-btn {
          background-color: #f5f5f5;
          color: #666;
        }
        .confirm-btn {
          background-color: #007AFF;
          color: white;
          &:disabled {
            background-color: #ccc;
          }
        }
      }
    }
prd/ÈÎÎñ¸½¼þͬ²½ImageData¹¦ÄÜ˵Ã÷.md
New file
@@ -0,0 +1,223 @@
# ä»»åŠ¡é™„ä»¶åŒæ­¥ImageData功能说明
## åŠŸèƒ½æ¦‚è¿°
本功能实现了将任务附件(sys_task_attachment表)自动同步到ImageData表的能力。当任务关联了服务单(serviceOrderId)或调度单(dispatchOrdId)后,系统会自动将任务附件同步到ImageData表,以便在旧系统中查看和使用。
## æ ¸å¿ƒåŠŸèƒ½
### 1. è‡ªåŠ¨åŒæ­¥
- ä¸Šä¼ é™„件时,如果任务已关联服务单或调度单,自动同步到ImageData
- æ”¯æŒæ™®é€šæ–‡ä»¶ä¸Šä¼ å’Œå¾®ä¿¡ä¸Šä¼ ä¸¤ç§æ–¹å¼
- åŒæ­¥å¤±è´¥ä¸å½±å“é™„件上传的主流程
### 2. æ‰‹åŠ¨åŒæ­¥
- æä¾›API接口,支持手动触发同步
- æ”¯æŒæ‰¹é‡åŒæ­¥ä»»åŠ¡çš„æ‰€æœ‰é™„ä»¶
- æ”¯æŒæ£€æŸ¥é™„件是否已同步
### 3. é™„件分类映射
附件分类自动映射到ImageData的图片类型:
- 1-知情同意书 â†’ ImageType 0
- 2-病人资料 â†’ ImageType 1
- 3-操作记录 â†’ ImageType 2
- 4-出车前 â†’ ImageType 3
- 5-出车后 â†’ ImageType 4
- 6-系安全带 â†’ ImageType 5
## æ•°æ®åº“变更
### sys_task_attachment è¡¨æ–°å¢žå­—段
```sql
-- æ˜¯å¦å·²åŒæ­¥åˆ°ImageData (0-未同步 1-已同步)
synced_to_image_data TINYINT(1) DEFAULT 0
-- åŒæ­¥æ—¶é—´
sync_time DATETIME
-- å…³è”çš„ImageData ID
image_data_id BIGINT
```
### æ‰§è¡ŒSQL脚本
```bash
执行文件: sql/update_sys_task_attachment_sync_fields.sql
```
## æ–°å¢žæ–‡ä»¶
### 1. æœåŠ¡æŽ¥å£
- `ITaskAttachmentSyncService.java` - é™„件同步服务接口
- `TaskAttachmentSyncServiceImpl.java` - é™„件同步服务实现
### 2. Controller
- `TaskAttachmentSyncController.java` - é™„件同步控制器
### 3. SQL脚本
- `update_sys_task_attachment_sync_fields.sql` - æ•°æ®åº“更新脚本
## ä¿®æ”¹æ–‡ä»¶
### 1. å®žä½“ç±»
- `SysTaskAttachment.java` - æ·»åŠ åŒæ­¥ç›¸å…³å­—æ®µ
### 2. Mapper
- `SysTaskAttachmentMapper.xml` - æ›´æ–°SQL语句
### 3. æœåŠ¡å®žçŽ°
- `SysTaskServiceImpl.java` - é›†æˆè‡ªåŠ¨åŒæ­¥é€»è¾‘
## API接口
### 1. æ‰‹åŠ¨åŒæ­¥ä»»åŠ¡é™„ä»¶
**接口**: `POST /task/attachment/sync/task/{taskId}`
**权限**: `task:general:edit`
**参数**:
- `taskId`: ä»»åŠ¡ID(路径参数)
**返回示例**:
```json
{
  "code": 200,
  "msg": "成功同步 3 ä¸ªé™„ä»¶"
}
```
### 2. æ£€æŸ¥é™„件同步状态
**接口**: `GET /task/attachment/sync/check/{attachmentId}`
**权限**: `task:general:query`
**参数**:
- `attachmentId`: é™„ä»¶ID(路径参数)
**返回示例**:
```json
{
  "code": 200,
  "msg": "已同步",
  "data": true
}
```
## ä½¿ç”¨åœºæ™¯
### åœºæ™¯1:新上传附件自动同步
1. ç”¨æˆ·åœ¨å°ç¨‹åºä¸Šä¼ ä»»åС附件
2. ç³»ç»Ÿä¿å­˜é™„件到sys_task_attachment表
3. æ£€æµ‹ä»»åŠ¡æ˜¯å¦å·²å…³è”æœåŠ¡å•/调度单
4. å¦‚果已关联,自动同步到ImageData表
5. æ›´æ–°é™„件的同步状态字段
### åœºæ™¯2:手动批量同步
1. ä»»åŠ¡åŽæœŸå…³è”äº†æœåŠ¡å•/调度单
2. è°ƒç”¨æ‰‹åŠ¨åŒæ­¥æŽ¥å£åŒæ­¥åŽ†å²é™„ä»¶
3. ç³»ç»Ÿæ‰¹é‡åŒæ­¥è¯¥ä»»åŠ¡çš„æ‰€æœ‰æœªåŒæ­¥é™„ä»¶
4. è¿”回同步成功的附件数量
### åœºæ™¯3:旧系统查看附件
1. ç”¨æˆ·åœ¨æ—§ç³»ç»Ÿä¸­æŸ¥çœ‹è°ƒåº¦å•
2. è°ƒç”¨ImageData接口获取图片列表
3. æ˜¾ç¤ºé€šè¿‡æ–°ç³»ç»Ÿä¸Šä¼ çš„任务附件
4. å®žçŽ°æ–°æ—§ç³»ç»Ÿæ•°æ®äº’é€š
## æŠ€æœ¯å®žçް
### 1. åŒæ­¥æœåŠ¡
```java
@Service
public class TaskAttachmentSyncServiceImpl implements ITaskAttachmentSyncService {
    // åŒæ­¥å•个附件
    public boolean syncAttachmentToImageData(
        SysTaskAttachment attachment,
        Long serviceOrderId,
        Long dispatchOrdId);
    // æ‰¹é‡åŒæ­¥ä»»åС附件
    public int syncTaskAttachmentsToImageData(
        Long taskId,
        Long serviceOrderId,
        Long dispatchOrdId);
}
```
### 2. è‡ªåŠ¨åŒæ­¥è§¦å‘
在上传附件时自动触发:
```java
// ä¸Šä¼ é™„件成功后
if (result > 0) {
    // å°è¯•自动同步到ImageData
    tryAutoSyncAttachment(attachment, taskId);
}
```
### 3. æ•°æ®æºåˆ‡æ¢
同步到ImageData时自动切换到SQL Server数据源:
```java
@DataSource(DataSourceType.SQLSERVER)
public boolean syncAttachmentToImageData(...) {
    // åœ¨SQL Server数据源中执行
    imageDataService.insertImageData(imageData);
}
```
## æ³¨æ„äº‹é¡¹
### 1. æ•°æ®æºé…ç½®
- ç¡®ä¿SQL Server数据源配置正确
- ç¡®ä¿ImageData表存在且可访问
### 2. æ–‡ä»¶è·¯å¾„
- é™„件路径使用相对路径存储
- åŒæ­¥æ—¶ä¿æŒè·¯å¾„格式一致
### 3. äº‹åŠ¡ç®¡ç†
- åŒæ­¥å¤±è´¥ä¸å½±å“ä¸»æµç¨‹
- é”™è¯¯ä¿¡æ¯ä»…记录日志
### 4. æ€§èƒ½è€ƒè™‘
- è‡ªåŠ¨åŒæ­¥æ˜¯å¼‚æ­¥çš„ï¼Œä¸å½±å“ç”¨æˆ·ä½“éªŒ
- æ‰¹é‡åŒæ­¥æ—¶é€ä¸ªå¤„理,避免事务超时
## å¸¸è§é—®é¢˜
### Q1: å¦‚何判断附件是否需要同步?
A: æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å…³è”äº†serviceOrderId或dispatchOrdId
### Q2: åŒæ­¥å¤±è´¥ä¼šå½±å“é™„件上传吗?
A: ä¸ä¼šï¼ŒåŒæ­¥å¤±è´¥åªè®°å½•日志,不影响主流程
### Q3: å¦‚何重新同步失败的附件?
A: è°ƒç”¨æ‰‹åŠ¨åŒæ­¥æŽ¥å£ï¼Œç³»ç»Ÿä¼šè‡ªåŠ¨è·³è¿‡å·²åŒæ­¥çš„é™„ä»¶
### Q4: ImageData的图片类型如何确定?
A: æ ¹æ®é™„件分类自动映射到对应的ImageType
## æ‰©å±•建议
### 1. å®šæ—¶åŒæ­¥ä»»åŠ¡
可以添加定时任务,定期检查并同步未同步的附件
### 2. åŒæ­¥çŠ¶æ€é€šçŸ¥
可以在同步完成后发送通知给相关人员
### 3. åŒæ­¥æ—¥å¿—
可以添加详细的同步日志表,记录每次同步的详细信息
### 4. æ‰¹é‡æ“ä½œ
可以添加批量同步多个任务附件的功能
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
@@ -1,5 +1,6 @@
package com.ruoyi.web.controller.task;
import java.util.Date;
import java.util.List;
import java.io.File;
import java.io.FileInputStream;
@@ -9,6 +10,11 @@
import com.ruoyi.common.utils.WechatUtils;
import com.ruoyi.common.config.WechatConfig;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.service.ISysTaskEmergencyService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import com.ruoyi.system.task.ITaskAttachmentService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -38,10 +44,24 @@
    
    @Autowired
    private ISysTaskService sysTaskService;
    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private ITaskAttachmentSyncService taskAttachmentSyncService;
    @Autowired
    private ITaskAttachmentService taskAttachmentService;
    
    @Autowired
    private WechatConfig wechatConfig;
    @Autowired
    private ISysTaskEmergencyService sysTaskEmergencyService;
    @Autowired
    private ISysUserService userService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡é™„ä»¶åˆ—è¡¨
     */
@@ -62,8 +82,10 @@
                            @RequestParam("file") MultipartFile file,
                            @RequestParam(value = "category", required = false) String category) {
        try {
            int result = sysTaskService.uploadAttachment(taskId, file, category);
            if (result > 0) {
            Long attachmentId= sysTaskService.uploadAttachment(taskId, file, category);
            if (attachmentId > 0) {
                //在这里进行同步
                SyncAttachmentToImageData(taskId,sysTaskService.getTaskDetail(taskId).getCreatorId(),attachmentId);
                return success("上传成功");
            } else {
                return error("上传失败");
@@ -73,6 +95,27 @@
        }
    }
    private void SyncAttachmentToImageData(Long taskId,Long taskCreatorId, Long attachmentId) {
       Integer oaUserId= sysUserService.selectUserById(taskCreatorId).getOaUserId();
        SysTaskEmergency taskEmergency= sysTaskEmergencyService.selectSysTaskEmergencyByTaskId(taskId);
        if(taskEmergency!=null ){
            Long serviceOrderId= taskEmergency.getLegacyServiceOrdId();
            Long dispathOrdId=taskEmergency.getLegacyDispatchOrdId();
            if(serviceOrderId!=null && dispathOrdId!=null){
                SysTaskAttachment attachment=sysTaskService.getAttachmentById(attachmentId);
               Long imageDataId= taskAttachmentSyncService.syncAttachmentToImageData(attachment, serviceOrderId, dispathOrdId,oaUserId);
                //同步成功
                if(imageDataId>0){
                    attachment.setSyncedToImageData(1);
                    attachment.setSyncTime(new Date());
                    attachment.setImageDataId(imageDataId);
                    taskAttachmentService.updateAttachment(attachment);
                }
            }
        }
    }
    /**
     * åˆ é™¤ä»»åС附件
     */
@@ -112,8 +155,10 @@
            }
            
            // é€šè¿‡mediaId上传附件
            int result = sysTaskService.uploadAttachmentFromWechat(taskId, accessToken, mediaId, category);
            if (result > 0) {
            Long attachmentId = sysTaskService.uploadAttachmentFromWechat(taskId, accessToken, mediaId, category);
            SysTask task=sysTaskService.getTaskDetail(taskId);
            if (attachmentId > 0) {
                this.SyncAttachmentToImageData(taskId,task.getCreatorId(),attachmentId);
                return success("上传成功");
            } else {
                return error("上传失败");
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskAttachmentSyncController.java
New file
@@ -0,0 +1,108 @@
package com.ruoyi.web.controller.task;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.service.ISysTaskService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import com.ruoyi.system.task.ITaskAttachmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * ä»»åŠ¡é™„ä»¶åŒæ­¥Controller
 * ç”¨äºŽå°†ä»»åŠ¡é™„ä»¶åŒæ­¥åˆ°ImageData表
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/task/attachment/sync")
public class TaskAttachmentSyncController extends BaseController {
    @Autowired
    private ITaskAttachmentSyncService taskAttachmentSyncService;
    @Autowired
    private ITaskAttachmentService taskAttachmentService;
    @Autowired
    private ISysTaskService sysTaskService;
    @Autowired
    private ISysUserService sysUserService;
    /**
     * æ‰‹åŠ¨åŒæ­¥å•ä¸ªä»»åŠ¡çš„é™„ä»¶åˆ°ImageData
     *
     * @param taskId ä»»åŠ¡ID
     * @return åŒæ­¥ç»“æžœ
     */
    @PreAuthorize("@ss.hasPermi('task:general:edit')")
    @Log(title = "任务附件同步", businessType = BusinessType.OTHER)
    @PostMapping("/task/{taskId}")
    public AjaxResult syncTaskAttachments(@PathVariable("taskId") Long taskId) {
        try {
            // æŸ¥è¯¢ä»»åŠ¡è¯¦æƒ…
            SysTask task = sysTaskService.getTaskDetail(taskId);
            SysUser user = sysUserService.selectUserById(task.getCreatorId());
            Integer oaUserId=user.getOaUserId();
            if (task == null) {
                return error("任务不存在");
            }
            // èŽ·å–serviceOrderId和dispatchOrdId
            Long serviceOrderId = null;
            Long dispatchOrdId = null;
            if (task.getEmergencyInfo() != null) {
                serviceOrderId = task.getEmergencyInfo().getLegacyServiceOrdId();
                dispatchOrdId = task.getEmergencyInfo().getLegacyDispatchOrdId();
            }
            if (serviceOrderId == null && dispatchOrdId == null) {
                return error("任务未关联服务单或调度单,无法同步");
            }
            List<SysTaskAttachment> attachmentList=sysTaskService.getAttachmentsByTaskId(taskId);
            // æ‰§è¡ŒåŒæ­¥
            List<SysTaskAttachment> attachments = taskAttachmentSyncService.syncTaskAttachmentsToImageData(
                    attachmentList, serviceOrderId, dispatchOrdId,oaUserId);
            for(SysTaskAttachment attachment:attachments){
                taskAttachmentService.updateAttachment(attachment);
            }
            return success("成功同步 " + attachments.stream().count() + " ä¸ªé™„ä»¶");
        } catch (Exception e) {
            logger.error("同步任务附件失败", e);
            return error("同步失败:" + e.getMessage());
        }
    }
    /**
     * æ£€æŸ¥é™„件是否已同步
     *
     * @param attachmentId é™„ä»¶ID
     * @return åŒæ­¥çŠ¶æ€
     */
    @PreAuthorize("@ss.hasPermi('task:general:query')")
    @GetMapping("/check/{attachmentId}")
    public AjaxResult checkAttachmentSynced(@PathVariable("attachmentId") Long attachmentId) {
        try {
            boolean synced = taskAttachmentService.isAttachmentSynced(attachmentId);
            return AjaxResult.success(synced ? "已同步" : "未同步", synced);
        } catch (Exception e) {
            logger.error("检查附件同步状态失败", e);
            return error("检查失败:" + e.getMessage());
        }
    }
}
ruoyi-admin/src/main/resources/application.yml
@@ -14,6 +14,8 @@
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # å›¾ç‰‡èµ„源域名(用于访问附件图片)
  imageUrl: https://dsp.966120.com.cn/images/
 
# å¼€å‘环境配置
ruoyi-common/src/main/java/com/ruoyi/common/config/ImageUrlConfig.java
New file
@@ -0,0 +1,27 @@
package com.ruoyi.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * å›¾ç‰‡URL配置
 *
 * @author ruoyi
 */
@Component
@ConfigurationProperties(prefix = "ruoyi")
public class ImageUrlConfig {
    /**
     * å›¾ç‰‡èµ„源域名
     */
    private String imageUrl;
    public String getImageUrl() {
        return imageUrl;
    }
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }
}
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
@@ -1,5 +1,6 @@
package com.ruoyi.quartz.task;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,6 +29,8 @@
    
    @Autowired
    private ITaskStatusPushService taskStatusPushService;
    
    /**
     * æ‰¹é‡åŒæ­¥æœªåŒæ­¥çš„æ€¥æ•‘转运任务到旧系统
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskAttachment.java
@@ -2,8 +2,10 @@
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.springframework.beans.factory.annotation.Value;
/**
 * ä»»åŠ¡é™„ä»¶å¯¹è±¡ sys_task_attachment
@@ -37,6 +39,7 @@
    @Excel(name = "文件类型")
    private String fileType;
    /** é™„件分类 */
    @Excel(name = "附件分类")
    private String attachmentCategory;
@@ -49,6 +52,19 @@
    /** ä¸Šä¼ è€… */
    @Excel(name = "上传者")
    private String uploadBy;
    /** æ˜¯å¦å·²åŒæ­¥åˆ°ImageData (0-未同步 1-已同步) */
    private Integer syncedToImageData;
    /** åŒæ­¥æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date syncTime;
    /** å…³è”çš„ImageData ID */
    private Long imageDataId;
    /** å®Œæ•´æ–‡ä»¶URL(不存储,用于返回给前端) */
    private String fileUrl;
    public void setAttachmentId(Long attachmentId) {
        this.attachmentId = attachmentId;
@@ -121,6 +137,38 @@
    public String getUploadBy() {
        return uploadBy;
    }
    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }
    public String getFileUrl() {
        return fileUrl;
    }
    public void setSyncedToImageData(Integer syncedToImageData) {
        this.syncedToImageData = syncedToImageData;
    }
    public Integer getSyncedToImageData() {
        return syncedToImageData;
    }
    public void setSyncTime(Date syncTime) {
        this.syncTime = syncTime;
    }
    public Date getSyncTime() {
        return syncTime;
    }
    public void setImageDataId(Long imageDataId) {
        this.imageDataId = imageDataId;
    }
    public Long getImageDataId() {
        return imageDataId;
    }
    @Override
    public String toString() {
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
@@ -129,6 +129,8 @@
    /** è°ƒåº¦å•同步错误信息 */
    private String dispatchSyncErrorMsg;
    public Long getId() {
        return id;
    }
ruoyi-system/src/main/java/com/ruoyi/system/file/FileUploadServiceImpl.java
@@ -85,7 +85,7 @@
            log.info("开始上传文件到PHP接口: fileName={}, targetPath={}", fileName, targetPath);
            
            // è°ƒç”¨PHP上传接口
            String response = HttpUtils.postFile(legacyConfig.getFileServerUrl(), params, fileName);
            String response = HttpUtils.postFile(legacyConfig.getFileUploadUrl(), params, fileName);
            
            log.info("PHP接口响应: {}", response);
            
@@ -305,6 +305,8 @@
                        int end = dataSection.indexOf("\"", start);
                        if (start > 11 && end > start) {
                            String filePath = dataSection.substring(start, end);
                            // åŽ»é™¤è½¬ä¹‰å­—ç¬¦
                            filePath = filePath.replace("\\", "");
                            log.info("提取到文件路径: {}", filePath);
                            return filePath;
                        }
@@ -346,6 +348,7 @@
                        if (start > 16 && end > start) {
                            String thumbnailPath = dataSection.substring(start, end);
                            log.info("提取到缩略图路径: {}", thumbnailPath);
                            thumbnailPath = thumbnailPath.replace("\\", ""); // åŽ»é™¤è½¬ä¹‰å­—ç¬¦
                            return thumbnailPath;
                        }
                    }
@@ -359,6 +362,7 @@
                if (start > 16 && end > start) {
                    String thumbnailPath = response.substring(start, end);
                    log.info("提取到缩略图路径(旧格式): {}", thumbnailPath);
                    thumbnailPath = thumbnailPath.replace("\\", ""); // åŽ»é™¤è½¬ä¹‰å­—ç¬¦
                    return thumbnailPath;
                }
            }
@@ -471,20 +475,7 @@
                return uploadResponse;
            }
            
            // å¦‚果是图片,生成缩略图
            if (isImage && uploadResponse.getFilePath() != null) {
                String originalPath = uploadResponse.getFilePath();
                String thumbnailPath = generateThumbnailPath(originalPath);
                // ç”Ÿæˆç¼©ç•¥å›¾
                if (createThumbnail(originalPath, thumbnailPath, 100, 0)) {
                    uploadResponse.setThumbnailPath(thumbnailPath);
                    log.info("缩略图生成成功:{}", thumbnailPath);
                } else {
                    log.warn("缩略图生成失败:{}", originalPath);
                }
            }
            log.info("上传文件成功 æ–‡ä»¶:{} ç¼©ç•¥:{}",uploadResponse.getFilePath(),uploadResponse.getThumbnailPath());
            return uploadResponse;
            
        } catch (Exception e) {
ruoyi-system/src/main/java/com/ruoyi/system/imagedata/ImageDataServiceImpl.java
@@ -223,10 +223,6 @@
            if (dispatchOrdID != null && dispatchOrdID > 0) {
                return processDispatchOrderImage(dispatchOrdID, serviceOrdID, mediaId, imageTypeEnum, adminId);
            }
            // å¦‚果只有OA用户ID,则更新用户头像
            else if (oaid != null && oaid > 0) {
                return updateUserAvatar(oaid, mediaId);
            }
            else {
                return "参数错误:缺少必要的参数";
            }
@@ -270,23 +266,6 @@
        }
    }
    /**
     * æ›´æ–°ç”¨æˆ·å¤´åƒ
     */
    private String updateUserAvatar(Integer oaid, String mediaId) {
        try {
            // è¿™é‡Œéœ€è¦è°ƒç”¨OA用户服务来更新头像
            // ç”±äºŽæ²¡æœ‰OA用户服务的具体实现,这里只是示例
            String avatarUrl = "/upload/" + oaid + "_" + mediaId + ".jpg";
            // TODO: è°ƒç”¨OA用户服务更新头像
            // oaUserService.updateAvatar(oaid, avatarUrl);
            return "用户头像更新成功";
        } catch (Exception e) {
            return "更新用户头像失败:" + e.getMessage();
        }
    }
    /**
     * ç”Ÿæˆå›¾ç‰‡URL
@@ -435,20 +414,7 @@
        }
    }
    /**
     * æ›´æ–°ç”¨æˆ·å¤´åƒï¼ˆåŒ…含文件URL)
     */
    private String updateUserAvatarWithFile(Integer oaid, String mediaId, String imageUrl) {
        try {
            // è¿™é‡Œéœ€è¦è°ƒç”¨OA用户服务来更新头像
            // TODO: è°ƒç”¨OA用户服务更新头像
            // oaUserService.updateAvatar(oaid, imageUrl);
            return "用户头像更新成功,URL:" + imageUrl;
        } catch (Exception e) {
            return "更新用户头像失败:" + e.getMessage();
        }
    }
    /**
     * æ£€æŸ¥æ–‡ä»¶å…¼å®¹æ€§ï¼ˆç¡®ä¿ä¸Žæ—§ç³»ç»Ÿå…¼å®¹ï¼‰
@@ -529,9 +495,6 @@
                ImageTypeEnum imageTypeEnum = ImageTypeEnum.getByCode(imageType);
                return processDispatchOrderImageWithFiles(dispatchOrdID, serviceOrdID, null,
                        imageTypeEnum, adminId, imageUrl, thumbnailUrl);
            } else if (oaid != null && oaid > 0) {
                // OA用户头像
                return updateUserAvatarWithFile(oaid, null, imageUrl);
            } else {
                return "参数错误:缺少必要的参数(调度单ID或OA用户ID)";
            }
@@ -612,8 +575,6 @@
            if (dispatchOrdID != null && dispatchOrdID > 0) {
                return processDispatchOrderImageWithFiles(dispatchOrdID, null, mediaId,
                        ImageTypeEnum.getByCode(imageType), adminId, originalImageUrl, thumbnailUrl);
            } else if (oaid != null && oaid > 0) {
                return updateUserAvatarWithFile(oaid, mediaId, originalImageUrl);
            }
            return "图片上传成功,文件路径:" + originalImageUrl;
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java
@@ -41,7 +41,7 @@
     * @param sysTaskAttachment ä»»åС附件
     * @return ç»“æžœ
     */
    public int insertSysTaskAttachment(SysTaskAttachment sysTaskAttachment);
    public Long insertSysTaskAttachment(SysTaskAttachment sysTaskAttachment);
    /**
     * ä¿®æ”¹ä»»åС附件
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -99,7 +99,7 @@
     * @param category é™„件分类
     * @return ç»“æžœ
     */
    public int uploadAttachment(Long taskId, MultipartFile file, String category);
    public Long uploadAttachment(Long taskId, MultipartFile file, String category);
    
    /**
     * ä»Žå¾®ä¿¡mediaId上传任务附件
@@ -108,9 +108,9 @@
     * @param accessToken å¾®ä¿¡AccessToken
     * @param mediaId å¾®ä¿¡mediaId
     * @param category é™„件分类
     * @return ç»“æžœ
     * @return
     */
    public int uploadAttachmentFromWechat(Long taskId, String accessToken, String mediaId, String category);
    public Long uploadAttachmentFromWechat(Long taskId, String accessToken, String mediaId, String category);
    /**
     * åˆ é™¤ä»»åС附件
@@ -128,6 +128,8 @@
     */
    public SysTaskAttachment getAttachmentById(Long attachmentId);
    public List<SysTaskAttachment> getAttachmentsByTaskId(Long taskId);
    /**
     * åˆ†é…è½¦è¾†ç»™ä»»åŠ¡
     * 
ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.system.service;
import com.ruoyi.system.domain.SysTaskAttachment;
import java.util.List;
/**
 * ä»»åŠ¡é™„ä»¶åŒæ­¥æœåŠ¡æŽ¥å£
 * ç”¨äºŽå°†ä»»åŠ¡é™„ä»¶åŒæ­¥åˆ°ImageData表
 *
 * @author ruoyi
 */
public interface ITaskAttachmentSyncService {
    /**
     * åŒæ­¥å•个附件到ImageData
     *
     * @param attachment ä»»åŠ¡é™„ä»¶å¯¹è±¡
     * @param serviceOrderId æœåŠ¡å•ID
     * @param dispatchOrdId è°ƒåº¦å•ID
     * @return æ˜¯å¦åŒæ­¥æˆåŠŸ
     */
    Long syncAttachmentToImageData(SysTaskAttachment attachment, Long serviceOrderId, Long dispatchOrdId,Integer oaUserId);
    /**
     * æ‰¹é‡åŒæ­¥ä»»åŠ¡çš„æ‰€æœ‰é™„ä»¶åˆ°ImageData
     *
     * @param attachmentList ä»»åŠ¡åˆ—è¡¨
     * @param serviceOrderId æœåŠ¡å•ID
     * @param dispatchOrdId è°ƒåº¦å•ID
     * @return åŒæ­¥æˆåŠŸçš„é™„ä»¶æ•°é‡
     */
    List<SysTaskAttachment> syncTaskAttachmentsToImageData(List<SysTaskAttachment> attachmentList, Long serviceOrderId, Long dispatchOrdId, Integer oaUserId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacySystemSyncServiceImpl.java
@@ -12,6 +12,9 @@
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.*;
import com.ruoyi.system.service.*;
import com.ruoyi.system.task.ITaskAttachmentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,11 +25,6 @@
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.domain.SysTaskVehicle;
import com.ruoyi.system.domain.SysTaskAssignee;
import com.ruoyi.system.domain.VehicleInfo;
import com.ruoyi.system.domain.vo.TaskCreateVO;
import com.ruoyi.system.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
@@ -35,10 +33,7 @@
import com.ruoyi.system.mapper.VehicleInfoMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.system.service.ILegacySystemSyncService;
import com.ruoyi.system.utils.TaskStatusConverter;
import com.ruoyi.system.service.IDispatchOrdService;
import com.ruoyi.system.service.ISysTaskEmergencyService;
/**
 * æ—§ç³»ç»ŸåŒæ­¥Service业务层处理
@@ -77,6 +72,15 @@
    
    @Autowired
    private ISysTaskEmergencyService sysTaskEmergencyService;
    @Autowired
    private ITaskAttachmentSyncService taskAttachmentSyncService;
    @Autowired
    private ISysTaskService sysTaskService;
    @Autowired
    private ITaskAttachmentService taskAttachmentService;
    
    /**
     * åŒæ­¥æ€¥æ•‘转运任务到旧系统
@@ -265,7 +269,27 @@
            return false;
        }
    }
    /**
     * åŒæ­¥ä»»åŠ¡é™„ä»¶åˆ°æ—§ç³»ç»Ÿ
     * @param taskAttachments
     * @param serviceOrdId
     * @param dispatchOrdId
     * @param oaUserID
     */
    public void syncAttachmentToLegacy(List<SysTaskAttachment> taskAttachments, Long serviceOrdId,Long dispatchOrdId,Integer oaUserID) {
        taskAttachments.forEach((taskAttachment)->{
            Long imageDataId=  taskAttachmentSyncService.syncAttachmentToImageData(taskAttachment,serviceOrdId,dispatchOrdId,oaUserID);
            if (imageDataId != null && imageDataId > 0) {
                taskAttachment.setSyncedToImageData(1);
                taskAttachment.setSyncTime(new Date());
                taskAttachment.setImageDataId(imageDataId);
                taskAttachmentService.updateAttachment(taskAttachment);
            }
        });
    }
    /**
     * åŒæ­¥è°ƒåº¦å•到旧系统(admin_save_24.asp)
     */
@@ -303,16 +327,23 @@
                log.info("调度单已同步过,任务ID: {}, DispatchOrdID: {}", taskId, emergency.getLegacyDispatchOrdId());
                return emergency.getLegacyDispatchOrdId();
            }
            Long serviceOrdId=emergency.getLegacyServiceOrdId();
            // å¿…须先同步服务单
            if (emergency.getLegacyServiceOrdId() == null || emergency.getLegacyServiceOrdId() <= 0) {
            if (serviceOrdId == null || serviceOrdId <= 0) {
                log.warn("服务单未同步,无法同步调度单,任务ID: {}", taskId);
                return null;
            }
            // æ›´æ–°åŒæ­¥çŠ¶æ€ä¸ºåŒæ­¥ä¸­
            emergency.setDispatchSyncStatus(1);
            sysTaskEmergencyService.updateSysTaskEmergency(emergency);
            SysUser u=sysUserMapper.selectUserById(task.getCreatorId());
            Integer oaUserID= u.getOaUserId();
            
            // æž„建请求参数
            Map<String, String> params = buildDispatchOrderParams(task, emergency);
@@ -330,7 +361,13 @@
                emergency.setDispatchSyncTime(new Date());
                emergency.setDispatchSyncErrorMsg(null);
                sysTaskEmergencyService.updateSysTaskEmergency(emergency);
                List<SysTaskAttachment> taskAttachments= sysTaskService.getAttachmentsByTaskId(taskId);
                if (taskAttachments != null && !taskAttachments.isEmpty()) {
                    //同步附件
                   this.syncAttachmentToLegacy(taskAttachments,serviceOrdId,dispatchOrdId,oaUserID);
                }
                log.info("调度单同步成功,任务ID: {}, DispatchOrdID: {}", taskId, dispatchOrdId);
                return dispatchOrdId;
            } else {
@@ -395,6 +432,7 @@
                for (SysTaskEmergency emergency : pendingTasks) {
                    log.info("开始同步调度单,任务ID: {}", emergency.getTaskId());
                    Long dispatchOrdId = syncDispatchOrderToLegacy(emergency.getTaskId());
                    if (dispatchOrdId != null && dispatchOrdId > 0) {
                        pageSuccessCount++;
                    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.service.impl;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
@@ -12,7 +13,10 @@
import java.net.HttpURLConnection;
import java.net.URL;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.mapper.*;
import com.ruoyi.system.utils.TaskCodeGenerator;
import com.ruoyi.common.config.ImageUrlConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -35,20 +39,15 @@
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.mapper.SysTaskMapper;
import com.ruoyi.system.mapper.SysTaskVehicleMapper;
import com.ruoyi.system.mapper.SysTaskAttachmentMapper;
import com.ruoyi.system.mapper.SysTaskLogMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskWelfareMapper;
import com.ruoyi.system.mapper.SysTaskAssigneeMapper;
import com.ruoyi.system.mapper.VehicleInfoMapper;
import com.ruoyi.system.domain.VehicleInfo;
import com.ruoyi.system.service.ISysTaskService;
import com.ruoyi.system.service.ILegacySystemSyncService;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import com.ruoyi.system.event.TaskCreatedEvent;
import com.ruoyi.system.event.TaskAssignedEvent;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@@ -60,6 +59,8 @@
 */
@Service
public class SysTaskServiceImpl implements ISysTaskService {
    private static final Logger log = LoggerFactory.getLogger(SysTaskServiceImpl.class);
    
    @Autowired
    private SysTaskMapper sysTaskMapper;
@@ -90,6 +91,15 @@
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    @Autowired
    private ImageUrlConfig imageUrlConfig;
    @Autowired(required = false)
    private ITaskAttachmentSyncService taskAttachmentSyncService;
    @Autowired
    private SysUserMapper sysUserMapper;
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†
@@ -505,23 +515,23 @@
     */
    @Override
    @Transactional
    public int uploadAttachment(Long taskId, MultipartFile file, String category) {
    public Long uploadAttachment(Long taskId, MultipartFile file, String category) {
        try {
            // ä¸Šä¼ æ–‡ä»¶
            // ä¸Šä¼ æ–‡ä»¶ï¼Œè¿”回相对路径(如:/task/2025/01/15/xxx.jpg)
            String fileName = FileUploadUtils.upload("/task", file);
            String filePath = FileUploadUtils.getDefaultBaseDir() + fileName;
            
            SysTaskAttachment attachment = new SysTaskAttachment();
            attachment.setTaskId(taskId);
            attachment.setFileName(file.getOriginalFilename());
            attachment.setFilePath(filePath);
            // ä¿å­˜ç›¸å¯¹è·¯å¾„,不包含 baseDir
            attachment.setFilePath(fileName);
            attachment.setFileSize(file.getSize());
            attachment.setFileType(getFileType(file.getOriginalFilename()));
            attachment.setAttachmentCategory(category);
            attachment.setUploadTime(DateUtils.getNowDate());
            attachment.setUploadBy(SecurityUtils.getUsername());
            
            int result = sysTaskAttachmentMapper.insertSysTaskAttachment(attachment);
            Long result = sysTaskAttachmentMapper.insertSysTaskAttachment(attachment);
            
            // è®°å½•操作日志
            if (result > 0) {
@@ -529,6 +539,8 @@
                recordTaskLog(taskId, "UPDATE", "上传附件", null, 
                             "上传文件:" + file.getOriginalFilename() + "(分类:" + categoryDesc + ")", 
                             SecurityUtils.getUserId(), SecurityUtils.getUsername());
            }
            
            return result;
@@ -544,11 +556,11 @@
     * @param accessToken å¾®ä¿¡AccessToken
     * @param mediaId å¾®ä¿¡mediaId
     * @param category é™„件分类
     * @return ç»“æžœ
     * @return è¿”回附件ID
     */
    @Override
    @Transactional
    public int uploadAttachmentFromWechat(Long taskId, String accessToken, String mediaId, String category) {
    public Long uploadAttachmentFromWechat(Long taskId, String accessToken, String mediaId, String category) {
        try {
            // ä»Žå¾®ä¿¡æœåŠ¡å™¨ä¸‹è½½æ–‡ä»¶
            String wechatUrl = String.format(
@@ -582,18 +594,22 @@
                fos.write(fileBytes);
            }
            
            // ç”Ÿæˆç›¸å¯¹è·¯å¾„(不包含baseDir)
            String relativeFilePath = "/task/" + datePath + "/" + fileName;
            // ä¿å­˜é™„件记录
            SysTaskAttachment attachment = new SysTaskAttachment();
            attachment.setTaskId(taskId);
            attachment.setFileName(fileName);
            attachment.setFilePath(filePath);
            // ä¿å­˜ç›¸å¯¹è·¯å¾„
            attachment.setFilePath(relativeFilePath);
            attachment.setFileSize((long) fileBytes.length);
            attachment.setFileType("jpg");
            attachment.setAttachmentCategory(category);
            attachment.setUploadTime(DateUtils.getNowDate());
            attachment.setUploadBy(SecurityUtils.getUsername());
            
            int result = sysTaskAttachmentMapper.insertSysTaskAttachment(attachment);
            Long result = sysTaskAttachmentMapper.insertSysTaskAttachment(attachment);
            
            // è®°å½•操作日志
            if (result > 0) {
@@ -601,9 +617,11 @@
                recordTaskLog(taskId, "UPDATE", "上传附件", null, 
                             "通过微信上传文件:" + fileName + "(分类:" + categoryDesc + ")", 
                             SecurityUtils.getUserId(), SecurityUtils.getUsername());
            }
            
            return result;
            return attachment.getAttachmentId();
        } catch (Exception e) {
            throw new RuntimeException("从微信上传文件失败:" + e.getMessage());
        }
@@ -675,7 +693,17 @@
     */
    @Override
    public SysTaskAttachment getAttachmentById(Long attachmentId) {
        return sysTaskAttachmentMapper.selectSysTaskAttachmentByAttachmentId(attachmentId);
        SysTaskAttachment attachment = sysTaskAttachmentMapper.selectSysTaskAttachmentByAttachmentId(attachmentId);
        if (attachment != null) {
            // æ‹¼æŽ¥å®Œæ•´URL
            buildAttachmentUrl(attachment);
        }
        return attachment;
    }
    @Override
    public List<SysTaskAttachment> getAttachmentsByTaskId(Long taskId) {
        return sysTaskAttachmentMapper.selectSysTaskAttachmentByTaskId(taskId);
    }
    /**
@@ -876,7 +904,12 @@
            // æŸ¥è¯¢å…³è”车辆
            task.setAssignedVehicles(sysTaskVehicleMapper.selectSysTaskVehicleByTaskId(taskId));
            // æŸ¥è¯¢é™„ä»¶
            task.setAttachments(sysTaskAttachmentMapper.selectSysTaskAttachmentByTaskId(taskId));
            List<SysTaskAttachment> attachments = sysTaskAttachmentMapper.selectSysTaskAttachmentByTaskId(taskId);
            // ä¸ºæ¯ä¸ªé™„件拼接完整URL
            if (attachments != null && !attachments.isEmpty()) {
                attachments.forEach(this::buildAttachmentUrl);
            }
            task.setAttachments(attachments);
            // æŸ¥è¯¢æ“ä½œæ—¥å¿—
            task.setOperationLogs(sysTaskLogMapper.selectSysTaskLogByTaskId(taskId));
            // åŠ è½½æ€¥æ•‘è½¬è¿æ‰©å±•ä¿¡æ¯
@@ -1191,4 +1224,24 @@
            default: return "其他";
        }
    }
    /**
     * æž„建附件的完整URL
     *
     * @param attachment é™„件对象
     */
    private void buildAttachmentUrl(SysTaskAttachment attachment) {
        if (attachment != null && StringUtils.isNotEmpty(attachment.getFilePath())) {
            String imageUrl = imageUrlConfig.getImageUrl();
            if (StringUtils.isNotEmpty(imageUrl)) {
                // æ‹¼æŽ¥å®Œæ•´URL:域名 + ç›¸å¯¹è·¯å¾„
                attachment.setFileUrl(imageUrl + attachment.getFilePath());
            } else {
                // å¦‚果未配置域名,直接使用相对路径
                attachment.setFileUrl(attachment.getFilePath());
            }
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java
New file
@@ -0,0 +1,204 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.config.LegacySystemConfig;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.system.domain.ImageData;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.file.FileUploadResponse;
import com.ruoyi.system.file.IFileUploadService;
import com.ruoyi.system.imagedata.IImageDataService;
import com.ruoyi.system.mapper.SysTaskAttachmentMapper;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.File;
import java.util.Date;
import java.util.List;
/**
 * ä»»åŠ¡é™„ä»¶åŒæ­¥æœåŠ¡å®žçŽ°
 * å°†ä»»åŠ¡é™„ä»¶åŒæ­¥åˆ°ImageData表
 *
 * @author ruoyi
 */
@Service
@DataSource(DataSourceType.SQLSERVER)
public class TaskAttachmentSyncServiceImpl implements ITaskAttachmentSyncService {
    private static final Logger log = LoggerFactory.getLogger(TaskAttachmentSyncServiceImpl.class);
    @Autowired
    private IImageDataService imageDataService;
    @Autowired
    private LegacySystemConfig legacyConfig;
    @Autowired
    private IFileUploadService fileUploadService;
    @Autowired
    private RuoYiConfig ruoYiConfig;
    /**
     * åŒæ­¥å•个附件到ImageData
     */
    @Override
    public Long syncAttachmentToImageData(SysTaskAttachment attachment, Long serviceOrderId, Long dispatchOrdId,Integer oaUserId) {
        if (attachment == null) {
            log.error("附件对象为空,无法同步");
            return 0L;
        }
        // æ£€æŸ¥æ˜¯å¦å·²åŒæ­¥
        if (attachment.getSyncedToImageData() != null && attachment.getSyncedToImageData() == 1) {
            log.info("附件ID={} å·²åŒæ­¥ï¼Œè·³è¿‡", attachment.getAttachmentId());
            return 0L;
        }
        // æ£€æŸ¥å¿…要参数
        if (dispatchOrdId == null || serviceOrderId == null) {
            log.error("调度单ID和服务单ID都为空,无法同步附件ID={}", attachment.getAttachmentId());
            return 0L;
        }
        try {
            // èŽ·å–é™„ä»¶åˆ†ç±»å¯¹åº”çš„ImageType
            Integer imageType = getImageTypeByCategory(attachment.getAttachmentCategory());
            // èŽ·å–é™„ä»¶æœ¬åœ°æ–‡ä»¶è·¯å¾„
            String filePath = attachment.getFilePath();
            if (!StringUtils.hasText(filePath)) {
                log.error("附件ID={} çš„æ–‡ä»¶è·¯å¾„为空", attachment.getAttachmentId());
                return 0L;
            }
            // æž„建完整的本地文件路径
            String fullFilePath = ruoYiConfig.getProfile() + filePath;
            File file = new File(fullFilePath);
            if (!file.exists()) {
                log.error("附件ID={} å¯¹åº”的文件不存在: {}", attachment.getAttachmentId(), fullFilePath);
                return 0L;
            }
            // ç”Ÿæˆç›®æ ‡è·¯å¾„(使用dispatchOrdId作为目录)
            String targetPath = dispatchOrdId != null ? dispatchOrdId.toString() : "default";
            // ä½¿ç”¨æ–‡ä»¶ä¸Šä¼ æœåŠ¡ä¸Šä¼ åˆ°æ–‡ä»¶æœåŠ¡å™¨ï¼ˆåŒ…å«ç¼©ç•¥å›¾ç”Ÿæˆï¼‰
            log.info("开始上传附件ID={} åˆ°æ–‡ä»¶æœåŠ¡å™¨ï¼Œç›®æ ‡è·¯å¾„={}", attachment.getAttachmentId(), targetPath);
            FileUploadResponse uploadResponse = fileUploadService.uploadLocalFileWithThumbnail(file, targetPath);
            if (!uploadResponse.isSuccess()) {
                log.error("附件ID={} ä¸Šä¼ åˆ°æ–‡ä»¶æœåŠ¡å™¨å¤±è´¥: {}", attachment.getAttachmentId(), uploadResponse.getMessage());
                return 0L;
            }
            log.info("附件ID={} ä¸Šä¼ æˆåŠŸï¼ŒåŽŸå›¾è·¯å¾„={}, ç¼©ç•¥å›¾è·¯å¾„={}",
                attachment.getAttachmentId(), uploadResponse.getFilePath(), uploadResponse.getThumbnailPath());
            // åˆ›å»ºImageData对象
            ImageData imageData = new ImageData();
            imageData.setDOrdIDDt(dispatchOrdId);
            imageData.setSOrdIDDt(serviceOrderId);
            imageData.setImageType(imageType);
            imageData.setImageUrl(uploadResponse.getFilePath());
            imageData.setImageUrls(uploadResponse.getThumbnailPath()); // ç¼©ç•¥å›¾è·¯å¾„
            imageData.setUpImageTime(attachment.getUploadTime() != null ? attachment.getUploadTime() : new Date());
            imageData.setUpImageOAid(oaUserId);
            imageData.setImageDel(0);
            // æ’å…¥ImageData
            int result = imageDataService.insertImageData(imageData);
            if (result > 0) {
                log.info("成功同步附件ID={} åˆ°ImageData,ImageDataId={}",
                    attachment.getAttachmentId(), imageData.getId());
                return imageData.getId();
            } else {
                log.error("同步附件ID={} åˆ°ImageData失败", attachment.getAttachmentId());
                return 0L;
            }
        } catch (Exception e) {
            log.error("同步附件ID={} åˆ°ImageData时发生异常", attachment.getAttachmentId(), e);
            return 0L;
        }
    }
    /**
     * æ‰¹é‡åŒæ­¥ä»»åŠ¡çš„æ‰€æœ‰é™„ä»¶åˆ°ImageData
     */
    @Override
    public List<SysTaskAttachment> syncTaskAttachmentsToImageData(List<SysTaskAttachment> attachments, Long serviceOrderId, Long dispatchOrdId,Integer oaUserId) {
        if (attachments == null || attachments.isEmpty()) {
            log.info("没有附件,无需同步");
            return null;
        }
        int successCount = 0;
        for (SysTaskAttachment attachment : attachments) {
            Long imageDataId = syncAttachmentToImageData(attachment, serviceOrderId, dispatchOrdId,oaUserId);
            if (imageDataId>0)
            {
                attachment.setSyncedToImageData(1);
                attachment.setSyncTime(new Date());
                attachment.setImageDataId(imageDataId);
                successCount++;
            }
        }
        log.info(" å…±åŒæ­¥ {}/{} ä¸ªé™„件到ImageData",
             successCount, attachments.size());
        return null;
    }
    /**
     * æ ¹æ®é™„件分类获取对应的ImageType
     *
     * é™„件分类映射:
     * 1-知情同意书 -> 0
     * 2-病人资料 -> 1
     * 3-操作记录 -> 2
     * 4-出车前 -> 3
     * 5-出车后 -> 4
     * 6-系安全带 -> 5
     */
    private Integer getImageTypeByCategory(String category) {
        if (category == null) {
            return 1;
        }
        switch (category) {
            case "1": // çŸ¥æƒ…同意书
                return 1;
            case "2": // ç—…人资料
                return 2;
            case "3": // æ“ä½œè®°å½•
                return 3;
            case "4": // å‡ºè½¦å‰
                return 4;
            case "5": // å‡ºè½¦åŽ
                return 5;
            case "6": // ç³»å®‰å…¨å¸¦
                return 6;
            default:
                return 1;
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/task/ITaskAttachmentService.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.system.task;
import com.ruoyi.system.domain.SysTaskAttachment;
public interface ITaskAttachmentService {
    boolean isAttachmentSynced(Long taskId);
    boolean updateAttachment(SysTaskAttachment attachment);
}
ruoyi-system/src/main/java/com/ruoyi/system/task/TaskAttachmentServiceImpl.java
New file
@@ -0,0 +1,35 @@
package com.ruoyi.system.task;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.mapper.SysTaskAttachmentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TaskAttachmentServiceImpl implements ITaskAttachmentService{
    @Autowired
    private SysTaskAttachmentMapper taskAttachmentMapper;
    /**
     * æ£€æŸ¥é™„件是否已同步
     */
    @Override
    public boolean isAttachmentSynced(Long attachmentId) {
        if (attachmentId == null) {
            return false;
        }
        SysTaskAttachment attachment = taskAttachmentMapper.selectSysTaskAttachmentByAttachmentId(attachmentId);
        if (attachment == null) {
            return false;
        }
        return attachment.getSyncedToImageData() != null && attachment.getSyncedToImageData() == 1;
    }
    @Override
    public boolean updateAttachment(SysTaskAttachment attachment) {
        return taskAttachmentMapper.updateSysTaskAttachment(attachment) > 0;
    }
}
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml
@@ -14,10 +14,13 @@
        <result property="attachmentCategory" column="attachment_category" />
        <result property="uploadTime"       column="upload_time"       />
        <result property="uploadBy"         column="upload_by"         />
        <result property="syncedToImageData" column="synced_to_image_data" />
        <result property="syncTime"         column="sync_time"         />
        <result property="imageDataId"      column="image_data_id"     />
    </resultMap>
    <sql id="selectSysTaskAttachmentVo">
        select attachment_id, task_id, file_name, file_path, file_size, file_type, attachment_category, upload_time, upload_by
        select attachment_id, task_id, file_name, file_path, file_size, file_type, attachment_category, upload_time, upload_by, synced_to_image_data, sync_time, image_data_id
        from sys_task_attachment
    </sql>
@@ -55,6 +58,9 @@
            <if test="attachmentCategory != null and attachmentCategory != ''">attachment_category,</if>
            <if test="uploadTime != null">upload_time,</if>
            <if test="uploadBy != null and uploadBy != ''">upload_by,</if>
            <if test="syncedToImageData != null">synced_to_image_data,</if>
            <if test="syncTime != null">sync_time,</if>
            <if test="imageDataId != null">image_data_id,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="taskId != null">#{taskId},</if>
@@ -65,6 +71,9 @@
            <if test="attachmentCategory != null and attachmentCategory != ''">#{attachmentCategory},</if>
            <if test="uploadTime != null">#{uploadTime},</if>
            <if test="uploadBy != null and uploadBy != ''">#{uploadBy},</if>
            <if test="syncedToImageData != null">#{syncedToImageData},</if>
            <if test="syncTime != null">#{syncTime},</if>
            <if test="imageDataId != null">#{imageDataId},</if>
         </trim>
    </insert>
@@ -79,6 +88,9 @@
            <if test="attachmentCategory != null and attachmentCategory != ''">attachment_category = #{attachmentCategory},</if>
            <if test="uploadTime != null">upload_time = #{uploadTime},</if>
            <if test="uploadBy != null and uploadBy != ''">upload_by = #{uploadBy},</if>
            <if test="syncedToImageData != null">synced_to_image_data = #{syncedToImageData},</if>
            <if test="syncTime != null">sync_time = #{syncTime},</if>
            <if test="imageDataId != null">image_data_id = #{imageDataId},</if>
        </trim>
        where attachment_id = #{attachmentId}
    </update>
sql/update_sys_task_attachment_sync_fields.sql
New file
@@ -0,0 +1,16 @@
-- ä¸ºsys_task_attachment表添加同步相关字段
-- ç”¨äºŽå°†ä»»åŠ¡é™„ä»¶åŒæ­¥åˆ°ImageData表
-- æ·»åŠ æ˜¯å¦å·²åŒæ­¥åˆ°ImageData字段
ALTER TABLE sys_task_attachment ADD COLUMN synced_to_image_data TINYINT(1) DEFAULT 0 COMMENT '是否已同步到ImageData (0-未同步 1-已同步)';
-- æ·»åŠ åŒæ­¥æ—¶é—´å­—æ®µ
ALTER TABLE sys_task_attachment ADD COLUMN sync_time DATETIME COMMENT '同步时间';
-- æ·»åŠ å…³è”çš„ImageData ID字段
ALTER TABLE sys_task_attachment ADD COLUMN image_data_id BIGINT COMMENT '关联的ImageData表的ID';
-- æ·»åŠ ç´¢å¼•ä»¥æé«˜æŸ¥è¯¢æ€§èƒ½
CREATE INDEX idx_synced_to_image_data ON sys_task_attachment(synced_to_image_data);
CREATE INDEX idx_image_data_id ON sys_task_attachment(image_data_id);
CREATE INDEX idx_task_id_synced ON sys_task_attachment(task_id, synced_to_image_data);