From 79c2d5fa54ca680f5fc439b3607d7f0482a2ff32 Mon Sep 17 00:00:00 2001
From: linwei <872216696@qq.com>
Date: Wed, 08 Jul 2026 19:55:17 +0800
Subject: [PATCH] feat(office): 添加ONLYOFFICE回调处理功能

---
 drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeUserDTO.java               |   35 +++++++
 drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java |  189 +++++++++++++++++++++++++++++++++++++
 drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeCallbackDTO.java           |   72 ++++++++++++++
 3 files changed, 296 insertions(+), 0 deletions(-)

diff --git a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeCallbackDTO.java b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeCallbackDTO.java
new file mode 100644
index 0000000..4894d5f
--- /dev/null
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeCallbackDTO.java
@@ -0,0 +1,72 @@
+package org.sxkj.resource.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * ONLYOFFICE 回调数据传输对象
+ *
+ * @author lw
+ * @since 2026-07-08
+ */
+@Data
+public class OfficeCallbackDTO implements Serializable {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 文件唯一标识key
+	 */
+	@ApiModelProperty(value = "文件唯一标识key")
+	private String key;
+
+	/**
+	 * 状态码(0=正在编辑,1=文档保存准备好,2=强制保存,3=发送邮件失败,4=关闭无更改,6=正在编辑但当前文档保存状态,7=强制保存时发生错误)
+	 */
+	@ApiModelProperty(value = "状态码")
+	private String status;
+
+	/**
+	 * 文件下载URL(status=1或2时有值)
+	 */
+	@ApiModelProperty(value = "文件下载URL")
+	private String url;
+
+	/**
+	 * 文件版本变化标识(status=1或2时有值)
+	 */
+	@ApiModelProperty(value = "文件版本变化标识")
+	private String changesurl;
+
+	/**
+	 * 文件历史记录版本key列表
+	 */
+	@ApiModelProperty(value = "文件历史记录版本key列表")
+	private List<String> history;
+
+	/**
+	 * 用户信息列表
+	 */
+	@ApiModelProperty(value = "用户信息列表")
+	private List<OfficeUserDTO> users;
+
+	/**
+	 * 用户操作标识
+	 */
+	@ApiModelProperty(value = "用户操作标识")
+	private String userdata;
+
+	/**
+	 * 最后修改的用户ID
+	 */
+	@ApiModelProperty(value = "最后修改的用户ID")
+	private String lastsave;
+
+	/**
+	 * 强制保存类型(0=手动保存,1=周期性自动保存,2=服务器端保存)
+	 */
+	@ApiModelProperty(value = "强制保存类型")
+	private Integer forcesavetype;
+}
\ No newline at end of file
diff --git a/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeUserDTO.java b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeUserDTO.java
new file mode 100644
index 0000000..f734c63
--- /dev/null
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/dto/OfficeUserDTO.java
@@ -0,0 +1,35 @@
+package org.sxkj.resource.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * ONLYOFFICE 用户信息传输对象
+ *
+ * @author lw
+ * @since 2026-07-08
+ */
+@Data
+public class OfficeUserDTO implements Serializable {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 用户ID
+	 */
+	@ApiModelProperty(value = "用户ID")
+	private String id;
+
+	/**
+	 * 用户名称
+	 */
+	@ApiModelProperty(value = "用户名称")
+	private String name;
+
+	/**
+	 * 用户邮箱
+	 */
+	@ApiModelProperty(value = "用户邮箱")
+	private String email;
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..19d8565
--- /dev/null
+++ b/drone-ops/drone-resource/src/main/java/org/sxkj/resource/endpoint/OfficeCallbackEndpoint.java
@@ -0,0 +1,189 @@
+package org.sxkj.resource.endpoint;
+
+import com.alibaba.fastjson.JSON;
+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;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.springblade.core.oss.model.BladeFile;
+import org.springblade.core.tenant.annotation.NonDS;
+import org.springblade.core.tool.utils.FileUtil;
+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.resource.builder.OssBuilder;
+import org.sxkj.resource.dto.OfficeCallbackDTO;
+import org.sxkj.resource.entity.Attach;
+import org.sxkj.resource.service.IAttachService;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ONLYOFFICE 回调端点
+ *
+ * @author lw
+ * @since 2026-07-08
+ */
+@Slf4j
+@NonDS
+@RestController
+@AllArgsConstructor
+@RequestMapping("/office")
+@Api(value = "ONLYOFFICE回调端点", tags = "ONLYOFFICE回调接口")
+public class OfficeCallbackEndpoint {
+
+	/**
+	 * 对象存储构建类
+	 */
+	private final OssBuilder ossBuilder;
+
+	/**
+	 * 附件表服务
+	 */
+	private final IAttachService attachService;
+
+	/**
+	 * ONLYOFFICE 保存回调接口
+	 * 对应前端 config.editorConfig.callbackUrl = "http://localhost:8080/office/callback"
+	 *
+	 * @param callbackDTO 回调数据对象
+	 * @return 回调响应结果
+	 */
+	@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);
+
+		// 步骤1:获取回调状态和文件信息
+		String status = callbackDTO.getStatus();
+		String fileKey = callbackDTO.getKey();
+		String fileDownloadUrl = callbackDTO.getUrl();
+
+		try {
+			// 步骤2:判断是否是保存操作(1=手动保存,2=强制保存)
+			if ("1".equals(status) || "2".equals(status)) {
+				// 步骤3:下载ONLYOFFICE返回的新文件
+				byte[] fileContent = downloadFile(fileDownloadUrl);
+
+				// 步骤4:获取文件名(可根据业务需求从数据库获取原始文件名)
+				String fileName = generateFileName(fileKey);
+
+				// 步骤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:业务扩展(可根据需求添加)
+				// - 更新数据库文件地址、版本号、更新时间
+				// - 记录操作日志、操作用户
+				// - 发送通知等
+			}
+
+			// 步骤9:成功必须返回 error:0
+			result.put("error", 0);
+		} catch (Exception e) {
+			// 步骤10:异常处理,非0代表失败,编辑器会提示保存失败
+			log.error("ONLYOFFICE文件保存异常,文件key: {}, 异常信息: {}", fileKey, e.getMessage(), e);
+			result.put("error", 1);
+			result.put("message", "文件保存异常:" + e.getMessage());
+		}
+
+		return result;
+	}
+
+	/**
+	 * 从ONLYOFFICE服务器下载文件
+	 *
+	 * @param downloadUrl 文件下载URL
+	 * @return 文件内容字节数组
+	 * @throws Exception 下载异常
+	 */
+	private byte[] downloadFile(String downloadUrl) throws Exception {
+		// 步骤1:创建HTTP客户端
+		try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+			// 步骤2:构建GET请求
+			HttpGet httpGet = new HttpGet(downloadUrl);
+
+			// 步骤3:执行请求并获取响应
+			try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+				// 步骤4:获取响应内容流并读取到字节数组
+				InputStream inputStream = response.getEntity().getContent();
+				return inputStream.readAllBytes();
+			}
+		}
+	}
+
+	/**
+	 * 上传文件到OSS
+	 *
+	 * @param fileName    文件名
+	 * @param fileContent 文件内容
+	 * @return BladeFile OSS文件信息
+	 * @throws Exception 上传异常
+	 */
+	private BladeFile uploadToOss(String fileName, byte[] fileContent) throws Exception {
+		// 步骤1:创建输入流
+		InputStream inputStream = new java.io.ByteArrayInputStream(fileContent);
+
+		// 步骤2:使用ossBuilder上传文件到指定存储桶(estack)
+		return ossBuilder.template("estack").putFile(fileName, inputStream);
+	}
+
+	/**
+	 * 保存附件信息到数据库
+	 *
+	 * @param fileName   文件名
+	 * @param fileSize   文件大小
+	 * @param bladeFile  OSS文件信息
+	 * @return 附件ID
+	 */
+	private Long saveAttachInfo(String fileName, int fileSize, BladeFile bladeFile) {
+		// 步骤1:获取文件扩展名
+		String fileExtension = FileUtil.getFileExtension(fileName);
+
+		// 步骤2:构建附件实体
+		Attach attach = new Attach();
+		attach.setDomainUrl(bladeFile.getDomain());
+		attach.setLink(bladeFile.getLink());
+		attach.setName(bladeFile.getName());
+		attach.setOriginalName(bladeFile.getOriginalName());
+		attach.setAttachSize(Long.valueOf(fileSize));
+		attach.setExtension(fileExtension);
+
+		// 步骤3:保存附件信息
+		attachService.save(attach);
+
+		// 步骤4:返回附件ID
+		return attach.getId();
+	}
+
+	/**
+	 * 根据文件key生成文件名
+	 *
+	 * @param fileKey 文件唯一标识
+	 * @return 文件名
+	 */
+	private String generateFileName(String fileKey) {
+		// 步骤1:可根据业务需求从数据库查询原始文件名
+		// FileEntity file = fileMapper.selectByKey(fileKey);
+		// return file.getFileName();
+
+		// 步骤2:默认使用fileKey作为文件名(可根据业务修改)
+		return fileKey + ".docx";
+	}
+}

--
Gitblit v1.9.3