| | |
| | | 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; |
| | |
| | | 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 回调端点 |
| | |
| | | |
| | | /** |
| | | * 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实体解码(将&转换为&) |
| | | 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(fileName); |
| | | attach.setAttachSize((long) fileContent.length); |
| | | attachService.updateById(attach); |
| | | log.info("附件记录更新完成,attachId={}, 新md5={}", attach.getId(), newMd5); |
| | | } |
| | | |
| | | /** |