From c38cecce9b0247f5eb28c61538bb293a41358aa6 Mon Sep 17 00:00:00 2001
From: linwei <872216696@qq.com>
Date: Sat, 11 Jul 2026 10:11:45 +0800
Subject: [PATCH] feat(resource): 集成OnlyOffice文档编辑功能

---
 drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java |  157 ++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 115 insertions(+), 42 deletions(-)

diff --git a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java
index 19d8565..b06d590 100644
--- a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java
@@ -1,10 +1,11 @@
 package org.sxkj.resource.endpoint;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
-import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -13,18 +14,18 @@
 import org.springblade.core.oss.model.BladeFile;
 import org.springblade.core.tenant.annotation.NonDS;
 import org.springblade.core.tool.utils.FileUtil;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.sxkj.common.utils.file.FileNameHarsher;
 import org.sxkj.resource.builder.OssBuilder;
-import org.sxkj.resource.dto.OfficeCallbackDTO;
 import org.sxkj.resource.entity.Attach;
 import org.sxkj.resource.service.IAttachService;
 
+import javax.servlet.http.HttpServletResponse;
 import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * ONLYOFFICE 回调端点
@@ -52,58 +53,130 @@
 
 	/**
 	 * ONLYOFFICE 保存回调接口
-	 * 对应前端 config.editorConfig.callbackUrl = "http://localhost:8080/office/callback"
+	 * <p>
+	 * 处理ONLYOFFICE文档编辑保存回调,支持以下状态:
+	 * status=2: 文档编辑完成,关闭编辑器时触发
+	 * status=3: 文档保存出错时触发
+	 * status=6: 文档强制保存(forcesave)时触发
+	 * </p>
+	 * <p>
+	 * 对应前端 config.editorConfig.callbackUrl = "http://192.168.1.33:8310/office/callback"
+	 * </p>
 	 *
-	 * @param callbackDTO 回调数据对象
-	 * @return 回调响应结果
+	 * @param payload  回调数据对象,包含status、url、key、filetype等字段
+	 * @param response HTTP响应对象
+	 * @throws Exception 回调处理异常
 	 */
-	@SneakyThrows
 	@PostMapping("/callback")
 	@ApiOperation(value = "ONLYOFFICE保存回调", notes = "ONLYOFFICE文档编辑保存回调接口")
-	public Map<String, Object> officeCallback(@RequestBody OfficeCallbackDTO callbackDTO) {
-		log.info("ONLYOFFICE回调开始,回调数据: {}", JSON.toJSONString(callbackDTO));
-		Map<String, Object> result = new HashMap<>(4);
+	public void officeCallback(@RequestBody JSONObject payload, HttpServletResponse response) throws Exception {
+		log.info("ONLYOFFICE回调开始,回调数据: {}", JSON.toJSONString(payload));
 
-		// 步骤1:获取回调状态和文件信息
-		String status = callbackDTO.getStatus();
-		String fileKey = callbackDTO.getKey();
-		String fileDownloadUrl = callbackDTO.getUrl();
+		// 步骤1:获取回调状态码(核心逻辑判断依据:2/3/6 表示需要保存文档)
+		int status = payload.getIntValue("status");
+		log.info("回调状态status={}", status);
 
 		try {
-			// 步骤2:判断是否是保存操作(1=手动保存,2=强制保存)
-			if ("1".equals(status) || "2".equals(status)) {
-				// 步骤3:下载ONLYOFFICE返回的新文件
-				byte[] fileContent = downloadFile(fileDownloadUrl);
+			// 步骤2:判断是否需要保存文档
+			// status=2: 文档被关闭,所有用户都退出编辑,需要保存
+			// status=3: 文档保存出错时触发
+			// status=6: 文档强制保存(forcesave),编辑过程中定时或手动触发
+			if (status == 2 || status == 3 || status == 6) {
+				String downloadUrl = payload.getString("url");
+				String key = payload.getString("key");
+				log.info("回调key={}, 下载地址: {}", key, downloadUrl);
 
-				// 步骤4:获取文件名(可根据业务需求从数据库获取原始文件名)
-				String fileName = generateFileName(fileKey);
+				// 步骤3:检查文件URL是否为空
+				if (downloadUrl == null || downloadUrl.isEmpty()) {
+					log.warn("缺少文件URL,status={}, key={}", status, key);
+					sendErrorResponse(response, "缺少文件URL");
+					return;
+				}
 
-				// 步骤5:上传文件到OSS
-				BladeFile bladeFile = uploadToOss(fileName, fileContent);
-				log.info("ONLYOFFICE文件下载成功,文件名: {}, 文件大小: {}", fileName, fileContent.length);
-				// 步骤6:保存附件信息到数据库
-				Long attachId = saveAttachInfo(fileName, fileContent.length, bladeFile);
-
-				// 步骤7:记录操作日志(可根据业务需求扩展)
-				log.info("ONLYOFFICE文件保存成功,文件key: {}, 文件名: {}, 附件ID: {}, 下载URL: {}",
-					fileKey, fileName, attachId, bladeFile.getLink());
-
-				// 步骤8:业务扩展(可根据需求添加)
-				// - 更新数据库文件地址、版本号、更新时间
-				// - 记录操作日志、操作用户
-				// - 发送通知等
+				// 步骤4:处理保存回调
+				handleSaveCallback(payload, key, downloadUrl);
 			}
 
-			// 步骤9:成功必须返回 error:0
-			result.put("error", 0);
+			// 步骤5:返回OnlyOffice要求的规范响应(error:0 表示处理成功)
+			sendSuccessResponse(response);
 		} catch (Exception e) {
-			// 步骤10:异常处理,非0代表失败,编辑器会提示保存失败
-			log.error("ONLYOFFICE文件保存异常,文件key: {}, 异常信息: {}", fileKey, e.getMessage(), e);
-			result.put("error", 1);
-			result.put("message", "文件保存异常:" + e.getMessage());
+			log.error("ONLYOFFICE回调处理失败,status={}, 错误: {}", status, e.getMessage(), e);
+			// 步骤6:异常时返回 error:1
+			sendErrorResponse(response, e.getMessage());
 		}
+	}
 
-		return result;
+	/**
+	 * 发送成功响应给OnlyOffice
+	 *
+	 * @param response HTTP响应对象
+	 * @throws Exception IO异常
+	 */
+	private void sendSuccessResponse(HttpServletResponse response) throws Exception {
+		JSONObject responseJson = new JSONObject();
+		responseJson.put("error", 0);
+		response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+		response.getWriter().write(responseJson.toString());
+	}
+
+	/**
+	 * 发送错误响应给OnlyOffice
+	 *
+	 * @param response HTTP响应对象
+	 * @param message  错误消息
+	 * @throws Exception IO异常
+	 */
+	private void sendErrorResponse(HttpServletResponse response, String message) throws Exception {
+		JSONObject responseJson = new JSONObject();
+		responseJson.put("error", 1);
+		response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+		response.getWriter().write(responseJson.toString());
+	}
+
+	/**
+	 * 处理保存回调(status=2或status=6)
+	 * <p>
+	 * 从ONLYOFFICE服务器下载编辑后的文件,上传到OSS,
+	 * 并更新附件表中的link和md5字段。
+	 * </p>
+	 *
+	 * @param payload     回调数据对象
+	 * @param key         文档唯一标识,对应附件表的md5字段
+	 * @param downloadUrl 编辑后文件的下载地址
+	 * @throws Exception 下载、上传或更新数据库时的异常
+	 */
+	private void handleSaveCallback(JSONObject payload, String key, String downloadUrl) throws Exception {
+		// 1. 根据key(md5)查询附件记录
+		Attach attach = attachService.getOne(
+			Wrappers.lambdaQuery(Attach.class).eq(Attach::getMd5, key)
+		);
+		if (attach == null) {
+			log.warn("未找到key={}对应的附件记录,跳过保存", key);
+			return;
+		}
+		log.info("找到附件记录,attachId={}, name={}", attach.getId(), attach.getName());
+
+		// 2. 从ONLYOFFICE服务器下载编辑后的文件
+		// 对URL进行HTML实体解码(将&amp;转换为&)
+		String decodedUrl = downloadUrl.replace("&amp;", "&");
+		log.info("开始从ONLYOFFICE下载编辑后文件,解码后URL: {}", decodedUrl);
+		byte[] fileContent = downloadFile(decodedUrl);
+		log.info("文件下载完成,大小: {} bytes", fileContent.length);
+
+		// 3. 上传文件到OSS
+		String fileName = attach.getName() != null ? attach.getName() : key + ".docx";
+		BladeFile bladeFile = uploadToOss(fileName, fileContent);
+		log.info("文件上传OSS完成,新link: {}", bladeFile.getLink());
+
+		// 4. 更新附件记录的link和md5
+		String newLink = bladeFile.getLink();
+		String newMd5 = FileNameHarsher.generateFileHash(newLink);
+		attach.setLink(newLink);
+		attach.setMd5(newMd5);
+		attach.setName(fileName);
+		attach.setAttachSize((long) fileContent.length);
+		attachService.updateById(attach);
+		log.info("附件记录更新完成,attachId={}, 新md5={}", attach.getId(), newMd5);
 	}
 
 	/**

--
Gitblit v1.9.3