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.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.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.entity.Attach; import org.sxkj.resource.service.IAttachService; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; /** * 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 保存回调接口 *

* 处理ONLYOFFICE文档编辑保存回调,支持以下状态: * status=2: 文档编辑完成,关闭编辑器时触发 * status=3: 文档保存出错时触发 * status=6: 文档强制保存(forcesave)时触发 *

*

* 对应前端 config.editorConfig.callbackUrl = "http://192.168.1.33:8310/office/callback" *

* * @param payload 回调数据对象,包含status、url、key、filetype等字段 * @param response HTTP响应对象 * @throws Exception 回调处理异常 */ @PostMapping("/callback") @ApiOperation(value = "ONLYOFFICE保存回调", notes = "ONLYOFFICE文档编辑保存回调接口") public void officeCallback(@RequestBody JSONObject payload, HttpServletResponse response) throws Exception { log.info("ONLYOFFICE回调开始,回调数据: {}", JSON.toJSONString(payload)); // 步骤1:获取回调状态码(核心逻辑判断依据:2/3/6 表示需要保存文档) int status = payload.getIntValue("status"); log.info("回调状态status={}", status); try { // 步骤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); // 步骤3:检查文件URL是否为空 if (downloadUrl == null || downloadUrl.isEmpty()) { log.warn("缺少文件URL,status={}, key={}", status, key); sendErrorResponse(response, "缺少文件URL"); return; } // 步骤4:处理保存回调 handleSaveCallback(payload, key, downloadUrl); } // 步骤5:返回OnlyOffice要求的规范响应(error:0 表示处理成功) sendSuccessResponse(response); } catch (Exception e) { log.error("ONLYOFFICE回调处理失败,status={}, 错误: {}", status, e.getMessage(), e); // 步骤6:异常时返回 error:1 sendErrorResponse(response, e.getMessage()); } } /** * 发送成功响应给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) *

* 从ONLYOFFICE服务器下载编辑后的文件,上传到OSS, * 并更新附件表中的link和md5字段。 *

* * @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实体解码(将&转换为&) String decodedUrl = downloadUrl.replace("&", "&"); 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(bladeFile.getName()); attach.setAttachSize((long) fileContent.length); attachService.updateById(attach); log.info("附件记录更新完成,attachId={}, 新md5={}", attach.getId(), newMd5); } /** * 从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"; } }