wlzboy
2025-11-08 99f528e235f11126fea44480c6e8888a9e463f2f
feat:任务附件上传和同步
2个文件已添加
8个文件已修改
696 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/task/general/detail.vue 170 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/attachment_sync_job.sql 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/sys_attachment_category_dict.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
@@ -80,7 +80,7 @@
    @PostMapping("/upload/{taskId}")
    public AjaxResult upload(@PathVariable("taskId") Long taskId, 
                            @RequestParam("file") MultipartFile file,
                            @RequestParam(value = "category", required = false) String category) {
                            @RequestParam(value = "category", required = true) String category) {
        try {
            Long attachmentId= sysTaskService.uploadAttachment(taskId, file, category);
            if (attachmentId > 0) {
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
@@ -29,6 +29,9 @@
    
    @Autowired
    private ITaskStatusPushService taskStatusPushService;
    @Autowired
    private ITaskAttachmentSyncService taskAttachmentSyncService;
    
@@ -131,4 +134,29 @@
            log.error("任务状态推送异常", e);
        }
    }
    /**
     * 批量同步任务附件到旧系统ImageData表
     *
     * 使用示例:
     * 在系统管理 -> 定时任务中添加:
     * 任务名称: 任务附件同步
     * 任务组名: DEFAULT
     * 调用目标字符串: legacySystemSyncTask.syncPendingAttachments()
     * cron表达式: 0 0/5 * * * ? (每5分钟执行一次)
     *
     * 同步条件:
     * 1. 所属任务的调度单已同步成功 (dispatch_sync_status = 2)
     * 2. 附件未同步 (synced_to_image_data = 0 或 null)
     * 3. 有调度单ID和服务单ID
     */
    public void syncPendingAttachments() {
        log.info("开始执行任务附件同步定时任务");
        try {
            int successCount = taskAttachmentSyncService.batchSyncPendingAttachments();
            log.info("任务附件同步完成,成功同步: {} 个附件", successCount);
        } catch (Exception e) {
            log.error("任务附件同步异常", e);
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java
@@ -74,4 +74,15 @@
     * @return 结果
     */
    public int deleteSysTaskAttachmentByTaskId(Long taskId);
    /**
     * 查询待同步的任务附件列表
     * 查询条件:
     * 1. 所属任务的调度单已同步成功 (dispatch_sync_status = 2)
     * 2. 附件未同步 (synced_to_image_data = 0 或 null)
     * 3. 有调度单ID和服务单ID
     *
     * @return 待同步的附件列表
     */
    public List<SysTaskAttachment> selectPendingSyncAttachments();
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java
@@ -32,5 +32,15 @@
     */
    List<SysTaskAttachment> syncTaskAttachmentsToImageData(List<SysTaskAttachment> attachmentList, Long serviceOrderId, Long dispatchOrdId, Integer oaUserId);
    
    /**
     * 批量同步待同步的任务附件到ImageData
     * 查询条件:
     * 1. 所属任务的调度单已同步成功
     * 2. 附件未同步
     * 3. 有调度单ID和服务单ID
     *
     * @return 成功同步的附件数量
     */
    int batchSyncPendingAttachments();
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -1,15 +1,12 @@
package com.ruoyi.system.service.impl;
import java.io.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -518,7 +515,9 @@
    public Long uploadAttachment(Long taskId, MultipartFile file, String category) {
        try {
            // 上传文件,返回相对路径(如:/task/2025/01/15/xxx.jpg)
            String fileName = FileUploadUtils.upload("/task", file);
            String fileName = category+"_"+System.currentTimeMillis()+"_"+file.getOriginalFilename();
            fileName=saveLocalPath(fileName,file.getInputStream());
            
            SysTaskAttachment attachment = new SysTaskAttachment();
            attachment.setTaskId(taskId);
@@ -543,7 +542,7 @@
            }
            
            return result;
            return attachment.getAttachmentId();
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败:" + e.getMessage());
        }
@@ -577,26 +576,8 @@
            String fileName = "wx_" + mediaId.substring(0, Math.min(20, mediaId.length())) + "_" + System.currentTimeMillis() + ".jpg";
            
            // 保存到本地
            String baseDir = FileUploadUtils.getDefaultBaseDir();
            String datePath = DateUtils.datePath();
            String uploadDir = baseDir + "/task/" + datePath;
            // 创建目录
            File uploadPath = new File(uploadDir);
            if (!uploadPath.exists()) {
                uploadPath.mkdirs();
            }
            // 保存文件
            String filePath = uploadDir + "/" + fileName;
            File file = new File(filePath);
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(fileBytes);
            }
            // 生成相对路径(不包含baseDir)
            String relativeFilePath = "/task/" + datePath + "/" + fileName;
            String relativeFilePath = saveLocalPath(fileName, fileBytes);
            // 保存附件记录
            SysTaskAttachment attachment = new SysTaskAttachment();
            attachment.setTaskId(taskId);
@@ -626,7 +607,61 @@
            throw new RuntimeException("从微信上传文件失败:" + e.getMessage());
        }
    }
    private static String saveLocalPath(String fileName, byte[] fileBytes) throws IOException {
        String baseDir = FileUploadUtils.getDefaultBaseDir();
        String datePath = DateUtils.datePath();
        String uploadDir = baseDir + "/task/" + datePath;
        // 创建目录
        File uploadPath = new File(uploadDir);
        if (!uploadPath.exists()) {
            uploadPath.mkdirs();
        }
        // 保存文件
        String filePath = uploadDir + "/" + fileName;
        File file = new File(filePath);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(fileBytes);
        }
        // 生成相对路径(不包含baseDir)
        String relativeFilePath = "/task/" + datePath + "/" + fileName;
        return relativeFilePath;
    }
    private String saveLocalPath(String fileName,InputStream stream){
        String baseDir = FileUploadUtils.getDefaultBaseDir();
        String datePath = DateUtils.datePath();
        String uploadDir = baseDir + "/task/" + datePath;
        // 创建目录
        File uploadPath = new File(uploadDir);
        if (!uploadPath.exists()) {
            uploadPath.mkdirs();
        }
        // 保存文件
        String filePath = uploadDir + "/" + fileName;
       //将inputstream写入文件
        try (OutputStream os = new FileOutputStream(filePath)) {
            byte[] buffer = new byte[1024]; // 缓冲区,减少 IO 次数
            int bytesRead;
            // 循环读取输入流中的数据,写入输出流
            while ((bytesRead = stream.read(buffer)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.flush(); // 强制刷新缓冲区,确保数据写入文件
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 生成相对路径(不包含baseDir)
        String relativeFilePath = "/task/" + datePath + "/" + fileName;
        return relativeFilePath;
    }
    /**
     * 从 URL 下载文件
     */
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java
@@ -6,6 +6,7 @@
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.system.domain.ImageData;
import com.ruoyi.system.domain.SysTaskAttachment;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.file.FileUploadResponse;
import com.ruoyi.system.file.IFileUploadService;
import com.ruoyi.system.imagedata.IImageDataService;
@@ -29,12 +30,13 @@
 * @author ruoyi
 */
@Service
@DataSource(DataSourceType.SQLSERVER)
public class TaskAttachmentSyncServiceImpl implements ITaskAttachmentSyncService {
    
    private static final Logger log = LoggerFactory.getLogger(TaskAttachmentSyncServiceImpl.class);
    
    @Autowired
    private SysTaskAttachmentMapper taskAttachmentMapper;
    @Autowired
    private IImageDataService imageDataService;
    
@@ -166,6 +168,107 @@
        return null;
    }
    
    /**
     * 批量同步待同步的任务附件到ImageData
     */
    @Override
    public int batchSyncPendingAttachments() {
        log.info("开始执行批量附件同步任务");
        try {
            // 查询待同步的附件列表
            List<SysTaskAttachment> pendingAttachments = taskAttachmentMapper.selectPendingSyncAttachments();
            if (pendingAttachments == null || pendingAttachments.isEmpty()) {
                log.info("没有待同步的附件");
                return 0;
            }
            log.info("查询到 {} 个待同步的附件", pendingAttachments.size());
            // 按任务ID分组同步
            int successCount = 0;
            Long currentTaskId = null;
            Long serviceOrderId = null;
            Long dispatchOrdId = null;
            Integer oaUserId = null;
            for (SysTaskAttachment attachment : pendingAttachments) {
                try {
                    // 如果是新任务,需要获取任务信息
                    if (!attachment.getTaskId().equals(currentTaskId)) {
                        currentTaskId = attachment.getTaskId();
                        // 通过联表查询已经包含了任务信息,这里需要单独查询
                        SysTaskEmergency emergencyInfo = getEmergencyInfoByTaskId(currentTaskId);
                        if (emergencyInfo != null) {
                            serviceOrderId = emergencyInfo.getLegacyServiceOrdId();
                            dispatchOrdId = emergencyInfo.getLegacyDispatchOrdId();
                            // 获取任务创建人的OA用户ID
                            oaUserId = getCreatorOaUserId(currentTaskId);
                        } else {
                            log.warn("任务ID={} 的急救转运信息为空,跳过", currentTaskId);
                            continue;
                        }
                    }
                    // 同步单个附件
                    Long imageDataId = syncAttachmentToImageData(attachment, serviceOrderId, dispatchOrdId, oaUserId);
                    if (imageDataId != null && imageDataId > 0) {
                        // 更新附件同步状态
                        attachment.setSyncedToImageData(1);
                        attachment.setSyncTime(new Date());
                        attachment.setImageDataId(imageDataId);
                        taskAttachmentMapper.updateSysTaskAttachment(attachment);
                        successCount++;
                        log.info("附件ID={} 同步成功,ImageDataId={}",
                            attachment.getAttachmentId(), imageDataId);
                    }
                } catch (Exception e) {
                    log.error("同步附件ID={} 失败", attachment.getAttachmentId(), e);
                    // 继续处理下一个附件
                }
            }
            log.info("附件同步完成,成功同步 {}/{} 个附件", successCount, pendingAttachments.size());
            return successCount;
        } catch (Exception e) {
            log.error("批量附件同步异常", e);
            return 0;
        }
    }
    /**
     * 根据任务ID获取急救转运信息
     */
    @Autowired
    private com.ruoyi.system.mapper.SysTaskEmergencyMapper taskEmergencyMapper;
    private SysTaskEmergency getEmergencyInfoByTaskId(Long taskId) {
        return taskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
    }
    /**
     * 获取任务创建人的OA用户ID
     */
    @Autowired
    private com.ruoyi.system.mapper.SysTaskMapper taskMapper;
    @Autowired
    private com.ruoyi.system.mapper.SysUserMapper userMapper;
    private Integer getCreatorOaUserId(Long taskId) {
        com.ruoyi.system.domain.SysTask task = taskMapper.selectSysTaskByTaskId(taskId);
        if (task != null && task.getCreatorId() != null) {
            com.ruoyi.common.core.domain.entity.SysUser user = userMapper.selectUserById(task.getCreatorId());
            if (user != null) {
                return user.getOaUserId();
            }
        }
        return null;
    }
    
    /**
ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml
@@ -109,4 +109,30 @@
    <delete id="deleteSysTaskAttachmentByTaskId" parameterType="Long">
        delete from sys_task_attachment where task_id = #{taskId}
    </delete>
    <!-- 查询待同步的任务附件列表 -->
    <select id="selectPendingSyncAttachments" resultMap="SysTaskAttachmentResult">
        SELECT
            a.attachment_id,
            a.task_id,
            a.file_name,
            a.file_path,
            a.file_size,
            a.file_type,
            a.attachment_category,
            a.upload_time,
            a.upload_by,
            a.synced_to_image_data,
            a.sync_time,
            a.image_data_id
        FROM sys_task_attachment a
        INNER JOIN sys_task t ON a.task_id = t.task_id
        INNER JOIN sys_task_emergency e ON t.task_id = e.task_id
        WHERE t.task_type = 'EMERGENCY_TRANSFER'
            AND e.dispatch_sync_status = 2
            AND e.legacy_dispatch_ord_id IS NOT NULL
            AND e.legacy_service_ord_id IS NOT NULL
            AND (a.synced_to_image_data = 0 OR a.synced_to_image_data IS NULL)
        ORDER BY a.upload_time ASC
    </select>
</mapper>
ruoyi-ui/src/views/task/general/detail.vue
@@ -203,11 +203,38 @@
      </div>
      
      <el-table :data="taskDetail.attachments" v-loading="attachmentLoading">
        <el-table-column label="文件名" align="center" prop="fileName" />
        <el-table-column label="文件类型" align="center" prop="fileType" />
        <el-table-column label="文件大小" align="center" prop="fileSize">
        <el-table-column label="缩略图" align="center" width="120">
          <template slot-scope="scope">
            <span>{{ formatFileSize(scope.row.fileSize) }}</span>
            <el-image
              v-if="isImage(scope.row.fileType)"
              :src="scope.row.fileUrl"
              :preview-src-list="[scope.row.fileUrl]"
              fit="cover"
              style="width: 80px; height: 80px; border-radius: 4px; cursor: pointer;"
            >
              <div slot="error" class="image-slot">
                <i class="el-icon-picture-outline" style="font-size: 40px; color: #C0C4CC;"></i>
              </div>
            </el-image>
            <div v-else style="text-align: center;">
              <i class="el-icon-document" style="font-size: 40px; color: #909399;"></i>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="业务分类" align="center" prop="attachmentCategory" width="150">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_attachment_category" :value="scope.row.attachmentCategory"/>
          </template>
        </el-table-column>
        <el-table-column label="同步状态" align="center" width="120">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.syncedToImageData === 0" type="info" size="small">
              <i class="el-icon-warning"></i> 未同步
            </el-tag>
            <el-tag v-else-if="scope.row.syncedToImageData === 1" type="success" size="small">
              <i class="el-icon-success"></i> 已同步
            </el-tag>
            <span v-else style="color: #C0C4CC;">--</span>
          </template>
        </el-table-column>
        <el-table-column label="上传时间" align="center" prop="uploadTime" width="180">
@@ -388,21 +415,42 @@
    </el-dialog>
    <!-- 上传附件对话框 -->
    <el-dialog title="上传附件" :visible.sync="uploadOpen" width="500px" append-to-body>
      <el-upload
        class="upload-demo"
        drag
        :action="uploadUrl"
        :headers="uploadHeaders"
        :data="uploadData"
        :on-success="handleUploadSuccess"
        :on-error="handleUploadError"
        :before-upload="beforeUpload"
        multiple>
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">只能上传jpg/png/pdf/doc/docx文件,且不超过10MB</div>
      </el-upload>
    <el-dialog title="上传附件" :visible.sync="uploadOpen" width="500px" append-to-body @close="cancelUpload">
      <el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="100px">
        <el-form-item label="业务分类" prop="category">
          <el-select v-model="uploadForm.category" placeholder="请选择业务分类" clearable style="width: 100%;">
            <el-option
              v-for="dict in dict.type.sys_attachment_category"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="附件" prop="files">
          <el-upload
            ref="upload"
            class="upload-demo"
            :action="uploadUrl"
            :headers="uploadHeaders"
            :data="uploadData"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            :before-upload="beforeUpload"
            :file-list="fileList"
            :auto-upload="false"
            multiple
            drag>
            <i class="el-icon-upload"></i>
            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            <div class="el-upload__tip" slot="tip">只能上传jpg/png/pdf/doc/docx文件,且不超过100MB</div>
          </el-upload>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitUpload">确 定</el-button>
        <el-button @click="cancelUpload">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>
@@ -414,7 +462,7 @@
export default {
  name: "TaskDetail",
  dicts: ['sys_task_type', 'sys_task_status', 'sys_vehicle_type', 'sys_task_vehicle_status', 'sys_user_sex', 'hospital_department'],
  dicts: ['sys_task_type', 'sys_task_status', 'sys_vehicle_type', 'sys_task_vehicle_status', 'sys_user_sex', 'hospital_department', 'sys_attachment_category'],
  data() {
    return {
      // 任务详情
@@ -433,6 +481,12 @@
      vehicleAssignOpen: false,
      // 是否显示上传对话框
      uploadOpen: false,
      // 上传表单
      uploadForm: {
        category: null
      },
      // 文件列表
      fileList: [],
      // 编辑表单
      editForm: {},
      // 分配表单
@@ -449,7 +503,7 @@
      vehicleLoading: false,
      attachmentLoading: false,
      // 上传相关
      uploadUrl: process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId,
      uploadUrl: process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + (new URLSearchParams(window.location.search).get('taskId') || ''),
      uploadHeaders: {
        Authorization: "Bearer " + getToken()
      },
@@ -480,12 +534,19 @@
        vehicleIds: [
          { required: true, message: "车辆不能为空", trigger: "change" }
        ]
      },
      uploadRules: {
        category: [
          { required: true, message: "业务分类不能为空", trigger: "change" }
        ]
      }
    };
  },
  created() {
    this.getTaskDetail();
    this.getUserList();
    // 初始化上传URL
    this.uploadUrl = process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId;
  },
  methods: {
    /** 获取任务详情 */
@@ -554,6 +615,10 @@
    },
    /** 上传附件 */
    handleUpload() {
      this.uploadForm = {
        category: null
      };
      this.fileList = [];
      this.uploadOpen = true;
    },
    /** 取消车辆分配 */
@@ -648,22 +713,67 @@
    },
    /** 上传前检查 */
    beforeUpload(file) {
      // 检查是否选择了业务分类
      if (!this.uploadForm.category) {
        this.$message.error('请先选择业务分类!');
        return false;
      }
      // 更新uploadData,确保每次上传都带有category参数
      this.uploadData = {
        category: this.uploadForm.category
      };
      const isValidType = ['image/jpeg', 'image/png', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'].includes(file.type);
      const isLt10M = file.size / 1024 / 1024 < 10;
      const isLt10M = file.size / 1024 / 1024 < 100;
      if (!isValidType) {
        this.$message.error('只能上传 JPG/PNG/PDF/DOC/DOCX 格式的文件!');
        return false;
      }
      if (!isLt10M) {
        this.$message.error('上传文件大小不能超过 10MB!');
        this.$message.error('上传文件大小不能超过 100MB!');
        return false;
      }
      return isValidType && isLt10M;
      return true;
    },
    /** 提交上传 */
    submitUpload() {
      this.$refs["uploadForm"].validate(valid => {
        if (valid) {
          // 检查是否选择了文件
          const fileList = this.$refs.upload.uploadFiles;
          if (!fileList || fileList.length === 0) {
            this.$message.warning('请选择要上传的文件');
            return;
          }
          // 触发上传
          this.$refs.upload.submit();
        }
      });
    },
    /** 取消上传 */
    cancelUpload() {
      this.uploadOpen = false;
      this.uploadForm = {
        category: null
      };
      this.fileList = [];
      if (this.$refs.upload) {
        this.$refs.upload.clearFiles();
      }
    },
    /** 上传成功 */
    handleUploadSuccess(response, file, fileList) {
      this.$modal.msgSuccess("上传成功");
      this.uploadOpen = false;
      this.getTaskDetail();
      // 检查是否所有文件都上传完成
      const allDone = fileList.every(f => f.status === 'success' || f.status === 'fail');
      if (allDone) {
        this.$modal.msgSuccess("上传成功");
        this.cancelUpload();
        this.getTaskDetail();
      }
    },
    /** 上传失败 */
    handleUploadError(err, file, fileList) {
@@ -687,6 +797,12 @@
        return typeItem ? typeItem.label : vehicleType;
      }
      return vehicleType;
    },
    /** 判断是否为图片类型 */
    isImage(fileType) {
      if (!fileType) return false;
      const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
      return imageTypes.includes(fileType.toLowerCase());
    }
  }
};
sql/attachment_sync_job.sql
New file
@@ -0,0 +1,239 @@
-- ==========================================
-- 任务附件同步定时任务配置脚本
-- ==========================================
--
-- 功能说明:
-- 将已同步调度单的任务附件同步到旧系统ImageData表
--
-- 使用场景:
-- 1. 新系统上传了任务附件
-- 2. 任务的调度单已成功同步到旧系统
-- 3. 附件需要同步到旧系统以便旧系统查看
--
-- 同步条件:
-- 1. 任务类型为急救转运 (task_type = 'EMERGENCY_TRANSFER')
-- 2. 调度单已同步成功 (dispatch_sync_status = 2)
-- 3. 有调度单ID和服务单ID (legacy_dispatch_ord_id IS NOT NULL AND legacy_service_ord_id IS NOT NULL)
-- 4. 附件未同步 (synced_to_image_data = 0 OR synced_to_image_data IS NULL)
--
-- 执行频率建议:
-- - 生产环境: 每5分钟执行一次 (0 0/5 * * * ?)
-- - 测试环境: 每2分钟执行一次 (0 0/2 * * * ?)
--
-- 注意事项:
-- 1. 依赖调度单同步完成
-- 2. 会上传文件并生成缩略图
-- 3. 附件分类映射见TaskAttachmentSyncServiceImpl类
-- ==========================================
-- 插入定时任务配置
INSERT INTO `sys_job` (
    `job_name`,
    `job_group`,
    `invoke_target`,
    `cron_expression`,
    `misfire_policy`,
    `concurrent`,
    `status`,
    `create_by`,
    `create_time`,
    `update_by`,
    `update_time`,
    `remark`
) VALUES (
    '任务附件同步',
    'DEFAULT',
    'legacySystemSyncTask.syncPendingAttachments()',
    '0 0/5 * * * ?',
    '2',
    '1',
    '0',
    'admin',
    NOW(),
    'admin',
    NOW(),
    '将已同步调度单的任务附件同步到旧系统ImageData表,每5分钟执行一次。同步条件:调度单已同步、附件未同步、有服务单和调度单ID。'
);
-- ==========================================
-- 验证查询
-- ==========================================
-- 1. 查看定时任务是否添加成功
SELECT
    job_id,
    job_name,
    job_group,
    invoke_target,
    cron_expression,
    CASE status
        WHEN '0' THEN '正常'
        WHEN '1' THEN '暂停'
    END AS job_status,
    remark
FROM sys_job
WHERE job_name = '任务附件同步';
-- 2. 查看需要同步的附件数量
SELECT
    COUNT(*) AS total_attachments,
    SUM(CASE WHEN a.attachment_category = '1' THEN 1 ELSE 0 END) AS consent_form,
    SUM(CASE WHEN a.attachment_category = '2' THEN 1 ELSE 0 END) AS patient_data,
    SUM(CASE WHEN a.attachment_category = '3' THEN 1 ELSE 0 END) AS operation_record,
    SUM(CASE WHEN a.attachment_category = '4' THEN 1 ELSE 0 END) AS before_departure,
    SUM(CASE WHEN a.attachment_category = '5' THEN 1 ELSE 0 END) AS after_departure,
    SUM(CASE WHEN a.attachment_category = '6' THEN 1 ELSE 0 END) AS seat_belt
FROM sys_task_attachment a
INNER JOIN sys_task t ON a.task_id = t.task_id
INNER JOIN sys_task_emergency e ON t.task_id = e.task_id
WHERE t.task_type = 'EMERGENCY_TRANSFER'
  AND e.dispatch_sync_status = 2
  AND e.legacy_dispatch_ord_id IS NOT NULL
  AND e.legacy_service_ord_id IS NOT NULL
  AND (a.synced_to_image_data = 0 OR a.synced_to_image_data IS NULL);
-- 3. 查看待同步附件详情(最近10条)
SELECT
    a.attachment_id,
    t.task_code,
    a.file_name,
    CASE a.attachment_category
        WHEN '1' THEN '知情同意书'
        WHEN '2' THEN '病人资料'
        WHEN '3' THEN '操作记录'
        WHEN '4' THEN '出车前'
        WHEN '5' THEN '出车后'
        WHEN '6' THEN '系安全带'
        ELSE '未分类'
    END AS category_name,
    e.legacy_service_ord_id,
    e.legacy_dispatch_ord_id,
    a.upload_time,
    a.synced_to_image_data,
    a.sync_time
FROM sys_task_attachment a
INNER JOIN sys_task t ON a.task_id = t.task_id
INNER JOIN sys_task_emergency e ON t.task_id = e.task_id
WHERE t.task_type = 'EMERGENCY_TRANSFER'
  AND e.dispatch_sync_status = 2
  AND e.legacy_dispatch_ord_id IS NOT NULL
  AND e.legacy_service_ord_id IS NOT NULL
  AND (a.synced_to_image_data = 0 OR a.synced_to_image_data IS NULL)
ORDER BY a.upload_time DESC
LIMIT 10;
-- 4. 查看已同步附件统计
SELECT
    COUNT(*) AS synced_attachments,
    MIN(a.sync_time) AS first_sync_time,
    MAX(a.sync_time) AS last_sync_time
FROM sys_task_attachment a
WHERE a.synced_to_image_data = 1
  AND a.sync_time IS NOT NULL;
-- 5. 按任务统计待同步附件
SELECT
    t.task_id,
    t.task_code,
    e.legacy_service_ord_id,
    e.legacy_dispatch_ord_id,
    COUNT(a.attachment_id) AS attachment_count,
    GROUP_CONCAT(
        CASE a.attachment_category
            WHEN '1' THEN '知情同意书'
            WHEN '2' THEN '病人资料'
            WHEN '3' THEN '操作记录'
            WHEN '4' THEN '出车前'
            WHEN '5' THEN '出车后'
            WHEN '6' THEN '系安全带'
        END
        SEPARATOR ', '
    ) AS categories
FROM sys_task t
INNER JOIN sys_task_emergency e ON t.task_id = e.task_id
INNER JOIN sys_task_attachment a ON t.task_id = a.task_id
WHERE t.task_type = 'EMERGENCY_TRANSFER'
  AND e.dispatch_sync_status = 2
  AND e.legacy_dispatch_ord_id IS NOT NULL
  AND e.legacy_service_ord_id IS NOT NULL
  AND (a.synced_to_image_data = 0 OR a.synced_to_image_data IS NULL)
GROUP BY t.task_id, t.task_code, e.legacy_service_ord_id, e.legacy_dispatch_ord_id
ORDER BY attachment_count DESC;
-- ==========================================
-- 附件分类映射规则参考
-- ==========================================
/*
附件分类 -> ImageData类型:
1-知情同意书 -> ImageType: 1
2-病人资料 -> ImageType: 2
3-操作记录 -> ImageType: 3
4-出车前 -> ImageType: 4
5-出车后 -> ImageType: 5
6-系安全带 -> ImageType: 6
详细说明见: com.ruoyi.system.service.impl.TaskAttachmentSyncServiceImpl.getImageTypeByCategory()
*/
-- ==========================================
-- 同步流程说明
-- ==========================================
/*
1. 查询待同步附件列表(符合条件的附件)
2. 按任务分组,获取服务单ID、调度单ID、创建人OA用户ID
3. 读取本地附件文件
4. 上传到文件服务器并生成缩略图
5. 将附件信息写入旧系统ImageData表
   - DOrdIDDt: 调度单ID
   - SOrdIDDt: 服务单ID
   - ImageType: 附件分类对应的类型
   - ImageUrl: 原图路径
   - ImageUrls: 缩略图路径
   - UpImageTime: 上传时间
   - UpImageOAid: 上传者OA用户ID
6. 更新附件同步状态
   - synced_to_image_data = 1
   - sync_time = 当前时间
   - image_data_id = ImageData表ID
*/
-- ==========================================
-- 手动触发测试
-- ==========================================
/*
-- 在定时任务管理页面手动执行一次任务,或者在代码中调用:
-- legacySystemSyncTask.syncPendingAttachments()
-- 查看同步日志(需要查看应用日志文件)
*/
-- ==========================================
-- 故障排查SQL
-- ==========================================
-- 查看同步失败的附件(如果有错误记录)
SELECT
    a.attachment_id,
    t.task_code,
    a.file_name,
    a.file_path,
    a.upload_time,
    a.synced_to_image_data,
    a.sync_time
FROM sys_task_attachment a
INNER JOIN sys_task t ON a.task_id = t.task_id
WHERE a.synced_to_image_data = 0
  AND a.upload_time < DATE_SUB(NOW(), INTERVAL 1 HOUR)
ORDER BY a.upload_time DESC;
-- 查看某个任务的所有附件同步状态
-- SELECT
--     a.attachment_id,
--     a.file_name,
--     a.attachment_category,
--     a.synced_to_image_data,
--     a.sync_time,
--     a.image_data_id
-- FROM sys_task_attachment a
-- WHERE a.task_id = ? -- 替换为具体的任务ID
-- ORDER BY a.upload_time DESC;
sql/sys_attachment_category_dict.sql
New file
@@ -0,0 +1,14 @@
-- 任务附件分类字典数据
-- 插入字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, update_by, update_time, remark)
VALUES ('任务附件分类', 'sys_attachment_category', '0', 'admin', sysdate(), 'admin', sysdate(), '任务附件的业务分类');
-- 插入字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, update_by, update_time, remark)
VALUES
(1, '知情同意书', '1', 'sys_attachment_category', '', 'primary', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '知情同意书附件'),
(2, '病人资料', '2', 'sys_attachment_category', '', 'success', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '病人资料附件'),
(3, '操作记录', '3', 'sys_attachment_category', '', 'info', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '操作记录附件'),
(4, '出车前', '4', 'sys_attachment_category', '', 'warning', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '出车前附件'),
(5, '出车后', '5', 'sys_attachment_category', '', 'danger', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '出车后附件'),
(6, '系安全带', '6', 'sys_attachment_category', '', 'default', 'N', '0', 'admin', sysdate(), 'admin', sysdate(), '系安全带附件');