From 6a77c800d56c6b4c3260c1b74ea7798d84f98ad8 Mon Sep 17 00:00:00 2001
From: linwei <872216696@qq.com>
Date: Fri, 10 Jul 2026 08:38:51 +0800
Subject: [PATCH] feat(resource): 集成OnlyOffice文档编辑功能
---
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java | 365 +++++++++++++++++++++++++++++++++++++++++++++-------
1 files changed, 315 insertions(+), 50 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 87457d6..ddc177e 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
@@ -12,45 +12,42 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.boot.ctrl.BladeController;
-import org.springblade.core.cache.utils.CacheUtil;
-import org.springblade.core.log.annotation.ApiLog;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
-import org.springblade.core.secure.annotation.PreAuth;
+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.constant.RoleConstant;
+import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateTimeUtil;
import org.springblade.core.tool.utils.Func;
-import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
-import org.sxkj.common.constant.CommonConstant;
import org.sxkj.common.model.ResponseResult;
-import org.sxkj.common.model.TimeRange;
-import org.sxkj.common.utils.HeaderUtils;
-import org.sxkj.common.utils.TimeRangeUtils;
-import org.sxkj.resource.dto.WaylineJobInfoQueryDto;
-import org.sxkj.resource.dto.WaylineJobInfoQueryParam;
+import org.sxkj.resource.dto.AttachDto;
import org.sxkj.resource.entity.Attach;
-import org.sxkj.resource.feign.IAttachClient;
+import org.sxkj.resource.param.AttachPageParam;
+import org.sxkj.resource.param.AttachParam;
import org.sxkj.resource.service.IAttachService;
-import org.sxkj.resource.vo.*;
-import org.sxkj.system.vo.TreeVo;
+import org.sxkj.resource.util.OnlyofficeJwt;
+import org.sxkj.resource.vo.AttachVO;
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.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
-
-import static org.springblade.core.cache.constant.CacheConstant.RESOURCE_CACHE;
+import java.util.concurrent.ConcurrentHashMap;
/**
* 附件表 控制器
@@ -67,7 +64,14 @@
private final IAttachService attachService;
- private final IAttachClient attachClient;
+ /** 远程文件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;
/**
* 详情
@@ -75,8 +79,8 @@
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@ApiOperation(value = "附件详情", notes = "传入attach")
- public R<Attach> detail(Attach attach) {
- Attach detail = attachService.getOne(Condition.getQueryWrapper(attach));
+ public R<Attach> detail(AttachParam attach) {
+ Attach detail = attachService.getOne(Wrappers.lambdaQuery(Attach.class).eq(Attach::getId, attach.getId()));
return R.data(detail);
}
@@ -86,11 +90,9 @@
@GetMapping("/page")
@ApiOperationSupport(order = 3)
@ApiOperation(value = "附件分页", notes = "传入attach")
- public R<IPage<AttachVO>> page(AttachVO attach, Query query) {
- // String areaCode = HeaderUtils.getAreaCode();
- // attach.setAreaCode(areaCode);
+ public R<IPage<AttachVO>> page(AttachPageParam attach, Query query) {
+ BladeUser user = AuthUtil.getUser();
IPage<AttachVO> pages = attachService.selectAttachPage(Condition.getPage(query), attach);
- AttachVO.settingNickName(pages.getRecords());
return R.data(pages);
}
@@ -100,7 +102,8 @@
@PostMapping("/submit")
@ApiOperationSupport(order = 6)
@ApiOperation(value = "新增或修改", notes = "传入attach")
- public R submit(@Valid @RequestBody Attach attach) {
+ public R submit(@Valid @RequestBody AttachDto attachDto) {
+ Attach attach = Objects.requireNonNull(BeanUtil.copy(attachDto, Attach.class));
return R.status(attachService.saveOrUpdate(attach));
}
@@ -132,17 +135,17 @@
/**
* 分页 附件表
*/
- // @GetMapping("/list")
- // @ApiOperationSupport(order = 2)
- // @ApiOperation(value = "附件分页", notes = "传入attach")
- // public R<IPage<Attach>> list(Attach attach, Query query) {
- // List<String> wayLineJobIds = attach.getWayLineJobIds();
- // QueryWrapper<Attach> queryWrapper1 = Condition.getQueryWrapper(attach);
- // QueryWrapper<Attach> queryWrapper = queryWrapper1.in(!CollectionUtils.isEmpty(wayLineJobIds), "wayline_job_id", wayLineJobIds);
- // queryWrapper.eq("is_deleted", 0);
- // IPage<Attach> pages = attachService.page(Condition.getPage(query), queryWrapper);
- // return R.data(pages);
- // }
+ @GetMapping("/list")
+ @ApiOperationSupport(order = 2)
+ @ApiOperation(value = "附件分页", notes = "传入attach")
+ public R<IPage<Attach>> list(Attach attach, Query query) {
+ String patrolTaskId = attach.getPatrolTaskId();
+ QueryWrapper<Attach> queryWrapper1 = Condition.getQueryWrapper(attach);
+ QueryWrapper<Attach> queryWrapper = queryWrapper1.eq(!StringUtils.isEmpty(patrolTaskId), "patrol_task_id", patrolTaskId);
+ queryWrapper.eq("is_deleted", 0);
+ IPage<Attach> pages = attachService.page(Condition.getPage(query), queryWrapper);
+ return R.data(pages);
+ }
/**
* 自定义分页 附件表
@@ -189,24 +192,68 @@
/**
* 新增 附件表
*/
- // @PostMapping("/save")
- // @ApiOperationSupport(order = 4)
- // @ApiOperation(value = "新增", notes = "传入attach")
- // public R save(@Valid @RequestBody Attach attach) {
- // return R.status(attachService.save(attach));
- // }
+ @PostMapping("/save")
+ @ApiOperationSupport(order = 4)
+ @ApiOperation(value = "新增", notes = "传入attach")
+ public R save(@Valid @RequestBody Attach attach) {
+ return R.status(attachService.save(attach));
+ }
/**
* 修改 附件表
*/
- // @PostMapping("/update")
- // @ApiOperationSupport(order = 5)
- // @ApiOperation(value = "修改", notes = "传入attach")
- // public R update(@Valid @RequestBody Attach attach) {
- // return R.status(attachService.updateById(attach));
- // }
+ @PostMapping("/update")
+ @ApiOperationSupport(order = 5)
+ @ApiOperation(value = "修改", notes = "传入attach")
+ public R update(@Valid @RequestBody Attach attach) {
+ return R.status(attachService.updateById(attach));
+ }
+ /**
+ * 流式附件下载接口,支持下载zip压缩包,不使用本地存储
+ * @param response
+ * @throws IOException
+ */
+ @ApiOperation(value = "流式附件下载接口", notes = "使用流方式返回数据,不使用本地存储")
+ @GetMapping("/downloadByByte")
+ public void downloadByByte(
+ @ApiParam(value = "附件下载参数") @RequestParam("attachIds") String attachIds, HttpServletResponse response) throws IOException {
+ // 设置文件名
+ String timestamp = DateTimeUtil.format(LocalDateTime.now(), "yyyyMMdd_HHmmss");
+ String fileName = "attachments_" + timestamp + ".zip";
+
+ // 设置响应头
+ response.setContentType("application/zip");
+ response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));
+
+ // 获取响应输出流
+ ServletOutputStream outputStream = null;
+ try {
+ outputStream = response.getOutputStream();
+
+ Boolean result = attachService.downloadByByte(attachIds, outputStream);
+ if (!result) {
+ return;
+ }
+ } catch (Exception e) {
+ if (!response.isCommitted()) {
+ response.reset();
+ response.setContentType("application/json;charset=UTF-8");
+ ResponseResult errorResult = ResponseResult.error("下载文件失败: " + e.getMessage());
+
+ // 使用输出流写入错误信息,避免getWriter()冲突
+ try {
+ byte[] errorBytes = JSON.toJSONString(errorResult).getBytes(StandardCharsets.UTF_8);
+ outputStream.write(errorBytes);
+ } catch (IOException ioException) {
+ log.error("Error writing error response: {}", ioException.getMessage());
+ }
+ } else {
+ log.error("Cannot send error response: response already committed. Exception: {}", e.getMessage());
+ }
+ }
+ }
/**
* 删除 附件表及对应的附件信息
@@ -348,4 +395,222 @@
// 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) {
+ // 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", "edit");
+ editorConfig.put("callbackUrl", "http://192.168.1.33:8310/office/callback");
+ 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 proxyDownloadUrl = buildProxyDownloadUrl(request, attachId);
+
+ // 11. 生成文档唯一标识
+ String key = generateRevisionId(fileName);
+
+ // 12. 构建文档配置,使用代理下载URL替代远程URL
+ Map<String, Object> document = new HashMap<>();
+ document.put("title", fileName != null ? fileName : "未命名文档");
+ document.put("url", proxyDownloadUrl); // 使用代理下载URL
+ document.put("fileType", fileType);
+ document.put("attachId", attach.getId());
+ 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: {}, 代理下载URL: {}", attachId, proxyDownloadUrl);
+ 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
+ }
+
+ /**
+ * 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();
+ }
+ }
+ }
+
+ /**
+ * 构建当前服务器的代理下载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;
+ }
+
}
--
Gitblit v1.9.3