From 99f528e235f11126fea44480c6e8888a9e463f2f Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期六, 08 十一月 2025 21:09:53 +0800
Subject: [PATCH] feat:任务附件上传和同步

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java            |   89 +++++--
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java             |   11 
 ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java                  |   28 ++
 ruoyi-ui/src/views/task/general/detail.vue                                                  |  170 +++++++++++--
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java    |    2 
 ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java         |   10 
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java |  107 ++++++++
 ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml                   |   26 ++
 sql/attachment_sync_job.sql                                                                 |  239 +++++++++++++++++++
 sql/sys_attachment_category_dict.sql                                                        |   14 +
 10 files changed, 639 insertions(+), 57 deletions(-)

diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
index 7175013..39189cc 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
+++ b/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) {
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
index 89bc162..bd8c852 100644
--- a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/LegacySystemSyncTask.java
+++ b/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);
+        }
+    }
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java
index 41f51ba..72a548c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskAttachmentMapper.java
+++ b/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();
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java
index fc2545e..8be9b6e 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITaskAttachmentSyncService.java
+++ b/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);
     
+    /**
+     * 鎵归噺鍚屾寰呭悓姝ョ殑浠诲姟闄勪欢鍒癐mageData
+     * 鏌ヨ鏉′欢锛�
+     * 1. 鎵�灞炰换鍔$殑璋冨害鍗曞凡鍚屾鎴愬姛
+     * 2. 闄勪欢鏈悓姝�
+     * 3. 鏈夎皟搴﹀崟ID鍜屾湇鍔″崟ID
+     * 
+     * @return 鎴愬姛鍚屾鐨勯檮浠舵暟閲�
+     */
+    int batchSyncPendingAttachments();
 
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
index 593f13c..508eb71 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
+++ b/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;
+       //灏唅nputstream鍐欏叆鏂囦欢
+        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 涓嬭浇鏂囦欢
      */
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java
index 4b4ea83..19ceac4 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TaskAttachmentSyncServiceImpl.java
+++ b/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;
     }
     
+    /**
+     * 鎵归噺鍚屾寰呭悓姝ョ殑浠诲姟闄勪欢鍒癐mageData
+     */
+    @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());
+            
+            // 鎸変换鍔D鍒嗙粍鍚屾
+            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={} 鍚屾鎴愬姛锛孖mageDataId={}", 
+                            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;
+    }
 
     
     /**
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml
index 05a426a..c017f7b 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysTaskAttachmentMapper.xml
+++ b/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>
diff --git a/ruoyi-ui/src/views/task/general/detail.vue b/ruoyi-ui/src/views/task/general/detail.vue
index 0450def..3685884 100644
--- a/ruoyi-ui/src/views/task/general/detail.vue
+++ b/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();
+    // 鍒濆鍖栦笂浼燯RL
+    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());
     }
   }
 };
diff --git a/sql/attachment_sync_job.sql b/sql/attachment_sync_job.sql
new file mode 100644
index 0000000..0c739b8
--- /dev/null
+++ b/sql/attachment_sync_job.sql
@@ -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. 闄勪欢鍒嗙被鏄犲皠瑙乀askAttachmentSyncServiceImpl绫�
+-- ==========================================
+
+-- 鎻掑叆瀹氭椂浠诲姟閰嶇疆
+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鍒嗛挓鎵ц涓�娆°�傚悓姝ユ潯浠讹細璋冨害鍗曞凡鍚屾銆侀檮浠舵湭鍚屾銆佹湁鏈嶅姟鍗曞拰璋冨害鍗旾D銆�'
+);
+
+-- ==========================================
+-- 楠岃瘉鏌ヨ
+-- ==========================================
+
+-- 1. 鏌ョ湅瀹氭椂浠诲姟鏄惁娣诲姞鎴愬姛
+SELECT 
+    job_id,
+    job_name,
+    job_group,
+    invoke_target,
+    cron_expression,
+    CASE status 
+        WHEN '0' THEN '姝e父'
+        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. 鎸変换鍔″垎缁勶紝鑾峰彇鏈嶅姟鍗旾D銆佽皟搴﹀崟ID銆佸垱寤轰汉OA鐢ㄦ埛ID
+3. 璇诲彇鏈湴闄勪欢鏂囦欢
+4. 涓婁紶鍒版枃浠舵湇鍔″櫒骞剁敓鎴愮缉鐣ュ浘
+5. 灏嗛檮浠朵俊鎭啓鍏ユ棫绯荤粺ImageData琛�
+   - DOrdIDDt: 璋冨害鍗旾D
+   - SOrdIDDt: 鏈嶅姟鍗旾D
+   - ImageType: 闄勪欢鍒嗙被瀵瑰簲鐨勭被鍨�
+   - ImageUrl: 鍘熷浘璺緞
+   - ImageUrls: 缂╃暐鍥捐矾寰�
+   - UpImageTime: 涓婁紶鏃堕棿
+   - UpImageOAid: 涓婁紶鑰匫A鐢ㄦ埛ID
+6. 鏇存柊闄勪欢鍚屾鐘舵��
+   - synced_to_image_data = 1
+   - sync_time = 褰撳墠鏃堕棿
+   - image_data_id = ImageData琛↖D
+*/
+
+-- ==========================================
+-- 鎵嬪姩瑙﹀彂娴嬭瘯
+-- ==========================================
+/*
+-- 鍦ㄥ畾鏃朵换鍔$鐞嗛〉闈㈡墜鍔ㄦ墽琛屼竴娆′换鍔★紝鎴栬�呭湪浠g爜涓皟鐢�:
+-- 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;
diff --git a/sql/sys_attachment_category_dict.sql b/sql/sys_attachment_category_dict.sql
new file mode 100644
index 0000000..1e14fc0
--- /dev/null
+++ b/sql/sys_attachment_category_dict.sql
@@ -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(), '绯诲畨鍏ㄥ甫闄勪欢');

--
Gitblit v1.9.1