吉安感知网项目-后端
linwei
2026-07-11 62091bb640608f24812e069947928c49b4ebdedf
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -9,16 +9,21 @@
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.annotation.NonDS;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateTimeUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.sxkj.common.model.ResponseResult;
@@ -27,17 +32,25 @@
import org.sxkj.resource.param.AttachPageParam;
import org.sxkj.resource.param.AttachParam;
import org.sxkj.resource.service.IAttachService;
import org.sxkj.resource.util.OnlyofficeJwt;
import org.sxkj.resource.vo.AttachVO;
import org.sxkj.resource.vo.AttachmentDownloadParam;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 附件表 控制器
@@ -47,12 +60,30 @@
@Slf4j
@NonDS
@RestController
@AllArgsConstructor
@RequiredArgsConstructor
@RequestMapping("/attach")
@Api(value = "附件表接口", tags = "附件")
public class AttachController extends BladeController {
   private final IAttachService attachService;
   private final OnlyofficeJwt onlyofficeJwt;
   /** OnlyOffice应用服务基础URL,供Document Server访问代理下载接口 */
   @Value("${onlyoffice.base-url:}")
   private String onlyofficeBaseUrl;
   /** OnlyOffice回调地址 */
   @Value("${onlyoffice.callback-url:}")
   private String onlyofficeCallbackUrl;
   /** 远程文件URL缓存,key为attachId,value为远程文件URL */
   private static final ConcurrentHashMap<Long, String> REMOTE_URL_CACHE = new ConcurrentHashMap<>();
   /** 代理下载连接超时时间(毫秒) */
   private static final int CONNECT_TIMEOUT = 10000;
   /** 代理下载读取超时时间(毫秒) */
   private static final int READ_TIMEOUT = 30000;
   /**
    * 详情
@@ -72,6 +103,7 @@
   @ApiOperationSupport(order = 3)
   @ApiOperation(value = "附件分页", notes = "传入attach")
   public R<IPage<AttachVO>> page(AttachPageParam attach, Query query) {
      BladeUser user = AuthUtil.getUser();
      IPage<AttachVO> pages = attachService.selectAttachPage(Condition.getPage(query), attach);
      return R.data(pages);
   }
@@ -191,15 +223,13 @@
   /**
    * 流式附件下载接口,支持下载zip压缩包,不使用本地存储
    * @param param
    * @param response
    * @throws IOException
    */
   @ApiOperation(value = "流式附件下载接口", notes = "使用流方式返回数据,不使用本地存储")
   @PostMapping("/downloadByByte")
   @GetMapping("/downloadByByte")
   public void downloadByByte(
      @ApiParam(value = "附件下载参数") @RequestBody AttachmentDownloadParam param,
      HttpServletResponse response) throws IOException {
      @ApiParam(value = "附件下载参数") @RequestParam("attachIds") String attachIds, HttpServletResponse response) throws IOException {
      // 设置文件名
      String timestamp = DateTimeUtil.format(LocalDateTime.now(), "yyyyMMdd_HHmmss");
@@ -214,7 +244,7 @@
      try {
         outputStream = response.getOutputStream();
         Boolean result = attachService.downloadByByte(param, outputStream);
         Boolean result = attachService.downloadByByte(attachIds, outputStream);
         if (!result) {
            return;
         }
@@ -377,4 +407,329 @@
   //    return ResponseResult.success(result);
   // }
   /**
    * 获取OnlyOffice文档编辑器配置信息
    * <p>
    * 通过代理下载方式解决OnlyOffice Document Server无法直接访问远程文件URL的问题。
    * 将远程文件URL缓存到本地,并生成代理下载URL供Document Server使用。
    * </p>
    *
    * @param request  HTTP请求对象,用于构建代理下载URL
    * @param attachId 附件ID,用于查询附件信息
    * @return 返回包含用户信息和附件信息的OnlyOffice配置数据
    */
   @GetMapping("/getOnlyOfficeConfig")
   @ApiOperationSupport(order = 10)
   @ApiOperation(value = "获取OnlyOffice配置", notes = "根据附件ID查询附件信息,返回包含当前用户信息和附件信息的OnlyOffice文档编辑器配置")
   public R<Map<String, Object>> getOnlyOfficeConfig(
      HttpServletRequest request,
      @ApiParam(value = "附件ID", required = true) @RequestParam Long attachId,
      @RequestParam(value = "mode", required = false) String mode) {
      // 1. 获取当前登录用户信息
      BladeUser user = AuthUtil.getUser();
      // 2. 根据附件ID查询附件信息
      Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
      if (attach == null) {
         return R.fail("附件不存在");
      }
      // 3. 构建用户信息对象
      Map<String, Object> userConfig = new HashMap<>();
      userConfig.put("name", user != null ? user.getUserName() : "未知用户");
      userConfig.put("id", user != null ? user.getUserId() : "0");
      // 4. 构建编辑器自定义配置
      Map<String, Object> customization = new HashMap<>();
      customization.put("autosave", true);
      customization.put("forcesave", true);
      // 5. 构建编辑器配置
      Map<String, Object> editorConfig = new HashMap<>();
      editorConfig.put("customization", customization);
      editorConfig.put("mode", StringUtil.isBlank(mode) ? "view" : mode);
      editorConfig.put("callbackUrl", onlyofficeCallbackUrl);
      editorConfig.put("lang", "zh-CN");
      editorConfig.put("user", userConfig);
      // 6. 从附件对象获取文件信息
      String fileName = attach.getName(); // 附件名称
      String fileUrl = attach.getLink(); // 附件URL地址(远程URL)
      String fileType = ""; // 文件类型(扩展名)
      // 7. 从文件名中提取文件类型
      if (fileName != null && fileName.contains(".")) {
         fileType = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
      } else if (fileUrl != null && fileUrl.contains(".")) {
         fileType = fileUrl.substring(fileUrl.lastIndexOf(".") + 1).toLowerCase();
      }
      // 8. 根据文件类型确定文档类型
      String documentType = "word"; // 默认为word类型
      if (fileType.equals("xlsx") || fileType.equals("xls")) {
         documentType = "cell";
      } else if (fileType.equals("pptx") || fileType.equals("ppt")) {
         documentType = "slide";
      }
      // 9. 将远程文件URL缓存到本地,供代理下载接口使用
      if (fileUrl != null && !fileUrl.isEmpty()) {
         REMOTE_URL_CACHE.put(attachId, fileUrl);
      }
      // 10. 构建代理下载URL,替代直接使用远程URL
      // Document Server通过代理下载接口获取文件,避免无法直接访问远程URL的问题
      String documentUrl;
      // String documentUrl = buildProxyDownloadUrl(request, attachId);
      if (onlyofficeBaseUrl != null && !onlyofficeBaseUrl.isEmpty()) {
         // 使用代理下载URL,Document Server通过本服务的代理接口获取文件
         documentUrl = onlyofficeBaseUrl + "/attach/onlyoffice-download?attachId=" + attachId;
      } else {
         // 未配置baseUrl时回退到直接使用远程URL
         documentUrl = fileUrl != null ? fileUrl : "";
      }
      // 11. 生成文档唯一标识
      String key = generateRevisionId(fileName);
      // 12. 构建文档配置,使用代理下载URL替代远程URL
      Map<String, Object> document = new HashMap<>();
      document.put("title", fileName != null ? fileName : "未命名文档");
      document.put("url", documentUrl);
      document.put("fileType", fileType);
      document.put("key", attach.getMd5());
      // 13. 构建最终返回结果
      Map<String, Object> result = new HashMap<>();
      result.put("documentType", documentType);
      result.put("document", document);
      result.put("editorConfig", editorConfig);
      // 14. 生成 JWT token
      Map<String, Object> result1 = onlyofficeJwt.getToken(result);
      log.info("ONLYOFFICE配置生成完成,attachId: {}, documentUrl: {}", attachId, documentUrl);
      return R.data(result1);
   }
   public static final Integer MAX_KEY_LENGTH = 20;
   public static final Integer ANONYMOUS_USER_ID = 4;
   public static final Integer KILOBYTE_SIZE = 1024;
   public String generateRevisionId(final String expectedKey) {
        /* if the expected key length is greater than 20
        then he expected key is hashed and a fixed length value is stored in the string format */
      String formatKey = expectedKey.length() > MAX_KEY_LENGTH
         ? Integer.toString(expectedKey.hashCode()) : expectedKey;
      String key = formatKey.replace("[^0-9-.a-zA-Z_=]", "_");
      return key.substring(0, Math.min(key.length(), MAX_KEY_LENGTH));  // the resulting key length is 20 or less
   }
   /**
    * 构建当前服务器的代理下载URL
    *
    * @param request  HTTP请求对象,用于获取服务器地址
    * @param attachId 附件ID
    * @return 代理下载URL地址
    */
   private String buildProxyDownloadUrl(HttpServletRequest request, Long attachId) {
      // 1. 获取请求的协议、主机名和端口
      String scheme = request.getScheme();
      String serverName = request.getServerName();
      int serverPort = request.getServerPort();
      // 2. 构建服务器基础URL(省略默认端口)
      StringBuilder baseUrl = new StringBuilder();
      baseUrl.append(scheme).append("://").append(serverName);
      if (("http".equals(scheme) && serverPort != 80) || ("https".equals(scheme) && serverPort != 443)) {
         baseUrl.append(":").append(serverPort);
      }
      // baseUrl.append("http:192.168.1.33:80/blade-resource");
      // 3. 拼接代理下载URL
      return baseUrl.toString() + "/attach/onlyoffice-download?attachId=" + attachId;
   }
   @GetMapping("/onlyoffice-download")
   @ApiOperation(value = "OnlyOffice代理下载", notes = "代理下载远程文件供OnlyOffice Document Server访问")
   public void onlyofficeDownload(
      HttpServletRequest request,
      HttpServletResponse response,
      @ApiParam(value = "附件ID", required = true) @RequestParam Long attachId) throws IOException {
      // 处理 OPTIONS 预检请求
      if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
         response.setHeader("Access-Control-Allow-Origin", "*");
         response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
         response.setHeader("Access-Control-Allow-Headers", "*");
         response.setStatus(HttpServletResponse.SC_OK);
         return;
      }
      log.info("ONLYOFFICE 下载请求,attachId: {}", attachId);
      // 1. 根据附件ID查询附件信息
      Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
      if (attach == null) {
         response.sendError(HttpServletResponse.SC_NOT_FOUND, "附件不存在,attachId: " + attachId);
         return;
      }
      // 2. 优先从缓存中获取远程文件URL
      String remoteUrl = REMOTE_URL_CACHE.get(attachId);
      if (remoteUrl == null || remoteUrl.isEmpty()) {
         remoteUrl = attach.getLink();
         log.info("从数据库获取远程URL,attachId: {}, link: {}", attachId, remoteUrl);
      }
      // 3. 验证远程URL
      if (remoteUrl == null || remoteUrl.isEmpty()) {
         response.sendError(HttpServletResponse.SC_NOT_FOUND, "附件缺少远程文件URL,attachId: " + attachId);
         return;
      }
      // 4. 获取纯文件名(不含路径)
      String fileName = attach.getName();
      if (fileName == null || fileName.isEmpty()) {
         fileName = "document.docx";
      }
      // 移除路径部分
      if (fileName.contains("/")) {
         fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
      }
      if (fileName.contains("\\")) {
         fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
      }
      log.info("下载文件名: {}", fileName);
      // 5. 下载远程文件
      HttpURLConnection connection = null;
      InputStream inputStream = null;
      OutputStream outputStream = null;
      try {
         URL url = new URL(remoteUrl);
         connection = (HttpURLConnection) url.openConnection();
         connection.setRequestMethod("GET");
         connection.setConnectTimeout(CONNECT_TIMEOUT);
         connection.setReadTimeout(READ_TIMEOUT);
         // 复制请求头(如果需要)
         String userAgent = request.getHeader("User-Agent");
         if (userAgent != null) {
            connection.setRequestProperty("User-Agent", userAgent);
         }
         int responseCode = connection.getResponseCode();
         if (responseCode != HttpURLConnection.HTTP_OK) {
            log.error("远程文件下载失败,状态码: {}, url: {}", responseCode, remoteUrl);
            response.sendError(HttpServletResponse.SC_BAD_GATEWAY, "远程文件下载失败,状态码: " + responseCode);
            return;
         }
         // 6. 设置响应头
         String contentType = connection.getContentType();
         if (contentType == null || contentType.isEmpty()) {
            contentType = getContentTypeByFileName(fileName);
         }
         response.setContentType(contentType);
         // 设置 Content-Length
         int contentLength = connection.getContentLength();
         if (contentLength > 0) {
            response.setContentLength(contentLength);
         }
         // 7. 正确设置 Content-Disposition
         String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
            .replaceAll("\\+", "%20");
         // ONLYOFFICE 需要 inline 方式访问
         response.setHeader("Content-Disposition",
            "inline; filename=\"" + encodedFileName + "\"; " +
               "filename*=UTF-8''" + encodedFileName);
         // CORS 头
         response.setHeader("Access-Control-Allow-Origin", "*");
         response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
         response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
         // 缓存控制
         response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
         response.setHeader("Pragma", "no-cache");
         response.setHeader("Expires", "0");
         // 8. 流式传输
         inputStream = connection.getInputStream();
         outputStream = response.getOutputStream();
         byte[] buffer = new byte[8192];
         int bytesRead;
         long totalBytes = 0;
         while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            totalBytes += bytesRead;
         }
         outputStream.flush();
         log.info("下载完成,文件大小: {} bytes", totalBytes);
      } catch (Exception e) {
         log.error("代理下载远程文件失败,attachId: {}, remoteUrl: {}, 错误: {}",
            attachId, remoteUrl, e.getMessage(), e);
         if (!response.isCommitted()) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
               "代理下载远程文件失败: " + e.getMessage());
         }
      } finally {
         if (inputStream != null) {
            try { inputStream.close(); } catch (IOException ignored) {}
         }
         if (outputStream != null) {
            try { outputStream.close(); } catch (IOException ignored) {}
         }
         if (connection != null) {
            connection.disconnect();
         }
      }
   }
   /**
    * 根据文件名获取 Content-Type
    */
   private String getContentTypeByFileName(String fileName) {
      if (fileName == null) return "application/octet-stream";
      String extension = "";
      int lastDot = fileName.lastIndexOf(".");
      if (lastDot > 0) {
         extension = fileName.substring(lastDot + 1).toLowerCase();
      }
      switch (extension) {
         case "docx":
            return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
         case "doc":
            return "application/msword";
         case "xlsx":
            return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
         case "xls":
            return "application/vnd.ms-excel";
         case "pptx":
            return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
         case "ppt":
            return "application/vnd.ms-powerpoint";
         case "pdf":
            return "application/pdf";
         case "txt":
            return "text/plain";
         case "csv":
            return "text/csv";
         case "rtf":
            return "application/rtf";
         default:
            return "application/octet-stream";
      }
   }
}