Parcourir la source

保存文件到指定目录并提供下载和预览接口

zhaohan il y a 10 mois
Parent
commit
a5cb8c7097

+ 13 - 0
ruoyi-admin/src/main/resources/application-prod.yml

@@ -90,5 +90,18 @@ sms:
   signName: 测试
   # 腾讯专用
   sdkAppId:
+file:
+  #上传附件保存本地地址
+  upload-path: E:/knowledge/files/
+  #上传附件访问地址
+  base-url: http://8.137.127.56:5666
+
+mcp:
+  server:
+    url: http://localhost:8085
+
+
+
+
 
 

+ 4 - 2
ruoyi-admin/src/main/resources/application.yml

@@ -72,7 +72,7 @@ spring:
     # 国际化资源文件路径
     basename: i18n/messages
   profiles:
-    active: dev
+    active: prod
 #      active: @profiles.active@
   # 文件上传
   servlet:
@@ -151,6 +151,8 @@ security:
     - /rag/hospital/list
     - /medical/medicalList
     - /patient/vitalSigns
+    - /file/download
+    - /file/view
 # 多租户配置
 tenant:
   # 是否开启
@@ -311,7 +313,7 @@ spring:
         sse:
           connections:
             server:
-              url: http://127.0.0.1:8081
+              url: http://127.0.0.1:8085
         stdio:
           servers-configuration: classpath:mcp-server.json
         request-timeout: 300s

+ 6 - 0
ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/KnowledgeAttach.java

@@ -80,5 +80,11 @@ public class KnowledgeAttach extends BaseEntity {
    * 写入向量数据库状态10未开始,20进行中,30已完成
    */
   private Integer vectorStatus;
+  /**
+   * 文件保存地址
+   */
+  private String publicUrl;
+  private String filePath;
+
 
 }

+ 165 - 0
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/FileController.java

@@ -0,0 +1,165 @@
+package org.ruoyi.chat.controller.chat;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.domain.KnowledgeAttach;
+import org.ruoyi.mapper.KnowledgeAttachMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@Slf4j
+@RestController
+@RequestMapping("/file")
+public class FileController {
+
+
+
+    @Value("${file.upload-path}")
+    private String uploadPath;
+
+    @Autowired
+    private KnowledgeAttachMapper attachMapper;
+
+    /**
+     * 下载文件(通过 docId)
+     */
+    @GetMapping("/download")
+    public void download(@RequestParam String docId, HttpServletResponse response) {
+        try {
+            // 1. 根据 docId 查询附件信息
+            KnowledgeAttach attach = attachMapper.selectOne(
+                    new LambdaQueryWrapper<KnowledgeAttach>()
+                            .eq(KnowledgeAttach::getDocId, docId)
+            );
+
+            if (attach == null) {
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("文件记录不存在");
+                return;
+            }
+
+            // 2. 获取 kid 和文件名
+            String kid = attach.getKid();
+            String docName = attach.getDocName(); // 你这里用的是原始文件名作为存储名
+
+            if (docName == null || docName.isEmpty()) {
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("文件名信息缺失");
+                return;
+            }
+
+            if (kid == null || kid.isEmpty()) {
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("知识库ID(kid)缺失");
+                return;
+            }
+
+            // ✅ 正确路径:uploadPath/kid/docName
+            Path filePath = Paths.get(uploadPath, kid, docName);
+            if (!Files.exists(filePath)) {
+                log.warn("文件未找到,路径:{}", filePath.toString());
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("文件在磁盘上未找到");
+                return;
+            }
+
+            // 3. 探测内容类型
+            String contentType;
+            try {
+                contentType = Files.probeContentType(filePath);
+            } catch (IOException e) {
+                contentType = "application/octet-stream";
+            }
+            if (contentType == null) {
+                contentType = "application/octet-stream";
+            }
+
+            // 4. 设置响应头(强制下载)
+            response.setContentType(contentType);
+            response.setHeader(
+                    "Content-Disposition",
+                    "attachment; filename=" + URLEncoder.encode(docName, StandardCharsets.UTF_8)
+            );
+            response.setHeader("Content-Length", String.valueOf(Files.size(filePath)));
+
+            // 5. 输出文件流
+            try (InputStream in = Files.newInputStream(filePath);
+                 OutputStream out = response.getOutputStream()) {
+                in.transferTo(out);
+                out.flush();
+            }
+
+        } catch (IOException e) {
+            log.error("文件下载失败 - docId: {}", docId, e);
+            try {
+                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+                response.getWriter().write("文件下载失败:" + e.getMessage());
+            } catch (IOException ignored) {}
+        }
+    }
+
+    /**
+     * 【可选】在线预览(不强制下载)
+     */
+    @GetMapping("/view")
+    public void view(@RequestParam String docId, HttpServletResponse response) {
+        try {
+            KnowledgeAttach attach = attachMapper.selectOne(
+                    new LambdaQueryWrapper<KnowledgeAttach>()
+                            .eq(KnowledgeAttach::getDocId, docId)
+            );
+
+            if (attach == null || attach.getKid() == null || attach.getDocName() == null) {
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("文件不存在");
+                return;
+            }
+
+            Path filePath = Paths.get(uploadPath, attach.getKid(), attach.getDocName());
+            if (!Files.exists(filePath)) {
+                response.setStatus(HttpStatus.NOT_FOUND.value());
+                response.getWriter().write("文件未找到");
+                return;
+            }
+
+            String contentType = Files.probeContentType(filePath);
+            if (contentType == null) contentType = "application/octet-stream";
+
+            // inline 表示尝试在浏览器中打开(如 PDF 可预览)
+            response.setContentType(contentType);
+            response.setHeader(
+                    "Content-Disposition",
+                    "inline; filename=" + URLEncoder.encode(attach.getDocName(), StandardCharsets.UTF_8)
+            );
+            response.setHeader("Content-Length", String.valueOf(Files.size(filePath)));
+
+            try (InputStream in = Files.newInputStream(filePath);
+                 OutputStream out = response.getOutputStream()) {
+                in.transferTo(out);
+                out.flush();
+            }
+
+        } catch (IOException e) {
+            log.error("文件预览失败 - docId: {}", docId, e);
+            try {
+                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+                response.getWriter().write("预览失败:" + e.getMessage());
+            } catch (IOException ignored) {}
+        }
+    }
+}

+ 1 - 1
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/ToolService.java

@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
 @Service
 public class ToolService {
 
-    @Value("${mcp.server.url:http://localhost:8085}")
+    @Value("${mcp.server.url}")
     private String mcpServerUrl;
 
     @Autowired

+ 1 - 1
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java

@@ -237,7 +237,7 @@ public class SseServiceImpl implements ISseService {
                         "\n" +
                         "在回答时请遵循以下规则:\n" +
                         "1. 只能引用提供的资料进行回答,不要编造信息。\n" +
-                        "2. 如果引用了某段资料,请在回答中对应位置用 ①②③ 等标注。- 多处引用相同资料,也都统一使用标注。\n" +
+                        "2. 如果引用了某段资料,请在回答中对应位置用 ①②③ 等标注。- 内容与来源要一一对应,多处引用相同资料,也都统一使用标注。\n" +
                         "3. 回答结尾,请根据标注,列出来源。例如:\n" +
                         "   来源:\n" +
                         "   ① 文件A.pdf\n" +

+ 42 - 4
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java

@@ -31,11 +31,17 @@ import org.ruoyi.service.VectorStoreService;
 import org.ruoyi.system.service.ISysOssService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.*;
 
 
@@ -65,6 +71,12 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
 
   private final ISysOssService ossService;
 
+  @Value("${file.upload-path}")
+  private String uploadPath;
+
+  @Value("${file.base-url}")
+  private String baseUrl;
+
   /**
    * 查询知识库
    */
@@ -100,11 +112,11 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
     lqw.like(StringUtils.isNotBlank(bo.getKname()), KnowledgeInfo::getKname, bo.getKname());
     lqw.eq(bo.getShare() != null, KnowledgeInfo::getShare, bo.getShare());
     lqw.eq(StringUtils.isNotBlank(bo.getDescription()), KnowledgeInfo::getDescription,
-        bo.getDescription());
+            bo.getDescription());
     lqw.eq(StringUtils.isNotBlank(bo.getKnowledgeSeparator()), KnowledgeInfo::getKnowledgeSeparator,
-        bo.getKnowledgeSeparator());
+            bo.getKnowledgeSeparator());
     lqw.eq(StringUtils.isNotBlank(bo.getQuestionSeparator()), KnowledgeInfo::getQuestionSeparator,
-        bo.getQuestionSeparator());
+            bo.getQuestionSeparator());
     lqw.eq(bo.getOverlapChar() != null, KnowledgeInfo::getOverlapChar, bo.getOverlapChar());
     lqw.eq(bo.getRetrieveLimit() != null, KnowledgeInfo::getRetrieveLimit, bo.getRetrieveLimit());
     lqw.eq(bo.getTextBlockSize() != null, KnowledgeInfo::getTextBlockSize, bo.getTextBlockSize());
@@ -166,7 +178,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
       baseMapper.insert(knowledgeInfo);
       if (knowledgeInfo != null) {
         vectorStoreService.createSchema(String.valueOf(knowledgeInfo.getId()),
-            bo.getVectorModelName());
+                bo.getVectorModelName());
       }
     } else {
       baseMapper.updateById(knowledgeInfo);
@@ -200,6 +212,31 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
     KnowledgeAttach knowledgeAttach = new KnowledgeAttach();
     knowledgeAttach.setKid(kid);
     String docId = RandomUtil.randomString(10);
+    try {
+      // 根据 kid 生成子目录
+      Path targetParentPath = Paths.get(uploadPath, kid);
+      Files.createDirectories(targetParentPath); // 确保目录存在
+
+      // 文件存储路径为 uploadPath/kid/fileName
+      Path targetPath = targetParentPath.resolve(fileName);
+
+      file.transferTo(targetPath);
+
+      // 生成可访问的 HTTP 链接
+      String downloadUrl = baseUrl + "/api/file/view?docId=" + URLEncoder.encode(docId, StandardCharsets.UTF_8);
+
+      // 保存信息
+      knowledgeAttach.setDocId(docId);
+      knowledgeAttach.setDocName(fileName);
+      knowledgeAttach.setDocType(fileName.substring(fileName.lastIndexOf(".") + 1));
+      knowledgeAttach.setPublicUrl(downloadUrl);// HTTP 下载链接
+      knowledgeAttach.setFilePath(targetPath.toString());
+
+    } catch (IOException e) {
+      log.error("保存文件失败", e);
+      throw new RuntimeException("文件保存失败", e);
+    }
+
     knowledgeAttach.setDocId(docId);
     knowledgeAttach.setDocName(fileName);
     knowledgeAttach.setDocType(fileName.substring(fileName.lastIndexOf(".")+1));
@@ -265,3 +302,4 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
   }
 
 }
+

+ 25 - 14
ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/McpRegistry.java

@@ -1,22 +1,33 @@
 package org.ruoyi.chat.util;
 
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+
+@Component
 public class McpRegistry {
 
-    // 工具名 => MCP服务URL
-    private static final Map<String, String> TOOL_TO_MCP_URL = new HashMap<>();
+    private static String MCP_SERVER_URL;
 
-    static {
-        // 初始化静态映射,可支持多个 MCP 服务接入
-        TOOL_TO_MCP_URL.put("loginUser", "http://localhost:8085/mcp/execute");
-        TOOL_TO_MCP_URL.put("queryKnowledgeList", "http://localhost:8085/mcp/execute");
-        TOOL_TO_MCP_URL.put("hospitalActivity", "http://localhost:8085/mcp/execute");
-        TOOL_TO_MCP_URL.put("get_weather", "http://localhost:8085/weather/mcp");
+    @Value("${mcp.server.url}")
+    public void setMcpServerUrl(String url) {
+        MCP_SERVER_URL = url;
+
+        // 在这里初始化 Map,确保 mcpServerUrl 已经有值
+        TOOL_TO_MCP_URL.put("loginUser", MCP_SERVER_URL + "/mcp/execute");
+        TOOL_TO_MCP_URL.put("queryKnowledgeList", MCP_SERVER_URL + "/mcp/execute");
+        TOOL_TO_MCP_URL.put("hospitalActivity", MCP_SERVER_URL + "/mcp/execute");
+        TOOL_TO_MCP_URL.put("get_weather", MCP_SERVER_URL + "/weather/mcp");
     }
 
+    // 工具名 => MCP服务URL
+    private static final Map<String, String> TOOL_TO_MCP_URL = new HashMap<>();
+
     /**
      * 获取某个工具对应的 MCP 服务地址
      */
@@ -25,10 +36,10 @@ public class McpRegistry {
     }
 
     /**
-     * 默认 MCP 地址(可根据需要设定为通用服务)
+     * 默认 MCP 地址
      */
     public static String getDefaultUrl() {
-        return "http://localhost:8085/mcp/execute";
+        return MCP_SERVER_URL + "/mcp/execute";
     }
 
     /**
@@ -39,23 +50,23 @@ public class McpRegistry {
     }
 
     /**
-     * 获取当前所有工具与 MCP 地址的映射(只读)
+     * 获取所有映射(只读)
      */
     public static Map<String, String> listMappings() {
         return Collections.unmodifiableMap(TOOL_TO_MCP_URL);
     }
 
     /**
-     * 移除某个工具的映射
+     * 移除某个工具
      */
     public static void removeTool(String toolName) {
         TOOL_TO_MCP_URL.remove(toolName);
     }
 
     /**
-     * 清空所有映射(慎用)
+     * 清空所有映射
      */
     public static void clear() {
         TOOL_TO_MCP_URL.clear();
     }
-}
+}