前言
在许多项目中,会遇到需要在前端页面直接编辑Word文档并保存的需求。经过调研,我们选择了功能强大的开源组件OnlyOffice来实现在线编辑功能。OnlyOffice不仅支持文档编辑,还具备文档格式转换、多人协同编辑和打印等功能,本文主要聚焦于文档编辑与保存的核心实现。
1、OnlyOffice的部署
部署OnlyOffice主要有两种方式:Docker部署和本地直接安装。相比之下,Docker部署更为简便,只需拉取镜像并配置启动参数即可。由于最初搜索到的是Linux本地部署方案,本文采用了第二种方式。以下是两种部署方式的参考链接:
2、代码逻辑开发
本方案采用前后端分离架构,前端使用基于Vue的Element UI框架,后端采用SpringBoot框架。
2.1、前端代码
关键步骤:
-
引入官方JS SDK:在前端页面中引入OnlyOffice提供的API JS文件。
<div id="placeholder"></div>
注意:需将 your-doc-server 替换为实际部署的OnlyOffice文档服务器地址。
-
初始化编辑器配置:按照官方API配置编辑器参数。
const config = {
document: {
mode: 'edit',
fileType: 'docx',
key: String(Math.floor(Math.random() * 10000)), // 文档唯一标识
title: route.query.name + '.docx',
url: import.meta.env.VITE_APP_API_URL + `/getFile/${route.query.id}`, // 获取文档的后端接口
permissions: {
comment: true,
download: true,
modifyContentControl: true,
modifyFilter: true,
edit: true,
fillForms: true,
review: true,
},
},
documentType: 'word',
editorConfig: {
user: { // 当前用户信息
id: 'liu',
name: 'liu',
},
customization: { // 自定义配置
plugins: false,
forcesave: true, // 启用强制保存
},
lang: 'zh',
callbackUrl: import.meta.env.VITE_APP_API_URL + `/callback`, // 关键:回调接口地址
},
height: '100%',
width: '100%',
};
new window.DocsAPI.DocEditor('onlyoffice', config);
url: 后端提供文档下载流的接口地址,例如 http://192.168.123.123:8089/getFile/12。
callbackUrl: 核心回调接口,文档的任何操作(如保存)都会触发对该地址的请求。
2.2、后端代码
后端主要提供两个核心接口:/getFile/{id} 用于提供文档流,/callback 用于接收OnlyOffice服务器的回调通知(如保存文档)。
1. Maven依赖
<!-- httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
2. 核心控制器 (OnlyOfficeController)
package com.ruoyi.web.controller.meetingminutes.onlyoffice;
import com.ruoyi.system.domain.MeetingTable;
import com.ruoyi.system.service.IMeetingTableService;
import com.ruoyi.web.controller.meetingminutes.utils.HttpsKitWithProxyAuth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Collections;
@RestController
public class OnlyOfficeController {
@Autowired
private IMeetingTableService meetingTableService;
private String meetingMinutesFilePath; // 文档存储路径
/**
* 接口1:根据会议ID获取对应的Word文档流
*/
@GetMapping("/getFile/{meeting_id}")
public ResponseEntity<byte[]> getFile(HttpServletResponse response, @PathVariable Long meeting_id) throws IOException {
MeetingTable meetingTable = meetingTableService.selectMeetingTableById(meeting_id);
meetingMinutesFilePath = meetingTable.getMeetingMinutesFilePath();
if (meetingMinutesFilePath == null || "".equals(meetingMinutesFilePath)) {
return null; // 无文档时返回null
}
File file = new File(meetingMinutesFilePath);
try (FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream fis = new BufferedInputStream(fileInputStream)) {
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", URLEncoder.encode(file.getName(), "UTF-8"));
return new ResponseEntity<>(buffer, headers, HttpStatus.OK);
} catch (Exception e) {
throw new RuntimeException("文件读取失败", e);
}
}
/**
* 接口2:处理OnlyOffice服务器的回调请求
*/
@CrossOrigin(origins = "*")
@PostMapping("/callback")
public ResponseEntity<Object> handleCallback(@RequestBody CallbackData callbackData) {
// 根据回调状态码处理不同逻辑
Integer status = callbackData.getStatus();
switch (status) {
case 2: // 文档已准备好保存
case 6: // 编辑中但当前状态已保存
System.out.println("文档准备保存,开始下载...");
String url = callbackData.getUrl(); // OnlyOffice提供的临时文件下载地址
try {
saveFile(url); // 下载并覆盖原文件
} catch (Exception e) {
System.out.println("保存文件异常");
}
System.out.println("保存成功.");
break;
case 3:
System.out.println("文档保存出错");
break;
case 4:
System.out.println("文档关闭,无更改");
break;
case 7:
System.out.println("强制保存文档出错");
break;
default:
break;
}
// 必须返回固定格式的响应
return ResponseEntity.ok(Collections.singletonMap("error", 0));
}
/**
* 从回调URL下载最新文档,覆盖本地文件
*/
public void saveFile(String downloadUrl) throws URISyntaxException, IOException {
HttpsKitWithProxyAuth.downloadFile(downloadUrl, meetingMinutesFilePath);
}
/**
* 回调数据实体类
*/
@Setter
@Getter
public static class CallbackData {
private Integer status; // 状态码 (1:编辑中,2:待保存,3:保存错误,4:无更改关闭,6:编辑中已保存,7:强制保存错误)
private String url; // 最新文档下载地址 (status=2,3,6,7时有值)
private String key; // 文档唯一标识
// ... 其他字段省略,可根据需要扩展
}
}
3. 工具类 (HttpsKitWithProxyAuth)
该类是一个支持HTTP/HTTPS及代理的通用文件下载工具,核心方法 downloadFile 用于从回调的 url 下载文档并保存到指定路径。由于代码较长,此处仅说明其作用,具体实现可参考网络资源或根据项目HTTP客户端库(如RestTemplate、OkHttp)进行简化。
3、问题与解决方案
3.1、服务访问失败
部署完成后,如果无法访问示例页面(example),请检查服务是否成功启动。
systemctl status ds* # 查看所有OnlyOffice相关服务状态
3.2、文档加载或保存失败
此问题多与安全配置(Token验证、IP过滤)有关。需要修改OnlyOffice文档服务器的配置文件。
- 配置文件位置:
/etc/onlyoffice/documentserver/
- 修改
local.json:将 token 相关配置的 enable 设置为 false,以禁用Token验证。
"token": {
"enable": {
"request": {
"inbox": false,
"outbox": false
},
"browser": false
}
}
- 修改
default.json:
- 将
request-filtering-agent 下的 allowPrivateIPAddress 和 allowMetaIPAddress 设为 true,允许私有IP访问。
- 同样将
token 的 enable 设置为 false。
- 将
rejectUnauthorized (通常在 ssl 配置下) 设为 false(仅测试环境,生产环境请谨慎)。
- 修改配置后,重启OnlyOffice服务。
3.3、业务系统自身的鉴权拦截
如果您的SpringBoot应用本身通过Spring Security等组件进行了接口鉴权(如JWT Token),需要确保OnlyOffice回调接口(/callback)和文档获取接口(/getFile/*)能被匿名访问。
在Spring Security配置中,将这些路径加入白名单:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/callback", "/getFile/**").permitAll() // 放行OnlyOffice相关接口
.anyRequest().authenticated()
... // 其他配置
}
3.4、使用公网文档链接测试
在调试阶段,可以直接使用一个公网可访问的.docx链接来快速验证OnlyOffice服务是否部署成功。将前端配置中的 url 替换为测试链接即可:
url: 'https://d2nlctn12v279m.cloudfront.net/assets/docs/samples/zh/demo.docx',
4、总结
通过本文介绍的步骤,可以完成SpringBoot项目与OnlyOffice文档服务器的整合,实现前端在线编辑Word文档并自动保存回后端服务器的功能。整个过程涉及服务部署、前后端API对接和回调处理,特别是在Docker或Linux环境下部署OnlyOffice时,注意网络和权限配置是成功的关键。希望这篇实践指南能帮助你顺利实现需求。