Переглянути джерело

知识库,导入和文件导入功能调整

徐滕 6 днів тому
батько
коміт
335b3ae721

+ 9 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseShareInfoDao.java

@@ -75,4 +75,13 @@ public interface WorkKnowledgeBaseShareInfoDao extends CrudDao<WorkKnowledgeBase
      * @return 文件信息(已锁定)
      */
     WorkKnowledgeBaseShareInfo getForUpdate(@org.apache.ibatis.annotations.Param("id") String id);
+
+    /**
+     * 根据文件名称和根节点ID查询匹配的记录(用于文件批量导入时按名称定位记录)
+     * @param name 文件名称(精确匹配)
+     * @param rootId 根节点ID
+     * @return 匹配的记录列表
+     */
+    List<WorkKnowledgeBaseShareInfo> findByNameAndRootId(@org.apache.ibatis.annotations.Param("name") String name,
+                                                         @org.apache.ibatis.annotations.Param("rootId") String rootId);
 }

+ 7 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseTreeInfoDao.java

@@ -41,6 +41,13 @@ public interface WorkKnowledgeBaseTreeInfoDao extends CrudDao<WorkKnowledgeBaseT
     String findIdByTreeName(@Param("treeName") String treeName);
 
     /**
+     * 根据节点名称和根节点ID查找所有匹配的节点id(限定在某个根节点范围内匹配)
+     * 匹配逻辑:节点名称相同 且 (节点本身就是rootId 或 节点的root_id等于rootId)
+     * 返回所有匹配的节点id列表,用于判断是否存在同名重复节点
+     */
+    List<String> findIdsByTreeNameAndRootId(@Param("treeName") String treeName, @Param("rootId") String rootId);
+
+    /**
      * 查询所有根节点(parent_id='0'的节点)
      */
     List<WorkKnowledgeBaseTreeInfo> findRootNodes();

+ 359 - 44
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java

@@ -1,7 +1,10 @@
 package com.jeeplus.modules.WorkKnowledgeBase.service;
 
+import com.jeeplus.common.config.Global;
+import com.jeeplus.common.oss.OSSClientUtil;
 import com.jeeplus.common.persistence.Page;
 import com.jeeplus.common.service.CrudService;
+import com.jeeplus.common.utils.DateUtils;
 import com.jeeplus.common.utils.IdGen;
 import com.jeeplus.common.utils.StringUtils;
 import com.jeeplus.modules.WorkKnowledgeBase.dao.*;
@@ -16,7 +19,14 @@ import com.jeeplus.modules.workprojectnotify.util.UtilNotify;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
@@ -35,6 +45,8 @@ import java.util.Set;
 @Transactional(readOnly = true)
 public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBaseShareInfoDao, WorkKnowledgeBaseShareInfo> {
 
+    private static final Logger logger = LoggerFactory.getLogger(WorkKnowledgeBaseShareService.class);
+
     /** 附件标识(attachmentFlag),进行隔离不同业务的附件 */
     private static final String ATTACHMENT_FLAG = "199";
 
@@ -414,6 +426,16 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         }
         
         boolean hasValidAttachment = hasValidAttachment(attachments);
+        // 调试日志:输出附件判定结果
+        logger.info("saveShare附件判定 - isNew:" + isNew + ", attachments:" + (attachments != null ? attachments.size() : "null")
+                + ", hasValidAttachment:" + hasValidAttachment
+                + ", treeNodeId:" + shareInfo.getTreeNodeId());
+        if (attachments != null) {
+            for (int idx = 0; idx < attachments.size(); idx++) {
+                Workattachment att = attachments.get(idx);
+                logger.info("  attachment[" + idx + "] id='" + att.getId() + "', url='" + att.getUrl() + "', delFlag='" + att.getDelFlag() + "'");
+            }
+        }
         // 编辑时,若前端未提交附件列表(用户未更换文件),回查DB已有附件进行判定
         if (!isNew && (attachments == null || attachments.isEmpty()) && StringUtils.isNotBlank(shareInfo.getId())) {
             List<Workattachment> existingAttachments = findAttachmentsByFileId(shareInfo.getId());
@@ -445,10 +467,20 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
                     shareInfo.setAuditStartDate(new Date());
                 }
             } else {
-                // 编辑:仅当状态为草稿(0)或审核未通过(4)时允许重新判定
+                // 编辑:仅当状态为草稿(0)、审核未通过(4)或未设置(null)时允许重新判定
                 String currentStatus = shareInfo.getAuditStatus();
-                if (currentStatus != null && (currentStatus.equals(AUDIT_STATUS_DRAFT) || currentStatus.equals(AUDIT_STATUS_REJECTED))) {
+                // 表单未提交auditStatus时,从数据库查询当前真实状态
+                if (StringUtils.isBlank(currentStatus) && StringUtils.isNotBlank(shareInfo.getId())) {
+                    WorkKnowledgeBaseShareInfo dbRecord = dao.get(shareInfo.getId());
+                    if (dbRecord != null) {
+                        currentStatus = dbRecord.getAuditStatus();
+                    }
+                }
+                logger.info("编辑模式 - currentStatus:'" + currentStatus + "', isQaCategory:" + isQaCategory + ", isInternalDocRootNode:" + isInternalDocRootNode);
+                // currentStatus为null时视为未初始化,允许重新判定;草稿(0)和审核未通过(4)也允许重新判定
+                if (currentStatus == null || currentStatus.equals(AUDIT_STATUS_DRAFT) || currentStatus.equals(AUDIT_STATUS_REJECTED)) {
                     shareInfo.setAuditStatus(hasValidAttachment ? AUDIT_STATUS_PENDING : AUDIT_STATUS_DRAFT);
+                    logger.info("状态重新判定 - 新auditStatus:'" + shareInfo.getAuditStatus() + "'");
                     shareInfo.setAuditPassCount(0);
                     shareInfo.setAuditRejectCount(0);
                     // 重新提交时更新提交审核人为当前登录人,并刷新本轮审核起始时间(历史审核记录依靠该时间隔离)
@@ -696,16 +728,25 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
 
     /**
      * 判断附件列表中是否存在有效(未删除的)附件
+     * 包括两种情况:
+     * 1. 已有附件:有id且delFlag为正常(0)
+     * 2. 新上传附件:id为空/null但url不为空(尚未持久化,delFlag可能未设置)
      */
     private boolean hasValidAttachment(List<Workattachment> attachments) {
         if (attachments == null || attachments.isEmpty()) {
             return false;
         }
         for (Workattachment attachment : attachments) {
-            if (attachment.getId() != null
+            // 已有附件:有id且未删除
+            if (attachment.getId() != null && !attachment.getId().isEmpty()
                     && Workattachment.DEL_FLAG_NORMAL.equals(attachment.getDelFlag())) {
                 return true;
             }
+            // 新上传附件:id为空但url有值(尚未持久化)
+            if ((attachment.getId() == null || attachment.getId().isEmpty())
+                    && StringUtils.isNotBlank(attachment.getUrl())) {
+                return true;
+            }
         }
         return false;
     }
@@ -1844,76 +1885,126 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     }
 
     /**
-     * 导入知识库数据
-     * @param dataList Excel解析后的数据列表
+     * 导入知识库数据(动态表头匹配)
+     * @param headers Excel表头列名列表
+     * @param dataRows Excel数据行(每行为单元格值列表)
      * @param rootId 根节点id
      * @return 导入结果描述
      */
     @Transactional(readOnly = false)
-    public String importData(List<WorkKnowledgeBaseImportInfo> dataList, String rootId) {
-        if (dataList == null || dataList.isEmpty()) {
+    public String importData(List<String> headers, List<List<String>> dataRows, String rootId) {
+        if (dataRows == null || dataRows.isEmpty()) {
             return "导入数据为空!";
         }
 
-        // 查询该根节点下的动态字段配置,用于将导入字段映射为动态字段id
+        // 1. 从表头中定位“所属节点名称”和“标准名称”列的索引
+        int nodeNameColIdx = -1;
+        int nameColIdx = -1;
+        for (int i = 0; i < headers.size(); i++) {
+            String h = headers.get(i).trim();
+            if ("所属节点名称".equals(h)) {
+                nodeNameColIdx = i;
+            } else if ("标准名称".equals(h)) {
+                nameColIdx = i;
+            }
+        }
+        if (nameColIdx == -1) {
+            return "导入失败:表头中未找到“标准名称”列!";
+        }
+
+        // 2. 查询该根节点下的动态字段配置,构建 fieldLabel -> dynamicFieldId 映射
         List<WorkKnowledgeBaseDynamicInfo> dynamicFields = dynamicInfoDao.findByRootId(rootId);
-        // 构建 fieldKey -> fieldId 的映射
-        Map<String, String> fieldKeyToIdMap = new HashMap<>();
-        for (WorkKnowledgeBaseDynamicInfo field : dynamicFields) {
-            fieldKeyToIdMap.put(field.getFieldKey(), field.getId());
+        Map<String, String> labelToFieldIdMap = new HashMap<>();
+        // 记录每个动态字段在表头中的列索引(-1表示该列不在表头中)
+        Map<String, Integer> fieldIdToColIdxMap = new HashMap<>();
+        if (dynamicFields != null) {
+            for (WorkKnowledgeBaseDynamicInfo field : dynamicFields) {
+                labelToFieldIdMap.put(field.getFieldLabel(), field.getId());
+                fieldIdToColIdxMap.put(field.getId(), -1);
+            }
+        }
+        // 遍历表头,将列名与动态字段的fieldLabel进行精确匹配
+        for (int i = 0; i < headers.size(); i++) {
+            String h = headers.get(i).trim();
+            if ("所属节点名称".equals(h) || "标准名称".equals(h)) {
+                continue; // 跳过固定列
+            }
+            String fieldId = labelToFieldIdMap.get(h);
+            if (fieldId != null) {
+                fieldIdToColIdxMap.put(fieldId, i);
+            }
         }
 
         int successCount = 0;
         int failCount = 0;
         StringBuilder failureMsg = new StringBuilder();
 
-        for (int i = 0; i < dataList.size(); i++) {
-            WorkKnowledgeBaseImportInfo importInfo = dataList.get(i);
+        for (int i = 0; i < dataRows.size(); i++) {
+            List<String> rowData = dataRows.get(i);
             try {
-                // 1. 根据体系节点名称查找对应的树节点id
-                String systemNodeName = importInfo.getSystemNodeName();
-                if (StringUtils.isBlank(systemNodeName)) {
-                    failCount++;
-                    failureMsg.append("<br/>第").append(i + 2).append("行:体系节点名称为空,跳过;");
-                    continue;
+                // 3. 获取所属节点名称
+                String nodeName = "";
+                if (nodeNameColIdx >= 0 && nodeNameColIdx < rowData.size()) {
+                    nodeName = rowData.get(nodeNameColIdx).trim();
                 }
-                String treeNodeId = treeInfoDao.findIdByTreeName(systemNodeName.trim());
-                if (StringUtils.isBlank(treeNodeId)) {
-                    failCount++;
-                    failureMsg.append("<br/>第").append(i + 2).append("行:未找到体系节点[").append(systemNodeName).append("],跳过;");
-                    continue;
+
+                // 4. 查找对应的树节点id
+                String treeNodeId;
+                if (nodeName.isEmpty()) {
+                    // 所属节点为空,放入根节点下
+                    treeNodeId = rootId;
+                } else {
+                    List<String> matchedIds = treeInfoDao.findIdsByTreeNameAndRootId(nodeName, rootId);
+                    if (matchedIds == null || matchedIds.isEmpty()) {
+                        failCount++;
+                        failureMsg.append("<br/>第").append(i + 2).append("行:未找到节点[").append(nodeName).append("],跳过;");
+                        continue;
+                    } else if (matchedIds.size() > 1) {
+                        failCount++;
+                        failureMsg.append("<br/>第").append(i + 2).append("行:节点[").append(nodeName).append("]存在多个同名节点,无法确定归属,请修改节点名称使其唯一,跳过;");
+                        continue;
+                    }
+                    treeNodeId = matchedIds.get(0);
                 }
 
-                // 2. 标准名称为必填项
-                String standardName = importInfo.getStandardName();
-                if (StringUtils.isBlank(standardName)) {
+                // 5. 标准名称为必填项
+                String name = "";
+                if (nameColIdx < rowData.size()) {
+                    name = rowData.get(nameColIdx).trim();
+                }
+                if (StringUtils.isBlank(name)) {
                     failCount++;
                     failureMsg.append("<br/>第").append(i + 2).append("行:标准名称为空,跳过;");
                     continue;
                 }
 
-                // 3. 新增 work_knowledge_base_share_info 记录
+                // 6. 新增 work_knowledge_base_share_info 记录
                 WorkKnowledgeBaseShareInfo shareInfo = new WorkKnowledgeBaseShareInfo();
                 shareInfo.setIsNewRecord(true);
                 shareInfo.setId(IdGen.uuid());
                 shareInfo.setTreeNodeId(treeNodeId);
-                shareInfo.setName(standardName.trim());
+                shareInfo.setName(name);
                 shareInfo.preInsert();
                 dao.insert(shareInfo);
 
-                // 4. 将其余字段作为动态字段值插入
-                insertDynamicValue(shareInfo.getId(), "SerialNumber", importInfo.getStandardSystemCode(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "StandardNum", importInfo.getStandardNoOrDocumentNo(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "ForwardingNum", importInfo.getTransmitDocumentNo(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "IssuingDep", importInfo.getPublishDepartment(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "EffectiveDate", importInfo.getEffectiveDate() != null ? new java.text.SimpleDateFormat("yyyy-MM-dd").format(importInfo.getEffectiveDate()) : "", fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "StandardLevel", importInfo.getStandardLevel(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "VoltageLevel", importInfo.getVoltageLevel(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "ProjectType", importInfo.getProjectType(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "PreparationStatus", importInfo.getCompileStatus(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "SupersededStandardNum", importInfo.getReplacedStandardNoAndName(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "StandardRelationship", importInfo.getStandardRelation(), fieldKeyToIdMap);
-                insertDynamicValue(shareInfo.getId(), "remarks", importInfo.getRemark(), fieldKeyToIdMap);
+                // 7. 将动态列的值插入
+                for (WorkKnowledgeBaseDynamicInfo field : dynamicFields) {
+                    Integer colIdx = fieldIdToColIdxMap.get(field.getId());
+                    if (colIdx == null || colIdx == -1) continue;
+                    String value = "";
+                    if (colIdx < rowData.size()) {
+                        value = rowData.get(colIdx).trim();
+                    }
+                    if (StringUtils.isNotBlank(value)) {
+                        WorkKnowledgeBaseDynamicValueInfo valueInfo = new WorkKnowledgeBaseDynamicValueInfo();
+                        valueInfo.setId(IdGen.uuid());
+                        valueInfo.setFileId(shareInfo.getId());
+                        valueInfo.setDynamicFieldId(field.getId());
+                        valueInfo.setFieldValue(value);
+                        valueInfo.preInsert();
+                        dynamicValueInfoDao.insert(valueInfo);
+                    }
+                }
 
                 successCount++;
             } catch (Exception e) {
@@ -1930,6 +2021,230 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     }
 
     /**
+     * 批量导入文件附件(通过URL方式)
+     * 根据Excel中的“标准名称”匹配已有记录,从“文件完整路径”列的外部URL下载文件,
+     * 上传到当前系统OSS后创建附件记录,
+     * 同时更新记录的审核状态为未审核(1)、内容属性为原创(0)
+     * 该方法独立于importData,不影响现有导入逻辑
+     *
+     * @param headers  Excel表头列名列表
+     * @param dataRows Excel数据行(每行为单元格值列表)
+     * @param rootId   根节点id
+     * @return 导入结果描述
+     */
+    @Transactional(readOnly = false)
+    public String importFilesByUrl(List<String> headers, List<List<String>> dataRows, String rootId) {
+        if (dataRows == null || dataRows.isEmpty()) {
+            return "导入数据为空!";
+        }
+
+        // 1. 从表头中定位“标准名称”和“文件完整路径”列的索引
+        int nameColIdx = -1;
+        int filePathColIdx = -1;
+        for (int i = 0; i < headers.size(); i++) {
+            String h = headers.get(i).trim();
+            if ("标准名称".equals(h)) {
+                nameColIdx = i;
+            } else if ("文件完整路径".equals(h)) {
+                filePathColIdx = i;
+            }
+        }
+        if (nameColIdx == -1) {
+            return "导入失败:表头中未找到“标准名称”列!";
+        }
+        if (filePathColIdx == -1) {
+            return "导入失败:表头中未找到“文件完整路径”列!";
+        }
+
+        int successCount = 0;
+        int failCount = 0;
+        StringBuilder failureMsg = new StringBuilder();
+
+        for (int i = 0; i < dataRows.size(); i++) {
+            List<String> rowData = dataRows.get(i);
+            try {
+                // 2. 获取标准名称
+                String name = "";
+                if (nameColIdx < rowData.size()) {
+                    name = rowData.get(nameColIdx).trim();
+                }
+                if (StringUtils.isBlank(name)) {
+                    failCount++;
+                    failureMsg.append("<br/>第").append(i + 2).append("行:标准名称为空,跳过;");
+                    continue;
+                }
+
+                // 3. 获取文件完整路径(外部系统URL)
+                String fileUrl = "";
+                if (filePathColIdx < rowData.size()) {
+                    fileUrl = rowData.get(filePathColIdx).trim();
+                }
+                if (StringUtils.isBlank(fileUrl)) {
+                    failCount++;
+                    failureMsg.append("<br/>第").append(i + 2).append("行:文件完整路径为空,跳过;");
+                    continue;
+                }
+
+                // 4. 根据名称和rootId查找匹配的记录
+                List<WorkKnowledgeBaseShareInfo> matchedRecords = dao.findByNameAndRootId(name, rootId);
+                if (matchedRecords == null || matchedRecords.isEmpty()) {
+                    failCount++;
+                    failureMsg.append("<br/>第").append(i + 2).append("行:未找到名称为[").append(name).append("]的记录,跳过;");
+                    continue;
+                }
+                if (matchedRecords.size() > 1) {
+                    failCount++;
+                    failureMsg.append("<br/>第").append(i + 2).append("行:名称[").append(name).append("]存在多条记录,无法确定唯一目标,跳过;");
+                    continue;
+                }
+
+                WorkKnowledgeBaseShareInfo record = matchedRecords.get(0);
+
+                // 5. 从路径中提取原始文件名和文件类型(兼容本地路径和URL)
+                String originalFileName = fileUrl;
+                // 去除URL参数(?后面的部分)
+                if (originalFileName.contains("?")) {
+                    originalFileName = originalFileName.substring(0, originalFileName.indexOf("?"));
+                }
+                // 提取文件名(兼容 / 和 \ 两种路径分隔符)
+                if (originalFileName.contains("/")) {
+                    originalFileName = originalFileName.substring(originalFileName.lastIndexOf("/") + 1);
+                }
+                if (originalFileName.contains("\\")) {
+                    originalFileName = originalFileName.substring(originalFileName.lastIndexOf("\\") + 1);
+                }
+                // URL解码(处理中文文件名等编码情况)
+                try {
+                    originalFileName = java.net.URLDecoder.decode(originalFileName, "UTF-8");
+                } catch (Exception e) {
+                    // 解码失败则使用原始名称
+                }
+                if (StringUtils.isBlank(originalFileName)) {
+                    originalFileName = "unknown";
+                }
+                String fileType = "";
+                if (originalFileName.contains(".")) {
+                    fileType = originalFileName.substring(originalFileName.lastIndexOf(".") + 1).toLowerCase();
+                }
+
+                // 6. 读取文件流并上传到OSS(支持本地路径和HTTP URL两种方式,兼容Windows/Linux)
+                InputStream fileStream = null;
+                HttpURLConnection connection = null;
+                String ossFullUrl;
+                boolean isLocalFile = false;
+                try {
+                    // 判定是本地路径还是HTTP URL
+                    String trimmedPath = fileUrl.trim();
+                    // 获取当前操作系统类型
+                    String osName = System.getProperty("os.name", "").toLowerCase();
+                    boolean isWindows = osName.contains("win");
+
+                    if (trimmedPath.startsWith("http://") || trimmedPath.startsWith("https://")) {
+                        // HTTP/HTTPS URL:任何系统都走网络下载
+                        isLocalFile = false;
+                    } else if (trimmedPath.startsWith("file://")) {
+                        // file:// 协议:任何系统都走本地读取
+                        isLocalFile = true;
+                    } else if (isWindows) {
+                        // Windows系统:识别盘符开头的路径(如 D:\ 或 C:/)
+                        isLocalFile = trimmedPath.matches("^[A-Za-z]:[/\\\\].*");
+                    } else {
+                        // Linux/Mac系统:识别以 / 开头的绝对路径
+                        isLocalFile = trimmedPath.startsWith("/");
+                    }
+
+                    if (isLocalFile) {
+                        // ========== 本地文件路径:直接读取 ==========
+                        String localPath = trimmedPath;
+                        if (localPath.startsWith("file://")) {
+                            localPath = localPath.substring("file://".length());
+                        }
+                        File localFile = new File(localPath);
+                        if (!localFile.exists()) {
+                            throw new RuntimeException("本地文件不存在:" + localPath + "(当前系统:" + osName + ")");
+                        }
+                        if (!localFile.isFile()) {
+                            throw new RuntimeException("路径不是文件:" + localPath);
+                        }
+                        fileStream = new FileInputStream(localFile);
+                        logger.info("读取本地文件[{}]:{}", osName, localPath);
+                    } else {
+                        // ========== HTTP URL:通过HTTP下载 ==========
+                        URL url = new URL(fileUrl);
+                        connection = (HttpURLConnection) url.openConnection();
+                        connection.setRequestMethod("GET");
+                        connection.setConnectTimeout(30000);
+                        connection.setReadTimeout(60000);
+                        connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+                        connection.setRequestProperty("Accept", "*/*");
+
+                        int responseCode = connection.getResponseCode();
+                        if (responseCode != HttpURLConnection.HTTP_OK) {
+                            throw new RuntimeException("外部文件下载失败,HTTP状态码:" + responseCode);
+                        }
+                        fileStream = connection.getInputStream();
+                    }
+
+                    // 构建OSS存储路径
+                    String datePath = DateUtils.formatDate(new Date(), "yyyy/MM/dd/");
+                    String ossDir = "attachment-file/workKnowledgeBaseShareInfo/" + datePath;
+                    String ossFileName = System.currentTimeMillis() + "_" + originalFileName;
+
+                    // 上传到OSS
+                    OSSClientUtil ossUtil = new OSSClientUtil();
+                    ossUtil.uploadFile2OSS(fileStream, ossDir, ossFileName);
+
+                    // 拼接完整OSS访问URL
+                    ossFullUrl = Global.getAliyunUrl() + "/" + ossDir + ossFileName;
+                    logger.info("文件上传OSS成功 - 原始路径:{}, OSS路径:{}", fileUrl, ossFullUrl);
+                } finally {
+                    if (fileStream != null) {
+                        try { fileStream.close(); } catch (Exception ignored) {}
+                    }
+                    if (connection != null) {
+                        connection.disconnect();
+                    }
+                }
+
+                // 7. 创建附件记录(使用OSS URL)
+                Workattachment attachment = new Workattachment();
+                attachment.setUrl(ossFullUrl);
+                attachment.setAttachmentName(originalFileName);
+                attachment.setType(fileType);
+                attachment.setAttachmentId(record.getId());
+                attachment.setAttachmentFlag(ATTACHMENT_FLAG);
+                attachment.setAttachmentUser(UserUtils.getUser().getId());
+                attachment.setCompanyId(UserUtils.getSelectCompany().getId());
+                attachment.setDelFlag(Workattachment.DEL_FLAG_NORMAL);
+                workattachmentService.insertOnWorkAttachment(attachment);
+
+                // 8. 更新记录的审核状态为未审核(1)、内容属性为原创(0)
+                record.setAuditStatus(AUDIT_STATUS_PENDING);
+                record.setContentAttribute("0");
+                record.setAuditPassCount(0);
+                record.setAuditRejectCount(0);
+                record.setSubmitAuditUserId(UserUtils.getUser().getId());
+                record.setAuditStartDate(new Date());
+                record.preUpdate();
+                dao.update(record);
+
+                successCount++;
+                logger.info("文件导入成功 - 记录名称:{}, 原始URL:{}, OSS地址:{}", name, fileUrl, ossFullUrl);
+            } catch (Exception e) {
+                failCount++;
+                failureMsg.append("<br/>第").append(i + 2).append("行导入失败:").append(e.getMessage()).append(";");
+                logger.error("文件导入异常 - 第{}行", i + 2, e);
+            }
+        }
+
+        String resultMsg = "成功导入 " + successCount + " 条文件记录";
+        if (failCount > 0) {
+            resultMsg += ",失败 " + failCount + " 条:" + failureMsg.toString();
+        }
+        return resultMsg;
+    }
+
+    /**
      * 插入单个动态字段值
      */
     private void insertDynamicValue(String fileId, String fieldKey, String fieldValue, Map<String, String> fieldKeyToIdMap) {

+ 85 - 4
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java

@@ -628,7 +628,7 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     }
 
     /**
-     * 执行导入
+     * 执行导入(动态表头匹配)
      */
     @RequiresPermissions("workKnowledgeBase:share:import")
     @RequestMapping(value = "importFile", method = RequestMethod.POST)
@@ -638,8 +638,89 @@ public class WorkKnowledgeBaseShareController extends BaseController {
         Map<String, Object> result = new HashMap<>();
         try {
             ImportExcel ei = new ImportExcel(file, 1, 0);
-            List<WorkKnowledgeBaseImportInfo> list = ei.getDataList(WorkKnowledgeBaseImportInfo.class);
-            String resultMsg = shareService.importData(list, rootId);
+            // 读取表头(第1行,即headerNum所在行,0-based索引)
+            org.apache.poi.ss.usermodel.Row headerRow = ei.getRow(1);
+            int colCount = ei.getLastCellNum();
+            List<String> headers = new ArrayList<>();
+            for (int i = 0; i < colCount; i++) {
+                Object val = ei.getCellValue(headerRow, i);
+                headers.add(val != null ? val.toString().trim() : "");
+            }
+            // 读取数据行
+            List<List<String>> dataRows = new ArrayList<>();
+            for (int i = ei.getDataRowNum(); i <= ei.getLastDataRowNum(); i++) {
+                org.apache.poi.ss.usermodel.Row row = ei.getRow(i);
+                if (row == null) continue;
+                List<String> rowData = new ArrayList<>();
+                boolean hasData = false;
+                for (int j = 0; j < colCount; j++) {
+                    Object val = ei.getCellValue(row, j);
+                    String cellStr = val != null ? val.toString().trim() : "";
+                    if (!cellStr.isEmpty()) hasData = true;
+                    rowData.add(cellStr);
+                }
+                if (hasData) {
+                    dataRows.add(rowData);
+                }
+            }
+            String resultMsg = shareService.importData(headers, dataRows, rootId);
+            result.put("success", true);
+            result.put("message", resultMsg);
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", "导入失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 批量导入文件附件页面(通过URL方式)
+     */
+    @RequiresPermissions("workKnowledgeBase:share:import")
+    @RequestMapping(value = "importFileBatchForm")
+    public String importFileBatchForm(@RequestParam(required = false) String rootId, Model model) {
+        model.addAttribute("rootId", rootId);
+        model.addAttribute("rootNodes", shareService.findRootNodes());
+        return "modules/WorkKnowledgeBase/workKnowledgeBaseShareImportBatch";
+    }
+
+    /**
+     * 执行批量文件导入(通过URL方式,根据标准名称匹配记录并创建附件)
+     */
+    @RequiresPermissions("workKnowledgeBase:share:import")
+    @RequestMapping(value = "importFileBatch", method = RequestMethod.POST)
+    @ResponseBody
+    public Map<String, Object> importFileBatch(@RequestParam MultipartFile file,
+                                               @RequestParam String rootId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            ImportExcel ei = new ImportExcel(file, 1, 0);
+            // 读取表头
+            org.apache.poi.ss.usermodel.Row headerRow = ei.getRow(1);
+            int colCount = ei.getLastCellNum();
+            List<String> headers = new ArrayList<>();
+            for (int i = 0; i < colCount; i++) {
+                Object val = ei.getCellValue(headerRow, i);
+                headers.add(val != null ? val.toString().trim() : "");
+            }
+            // 读取数据行
+            List<List<String>> dataRows = new ArrayList<>();
+            for (int i = ei.getDataRowNum(); i <= ei.getLastDataRowNum(); i++) {
+                org.apache.poi.ss.usermodel.Row row = ei.getRow(i);
+                if (row == null) continue;
+                List<String> rowData = new ArrayList<>();
+                boolean hasData = false;
+                for (int j = 0; j < colCount; j++) {
+                    Object val = ei.getCellValue(row, j);
+                    String cellStr = val != null ? val.toString().trim() : "";
+                    if (!cellStr.isEmpty()) hasData = true;
+                    rowData.add(cellStr);
+                }
+                if (hasData) {
+                    dataRows.add(rowData);
+                }
+            }
+            String resultMsg = shareService.importFilesByUrl(headers, dataRows, rootId);
             result.put("success", true);
             result.put("message", resultMsg);
         } catch (Exception e) {
@@ -692,7 +773,7 @@ public class WorkKnowledgeBaseShareController extends BaseController {
             // 构建导出列标题(固定列 + 动态列)
             List<String> headers = new ArrayList<>();
             // 固定列:体系节点名称、标准名称
-            headers.add("体系节点名称");
+            headers.add("所属节点名称");
             headers.add("标准名称");
             
             // 动态列:根据动态字段配置添加

+ 13 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml

@@ -501,4 +501,17 @@
         FOR UPDATE
     </select>
 
+    <!-- 根据文件名称和根节点ID查询匹配的记录(用于文件批量导入时按名称定位记录) -->
+    <select id="findByNameAndRootId" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseShareInfo">
+        SELECT <include refid="shareColumns"/>
+        FROM work_knowledge_base_share_info a
+        WHERE a.name = #{name}
+          AND a.del_flag = '0'
+          AND a.tree_node_id IN (
+              SELECT t.id FROM work_knowledge_base_tree_info t
+              WHERE (t.id = #{rootId} OR t.root_id = #{rootId})
+                AND t.del_flag = '0'
+          )
+    </select>
+
 </mapper>

+ 9 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseTreeInfoDao.xml

@@ -160,6 +160,15 @@
         LIMIT 1
     </select>
 
+    <!-- 根据节点名称和根节点ID查找所有匹配的节点id(限定在某个根节点范围内匹配) -->
+    <select id="findIdsByTreeNameAndRootId" resultType="java.lang.String">
+        SELECT id
+        FROM work_knowledge_base_tree_info
+        WHERE tree_name = #{treeName}
+          AND (id = #{rootId} OR root_id = #{rootId})
+          AND del_flag = '0'
+    </select>
+
     <!-- 查询所有根节点(parent_id='0'的节点) -->
     <select id="findRootNodes" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseTreeInfo">
         SELECT <include refid="treeColumns"/>

+ 11 - 0
src/main/webapp/WEB-INF/tags/table/singleAttachmentManager.tag

@@ -170,6 +170,17 @@
 
                     <%-- 文件大小 --%>
                     <td>${attach.fileSize}</td>
+                    <%-- 已有附件隐藏字段(表单提交时携带,避免编辑时丢失) --%>
+                    <input name="workAttachments[${status.index}].id" type="hidden" value="${attach.id}"/>
+                    <input name="workAttachments[${status.index}].attachmentId" type="hidden" value="${attach.attachmentId}"/>
+                    <input name="workAttachments[${status.index}].url" type="hidden" value="${attach.url}"/>
+                    <input name="workAttachments[${status.index}].temporaryUrl" type="hidden" value="${attach.temporaryUrl}"/>
+                    <input name="workAttachments[${status.index}].attachmentName" type="hidden" value="${attach.attachmentName}"/>
+                    <input name="workAttachments[${status.index}].createBy.id" type="hidden" value="${attach.createBy.id}"/>
+                    <input name="workAttachments[${status.index}].type" type="hidden" value="${attach.type}"/>
+                    <input name="workAttachments[${status.index}].attachmentFlag" type="hidden" value="${attach.attachmentFlag}"/>
+                    <input name="workAttachments[${status.index}].fileSize" type="hidden" value="${attach.fileSize}"/>
+                    <input name="workAttachments[${status.index}].delFlag" type="hidden" value="0"/>
 
                     <%-- 操作按钮(下载/删除) --%>
                     <td class="op-td">

+ 4 - 0
src/main/webapp/static/oss/ossupload.js

@@ -1426,6 +1426,7 @@ function multitest (ossClient, storeAs, file,attachmentId,attachmentFlag,uploadP
                         '<input name="workAttachments['+lenght+'].type" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.type+'"/>'+
                         '<input name="workAttachments['+lenght+'].attachmentFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.attachmentFlag+'"/>'+
                         '<input name="workAttachments['+lenght+'].fileSize" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.fileSize+'"/>'+
+                        '<input name="workAttachments['+lenght+'].delFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="0"/>'+
                         '<input name="workAttachments['+lenght+'].divIdType" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.divIdType+'"/>'+
                         '</td>'+
                         '<td class="op-td">'+
@@ -1567,6 +1568,7 @@ function multitestToDify (ossClient, storeAs, file,attachmentId,attachmentFlag,u
                         '<input name="workAttachments['+lenght+'].type" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.type+'"/>'+
                         '<input name="workAttachments['+lenght+'].attachmentFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.attachmentFlag+'"/>'+
                         '<input name="workAttachments['+lenght+'].fileSize" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.fileSize+'"/>'+
+                        '<input name="workAttachments['+lenght+'].delFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="0"/>'+
                         '<input name="workAttachments['+lenght+'].divIdType" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.divIdType+'"/>'+
                         '</td>'+
                         '<td >'+ ' <span class=\'layui-badge layui-bg-orange\'>未同步</span>'+'</td>'+
@@ -2400,6 +2402,7 @@ function invoiceReimbursementMultitest (ossClient, storeAs, file,attachmentId,at
                         '<input name="workAttachments['+lenght+'].type" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.type+'"/>'+
                         '<input name="workAttachments['+lenght+'].attachmentFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.attachmentFlag+'"/>'+
                         '<input name="workAttachments['+lenght+'].fileSize" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.fileSize+'"/>'+
+                        '<input name="workAttachments['+lenght+'].delFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="0"/>'+
                         '<input name="workAttachments['+lenght+'].divIdType" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.divIdType+'"/>'+
                         '</td>'+
                         '<td class="op-td">'+
@@ -3659,6 +3662,7 @@ function multitestOnFileSize (ossClient, storeAs, file,attachmentId,attachmentFl
                         '<input name="workAttachments['+lenght+'].type" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.type+'"/>'+
                         '<input name="workAttachments['+lenght+'].attachmentFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.attachmentFlag+'"/>'+
                         '<input name="workAttachments['+lenght+'].fileSize" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.fileSize+'"/>'+
+                        '<input name="workAttachments['+lenght+'].delFlag" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="0"/>'+
                         '<input name="workAttachments['+lenght+'].divIdType" type="hidden" readonly="readonly" maxlength="20" style="width:140px;" class="form-control" value="'+data.divIdType+'"/>'+
                         '</td>'+
                         '<td >'+fileSizeInMB+'</td>'+

+ 36 - 5
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp

@@ -747,9 +747,9 @@
                 title: '导入知识库数据',
                 maxmin: false,
                 content: '${ctx}/workKnowledgeBase/share/importForm?rootId=${rootId}',
-                skin: 'three-btns',
-                btn: ['开始导入', '关闭'],
-                /*btn1: function(index, layero) {
+                skin: 'three-btns with-demo',
+                btn: ['下载模板', '开始导入', '关闭'],
+                btn1: function(index, layero) {
                     // 下载模板按钮
                     var iframeWin = layero.find('iframe')[0];
                     var rootId = iframeWin.contentWindow.$('#rootId').val();
@@ -758,8 +758,8 @@
                         return;
                     }
                     window.location.href = '${ctx}/workKnowledgeBase/share/exportTemplate?rootId=' + rootId;
-                },*/
-                btn1: function(index, layero) {
+                },
+                btn2: function(index, layero) {
                     var iframeWin = layero.find('iframe')[0];
                     if (iframeWin.contentWindow.doSubmit(1)) {
                         setTimeout(function() {
@@ -772,6 +772,29 @@
             });
         }
 
+        /** 批量文件导入弹窗(通过外部URL下载文件并上传到OSS) */
+        function openImportFileBatchDialog() {
+            top.layer.open({
+                type: 2,
+                area: ['800px', '450px'],
+                title: '批量导入文件附件',
+                maxmin: false,
+                content: '${ctx}/workKnowledgeBase/share/importFileBatchForm?rootId=${rootId}',
+                skin: 'three-btns with-demo',
+                btn: ['开始导入', '关闭'],
+                btn1: function(index, layero) {
+                    var iframeWin = layero.find('iframe')[0];
+                    if (iframeWin.contentWindow.doSubmit(1)) {
+                        setTimeout(function() {
+                            top.layer.close(index);
+                            search();
+                        }, 3000);
+                    }
+                },
+                btn2: function(index) { top.layer.close(index); }
+            });
+        }
+
         /** 重置查询 */
         function resetSearch() {
             $('#searchForm input[type=text]').val('');
@@ -943,6 +966,14 @@
                                      导出积分统计
                                 </button>
                             </shiro:hasPermission>
+                            <shiro:hasPermission name="workKnowledgeBase:share:import">
+                                <button class="layui-btn layui-btn-sm layui-bg-cyan"
+                                        onclick="openImportFileBatchDialog()">
+                                    文件导入
+                                </button>
+                            </shiro:hasPermission>
+                                                    
+                                                    
                         </c:if>
                         <button class="layui-btn layui-btn-sm" onclick="search()" title="刷新">刷新</button>