From 62091bb640608f24812e069947928c49b4ebdedf Mon Sep 17 00:00:00 2001
From: linwei <872216696@qq.com>
Date: Sat, 11 Jul 2026 10:39:44 +0800
Subject: [PATCH] feat(resource): 集成OnlyOffice文档编辑功能
---
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java | 310 +++++++++++++++++++++++++++++++++++----------------
1 files changed, 214 insertions(+), 96 deletions(-)
diff --git a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
index 7f7d5b0..80ee579 100644
--- a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -9,7 +9,7 @@
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;
@@ -21,6 +21,9 @@
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;
@@ -57,12 +60,21 @@
@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<>();
@@ -410,8 +422,9 @@
@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) {
+ HttpServletRequest request,
+ @ApiParam(value = "附件ID", required = true) @RequestParam Long attachId,
+ @RequestParam(value = "mode", required = false) String mode) {
// 1. 获取当前登录用户信息
BladeUser user = AuthUtil.getUser();
@@ -434,9 +447,8 @@
// 5. 构建编辑器配置
Map<String, Object> editorConfig = new HashMap<>();
editorConfig.put("customization", customization);
- editorConfig.put("mode", "edit");
- // editorConfig.put("callbackUrl", "http://192.168.1.33:8310/office/callback");
- editorConfig.put("callbackUrl", "http://172.18.10.4:8010/office/callback");
+ editorConfig.put("mode", StringUtil.isBlank(mode) ? "view" : mode);
+ editorConfig.put("callbackUrl", onlyofficeCallbackUrl);
editorConfig.put("lang", "zh-CN");
editorConfig.put("user", userConfig);
@@ -467,7 +479,15 @@
// 10. 构建代理下载URL,替代直接使用远程URL
// Document Server通过代理下载接口获取文件,避免无法直接访问远程URL的问题
- String proxyDownloadUrl = buildProxyDownloadUrl(request, attachId);
+ 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);
@@ -475,9 +495,8 @@
// 12. 构建文档配置,使用代理下载URL替代远程URL
Map<String, Object> document = new HashMap<>();
document.put("title", fileName != null ? fileName : "未命名文档");
- document.put("url", proxyDownloadUrl); // 使用代理下载URL
+ document.put("url", documentUrl);
document.put("fileType", fileType);
- document.put("attachId", attach.getId());
document.put("key", attach.getMd5());
// 13. 构建最终返回结果
@@ -487,10 +506,12 @@
result.put("editorConfig", editorConfig);
// 14. 生成 JWT token
- Map<String, Object> result1 = OnlyofficeJwt.getToken(result);
- log.info("ONLYOFFICE配置生成完成,attachId: {}, 代理下载URL: {}", attachId, proxyDownloadUrl);
+ 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;
@@ -502,90 +523,6 @@
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
- }
-
- /**
- * OnlyOffice代理下载接口,用于代理下载远程文件
- * <p>
- * 当OnlyOffice Document Server无法直接访问远程文件URL时,
- * 通过本接口代理下载文件并流式返回给Document Server。
- * </p>
- *
- * @param request HTTP请求对象
- * @param response HTTP响应对象
- * @param attachId 附件ID,用于从缓存中查找对应的远程文件URL
- * @throws IOException 当下载或写入文件流时发生I/O异常
- */
- @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 {
-
- // 1. 从缓存中获取远程文件URL
- String remoteUrl = REMOTE_URL_CACHE.get(attachId);
- if (remoteUrl == null || remoteUrl.isEmpty()) {
- response.sendError(HttpServletResponse.SC_NOT_FOUND, "未找到对应的远程文件URL");
- return;
- }
-
- // 2. 根据附件ID查询附件信息,获取文件名
- Attach attach = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attachId));
- String fileName = (attach != null && attach.getName() != null) ? attach.getName() : "document.docx";
-
- // 3. 从远程URL下载文件
- 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);
-
- // 4. 检查响应状态码
- int responseCode = connection.getResponseCode();
- if (responseCode != HttpURLConnection.HTTP_OK) {
- response.sendError(HttpServletResponse.SC_BAD_GATEWAY, "远程文件下载失败,状态码: " + responseCode);
- return;
- }
-
- // 5. 获取远程文件的Content-Type
- String contentType = connection.getContentType();
- if (contentType == null || contentType.isEmpty()) {
- contentType = "application/octet-stream";
- }
-
- // 6. 设置响应头
- response.setContentType(contentType);
- response.setHeader("Content-Disposition", "attachment; filename=\""
- + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()) + "\"");
-
- // 7. 流式传输文件内容
- inputStream = connection.getInputStream();
- outputStream = response.getOutputStream();
- byte[] buffer = new byte[8192];
- int bytesRead;
- while ((bytesRead = inputStream.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- }
- outputStream.flush();
- } 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 {
- // 8. 关闭资源
- if (inputStream != null) {
- try { inputStream.close(); } catch (IOException ignored) {}
- }
- if (connection != null) {
- connection.disconnect();
- }
- }
}
/**
@@ -614,4 +551,185 @@
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";
+ }
+ }
+
}
--
Gitblit v1.9.3