ARTICLE · 1075658
SpringBoot + OnlyOffice:在线编辑 Word,真正难的是保存
结果去对象存储里一看,原文件一个字都没变。
第一次接 OnlyOffice,这个地方很容易判断错。编辑器里的保存,只代表内容暂存在 OnlyOffice Document Server,业务系统想拿到最终文件,还得老老实实接住它的回调。
整个链路其实就四步:
浏览器向 SpringBoot 获取编辑配置,OnlyOffice 根据配置下载原始文件,用户在线编辑,编辑完成后 OnlyOffice 再调用 SpringBoot,把修改后的文件地址交回来。
这里我一般先检查两个地址:
document.url
editorConfig.callbackUrl
这两个地址必须是 OnlyOffice 服务器能访问的地址。你在浏览器里能打开,不代表部署在 Docker 或内网机器里的 Document Server 也能打开。大量“下载文件失败”“回调没有日志”,最后查出来都是网络和地址写错了。
生成编辑配置
SpringBoot 不需要处理 Word 内容,只负责生成一份编辑器配置。
@RestController
@RequestMapping("/api/office")
publicclassOfficeEditorController{
privatefinal FileRepository fileRepository;
privatefinal OfficeTokenService tokenService;
publicOfficeEditorController(FileRepository fileRepository,
OfficeTokenService tokenService){
this.fileRepository = fileRepository;
this.tokenService = tokenService;
}
@GetMapping("/config/{fileId}")
public Map<String, Object> config(@PathVariable Long fileId,
LoginUser loginUser){
StoredFile file = fileRepository.mustFind(fileId);
String documentKey = file.id() + "_" + file.version();
Map<String, Object> config = new LinkedHashMap<>();
config.put("documentType", "word");
config.put("document", Map.of(
"fileType", "docx",
"key", documentKey,
"title", file.originalName(),
"url", "https://file.example.com/api/files/"
+ file.id() + "/download?ticket=" + file.readTicket()
));
config.put("editorConfig", Map.of(
"mode", "edit",
"lang", "zh",
"callbackUrl", "https://app.example.com/api/office/callback/"
+ file.id(),
"user", Map.of(
"id", String.valueOf(loginUser.id()),
"name", loginUser.displayName()
)
));
config.put("token", tokenService.sign(config));
return config;
}
}
key 不能随手拿文件名顶上。OnlyOffice 会用它识别文档和编辑缓存,同一份内容使用同一个 key,文件版本变化后就要生成新 key,否则可能打开旧缓存,甚至串到别的文档。官方也要求不同文档使用唯一 key,并限制长度不超过 128 个字符。
我一般直接用“文件 ID + 版本号”,省得拿 MD5、时间戳到处拼。保存成功后版本号加一,下次打开自然就是新 key。
前端拿到配置后初始化编辑器即可:
const config = await fetch(`/api/office/config/${fileId}`).then(r => r.json());
new DocsAPI.DocEditor("office-editor", {
...config,
width: "100%",
height: "100%"
});
保存别直接覆盖原文件
OnlyOffice 会通过 callbackUrl 通知文档状态。状态 2 表示文档已结束编辑并准备保存,状态 6 表示强制保存产生了当前版本;回调接口必须返回 {"error":0},否则编辑器会认为保存失败。
public record OfficeCallback(
int status,
String key,
String url,
String filetype
){
}
@PostMapping("/callback/{fileId}")
public Map<String, Integer> callback(@PathVariable Long fileId,
@RequestBody OfficeCallback callback,
HttpServletRequest request){
officeTokenVerifier.verifyCallback(request, callback);
if (callback.status() != 2 && callback.status() != 6) {
return Map.of("error", 0);
}
Path temporaryFile = fileStorage.createTemporary(fileId);
try {
onlyOfficeDownloader.download(callback.url(), temporaryFile);
fileStorage.replaceAtomically(
fileId,
temporaryFile,
callback.filetype()
);
fileRepository.increaseVersion(fileId);
return Map.of("error", 0);
} catch (Exception ex) {
log.error("OnlyOffice保存失败,fileId={}, key={}",
fileId, callback.key(), ex);
return Map.of("error", 1);
} finally {
fileStorage.deleteQuietly(temporaryFile);
}
}
这段代码有个细节:先下载到临时文件,校验成功后再原子替换。
不要拿回调里的 URL 直接覆盖原文件。下载到一半网络断了,原文件也被你清空了,这种事故比“保存失败”难收拾得多。
另外,状态 2 通常是在最后一个编辑者关闭文档后触发,并不是用户每敲一个字就回调;开启强制保存后,状态 6 才会在保存动作发生时返回当前版本。
Word 转 PDF
格式转换不要自己装 LibreOffice 起进程,OnlyOffice 已经提供了转换接口。当前文档建议向 /converter 发送 JSON 请求,旧文章里常见的 /ConvertService.ashx 是早期地址。
public URI convertToPdf(StoredFile file){
String convertKey = file.id() + "_" + file.version() + "_pdf";
Map<String, Object> command = Map.of(
"async", false,
"filetype", "docx",
"outputtype", "pdf",
"key", convertKey,
"title", file.originalName(),
"url", file.publicDownloadUrl()
);
ConvertResult result = restClient.post()
.uri(documentServer + "/converter?shardkey={key}", convertKey)
.body(Map.of("token", tokenService.sign(command)))
.retrieve()
.body(ConvertResult.class);
if (result == null || !result.endConvert()) {
thrownew IllegalStateException("Word 转 PDF 未完成");
}
return URI.create(result.fileUrl());
}
public record ConvertResult(
boolean endConvert,
String fileUrl,
Integer error
){
}
转换返回的 fileUrl 也只是临时结果地址。业务系统拿到后要及时下载,再存进自己的文件系统或对象存储,别把这个地址直接写进数据库长期使用。
生产环境还得补三件事:JWT 校验、短期下载凭证、回调幂等。
尤其是回调里的 url,不能什么地址都下载。至少限制 OnlyOffice 服务域名,否则这个接口稍微写松一点,就可能被利用去探测内网。
OnlyOffice 负责的是“编辑器”和“文档转换”,文件权限、版本号、历史记录、最终落盘仍然是 SpringBoot 的活。把 url、key、callbackUrl 这条链路接稳,在线 Word 编辑也就没多少玄学了。