Springboot+Vue2整合onlyoffice实现文档在线协同编辑
目录
- docker部署onlyoffice镜像
- vue2整合onlyoffice
- springboot回调接口配置
1.docker部署onlyoffice
# 使用docker拉取并启动onlyoffice镜像
docker run -itd --name onlyoffice -p 10086:80 -e JWT_ENABLED=true -e JWT_SECRET=mlzhilu_secret onlyoffice/documentserver:8.0
注意:
- 自7.2版本以后,onlyoffice默认开启jwt,可以手动设置JWT_ENABLED=false以关闭jwt校验,但是关闭jwt校验可能导致链接不安全,本文默认使用8.0版本
- 如果启用jwt校验的话,需要手动设置secret,否则onlyoffice会随机生成secret,这种情况下就需要等容器启动后到容器内部的
/etc/onlyoffice/documentserver/local.json
文件中查看 - documentserver服务默认使用http链接,因此内部端口为80,同时也支持https链接,内部端口443,如需开启https,需要手动添加相应环境变量并生成SSL证书(请自行了解)
2.vue2整合onlyoffice
- (1)新建vue页面
onlyoffice/index.vue
# onlyoffice/index.vue文件内容
import {getOnlyOfficeConfig} from "@/api/documents/menu";
export default {
name: "OnlineEditor",
data() {
return {
// 文档ID
documentId: '',
// 文档版本号
versionId: '',
// 打开文件的方式,true-编辑模式,false-预览模式
isEdit: true,
docEditor: null,
onlineEditorId: 'onlineEditor',
}
},
watch: {
documentId: {
handler: function () {
this.loadScript();
this.initEditor();
},
deep: true,
},
},
activated() {
if (this.documentId) {
this.loadScript();
this.initEditor();
}
},
created() {
// 从路由中获取参数
const documentId = this.$route.query.documentId;
const versionId = this.$route.query.versionId;
const isEdit = this.$route.query.isEdit;
if (versionId) this.versionId = versionId;
if (isEdit) this.isEdit = isEdit;
if (documentId) {
this.documentId = documentId;
this.onlineEditorId += this.documentId;
this.loadScript();
this.initEditor();
}
},
methods: {
// 动态加载onlyoffice api脚本
async loadScript(){
const scriptId = "script_"+this.documentId;
if (document.getElementById(scriptId)===null){
const script = document.createElement('script')
script.id = scriptId;
script.src = "http://10.49.47.24:10086/web-apps/apps/api/documents/api.js"
script.type = "text/javascript"
document.head.appendChild(script);
}
},
// 初始化onlyoffice编辑器
async initEditor(){
const scriptId = "script_"+this.documentId;
if (document.getElementById(scriptId)===null){
await this.loadScript();
}
// 保证每次刷新页面时重新加载onlyoffice对象,避免缓存问题
if (this.docEditor){
this.docEditor.destroyEditor();
this.docEditor = null;
}
const param = {
documentId: this.documentId,
versionId: this.versionId,
isEdit: this.isEdit
}
// 从后端获取onlyoffice配置,避免配置被修改
await getOnlyOfficeConfig(param).then(res=>{
let data = res.data;
this.docEditor = new window.DocsAPI.DocEditor(this.onlineEditorId, data);
})
},
},
// 关闭页面销毁onlyoffice对象
beforeDestroy() {
if (this.docEditor){
this.docEditor.destroyEditor();
this.docEditor = null;
}
}
}
- (2)父组件页面路由调用
# 通过点击时间出发路由跳转,并传递参数
handleEdit(item){
const route = this.$router.resolve({
path: "/components/edit/office",
query: {
documentId: item.id,
isEdit: true
},
});
// 在新窗口打开页面
window.open(route.href, "_blank");
},
3.SpringBoot回调接口配置
为了保证onlyoffice配置不被修改,我这里将onlyoffice配置信息通过后端接口的形式获取,这里将onlyoffice配置信息配置在SpringBoot的配置文件中,如果不需要的话可以将这些配置直接写在前端的js代码中。
- (1) 在配置文件(如:application.yml或application.properties)中添加如下配置
# onlyoffice配置
only-office:
secret: devops_20240521
config:
document:
# 文档下载接口,这个接口需要在springboot后端中实现
url: http://10.49.47.24:10010/dev-api/documents/only/office/download
permissions:
# 是否可以编辑
edit: true
print: false
download: true
# 是否可以填写表格,如果将mode参数设置为edit,则填写表单仅对文档编辑器可用。 默认值与edit或review参数的值一致。
fillForms: false
# 跟踪变化
review: true
editorConfig:
# onlyoffice回调接口,这个接口也需要在springboot后端中实现
callbackUrl: http://10.49.47.24:10010/dev-api/documents/only/office/callbackToSaveFile
lang: zh-CN
coEditing:
mode: fast,
change: true
# 定制化配置
customization:
forcesave: true
autosave: false
comments: true
compactHeader: false
compactToolbar: false
compatibleFeatures: false
customer:
address: 中国北京市海淀区
info: xxxxx文档在线写作平台
logo: https://example.com/logo-big.png
logoDark: https://example.com/dark-logo-big.png
mail: xxx@xxx.com
name: xxxxx平台
phone: 123456789
www: www.example.com
features:
# 是否开启拼写检查
spellcheck:
mode: true
change: true
region: zh-CN
type: desktop
- (2)OnlyOfficeConfig配置类
# OnlyOfficeConfig.java内容
/**
* onlyOffice配置
* 这里的配置会从 application.yml或application.properties 中读取
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Configuration
@ConfigurationProperties(prefix = "only-office")
public class OnlyOfficeConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String secret;
private Config config;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config implements Serializable {
private static final long serialVersionUID = 1L;
private Document document;
private EditorConfig editorConfig;
private String type;
private String token;
private String documentType;
private String height = "100%";
private String width = "100%";
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Document implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private String fileType;
private String key;
private String url;
private Permissions permissions;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Permissions implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean edit;
private Boolean print;
private Boolean download;
private Boolean fillForms;
private Boolean review;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class EditorConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String callbackUrl;
private String lang;
private CoEditing coEditing;
private Customization customization;
private String region;
private User user;
public User getUser(){
return StringUtils.isNull(user)?new User():user;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CoEditing implements Serializable {
private static final long serialVersionUID = 1L;
private String mode;
private Boolean change;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Customization implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean forcesave;
private Boolean autosave;
private Boolean comments;
private Boolean compactHeader;
private Boolean compactToolbar;
private Boolean compatibleFeatures;
private Customer customer;
private Features features;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Customer implements Serializable {
private static final long serialVersionUID = 1L;
private String address;
private String info;
private String logo;
private String logoDark;
private String mail;
private String name;
private String phone;
private String www;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Features implements Serializable {
private static final long serialVersionUID = 1L;
private Spellcheck spellcheck;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Spellcheck implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean mode;
private Boolean change;
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String image;
private String group;
}
}
}
}
- (3)Controller接口
这里需要注意的是:在对onlyoffice配置进行jwt加密时需要用到一个依赖prime-jwt
,坐标如下:
dependency>
groupId>com.inversoftgroupId>
artifactId>prime-jwtartifactId>
version>1.3.1version>
dependency>
# OnlyOfficeController.java内容
/**
* onlyoffice接口类
*/
@RestController
@RequestMapping("/only/office")
public class OnlyOfficeController {
@Resource
private OnlyOfficeConfig onlyOfficeConfig;
@Resource
private OnlyOfficeServiceImpl onlyOfficeService;
private static final HashMapString, ListString>> extensionMap = new HashMap>();
// 初始化扩展名映射
static {
extensionMap.put("word", Arrays.asList(
"doc", "docm", "docx", "docxf", "dot", "dotm", "dotx", "epub", "fb2", "fodt", "htm", "html", "mht", "mhtml",
"odt", "oform", "ott", "rtf", "stw", "sxw", "txt", "wps", "wpt", "xml"
));
extensionMap.put("cell", Arrays.asList(
"csv", "et", "ett", "fods", "ods", "ots", "sxc", "xls", "xlsb", "xlsm", "xlsx", "xlt", "xltm", "xltx",
"xml"
));
extensionMap.put("slide", Arrays.asList(
"dps", "dpt", "fodp", "odp", "otp", "pot", "potm", "potx", "pps", "ppsm", "ppsx", "ppt", "pptm", "pptx",
"sxi"
));
extensionMap.put("pdf", Arrays.asList("djvu", "oxps", "pdf", "xps"));
}
/**
* onlyoffice回调接口,这个接口的内容基本不需要修改,
* 只需要修改 onlyOfficeService.handleCallbackResponse(callBackResponse);
* 及其方法中的业务逻辑即可
*/
@PostMapping(value = "/callbackToSaveFile")
public void callbackToSaveFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\A");
String body = scanner.hasNext() ? scanner.next() : "";
CallBackResponse callBackResponse = JSONObject.parseObject(body, CallBackResponse.class);
// 只需要修改这行代码及其业务逻辑即可
onlyOfficeService.handleCallbackResponse(callBackResponse);
writer.write("{"error":0}");
}
/**
* 文档下载接口
*/
@GetMapping("/download")
public void officeDownload(@RequestParam("documentId")@NotNull String documentId,
@RequestParam(value = "versionId", required = false) String versionId,
HttpServletResponse response)
{
onlyOfficeService.downloadFile(documentId, versionId, response);
}
/**
* 获取onlyoffice配置接口
*/
@GetMapping("/config")
public AjaxResult getOnlyOfficeConfig(String documentId, String versionId, Boolean isEdit){
DevelopDocumentVo developDocumentVo = developDocumentService.selectDevelopDocumentById(documentId);
if (StringUtils.isNull(developDocumentVo)) return error("文件不存在");
String fileName = developDocumentVo.getFileName();
OnlyOfficeConfig.Config config = onlyOfficeConfig.getConfig();
OnlyOfficeConfig.Config.Document document = config.getDocument();
OnlyOfficeConfig.Config configuration = new OnlyOfficeConfig.Config();
OnlyOfficeConfig.Config.Document documentConfig = new OnlyOfficeConfig.Config.Document();
documentConfig.setKey(documentId);
// 编辑模式
if (StringUtils.isNotNull(isEdit)&&isEdit) {
documentConfig.setTitle(fileName);
}else { // 预览模式
documentConfig.setTitle(StringUtils.format("{}({})", fileName, "预览模式"));
}
documentConfig.setFileType(this.getExtension(fileName));
OnlyOfficeConfig.Config.Document.Permissions permissions = config.getDocument().getPermissions();
if (StringUtils.isNotNull(isEdit)){
permissions.setEdit(isEdit);
permissions.setReview(false);
}
documentConfig.setPermissions(permissions);
String documentUrl = StringUtils.isEmpty(versionId)
?StringUtils.format("{}?documentId={}", document.getUrl(), documentId)
:StringUtils.format("{}?documentId={}&versionId={}", document.getUrl(), documentId, versionId);
documentConfig.setUrl(documentUrl);
Long userId = SecurityUtils.getUserId();
SysUser sysUser = SecurityUtils.getLoginUser().getSysUser();
OnlyOfficeConfig.Config.EditorConfig editorConfig = config.getEditorConfig();
OnlyOfficeConfig.Config.EditorConfig.User user = editorConfig.getUser();
user.setId(String.valueOf(userId));
user.setName(sysUser.getNickName());
user.setImage(sysUser.getAvatar());
editorConfig.setUser(user);
configuration.setEditorConfig(editorConfig);
configuration.setDocumentType(this.getDocumentType(fileName));
configuration.setDocument(documentConfig);
String secret = onlyOfficeConfig.getSecret();
HashMapString, Object> claims = new HashMap>();
claims.put("document", documentConfig);
claims.put("editorConfig", editorConfig);
claims.put("documentType", this.getDocumentType(fileName));
claims.put("type", configuration.getType());
Signer signer = HMACSigner.newSHA256Signer(secret);
JWT jwt = new JWT();
for (String key : claims.keySet())
{
jwt.addClaim(key, claims.get(key));
}
String token = JWT.getEncoder().encode(jwt, signer);
configuration.setToken(token);
configuration.setType(config.getType());
return success(configuration);
}
}
- (4)CallBackResponse实体类
# CallBackResponse.java内容
/**
* onlyOffice回调响应参数实体
* 数据格式:
* {
* "key": "1797934023043756034",
* "status": 6,
* "url": "http://10.x.xx.42:10020/cache/files/data/179793402xxx6034_5182/output.docx/output.docx?md5=w6_C_mPuu6uWt7jsYURmWg&expires=1717572948&WOPISrc=179793402xxx6034&filename=output.docx",
* "changesurl": "http://10.x.xx.42:10020/cache/files/data/179793xxxx3756034_5182/changes.zip/changes.zip?md5=8lYUI4TD1s2bW-pzs_akgQ&expires=1717572948&WOPISrc=1797934023xxx56034&filename=changes.zip",
* "history": {
* "serverVersion": "8.0.1",
* "changes": [
* {
* "created": "2024-06-05 07:20:01",
* "user": {
* "id": "2",
* "name": "mlzhilu"
* }
* },
* {
* "created": "2024-06-05 07:20:44",
* "user": {
* "id": "1",
* "name": "超级管理员"
* }
* }
* ]
* },
* "users": [
* "1"
* ],
* "actions": [
* {
* "type": 2,
* "userid": "1"
* }
* ],
* "lastsave": "2024-06-05T07:20:45.000Z",
* "forcesavetype": 1,
* "token": "eyJhbGciOiJIU......-53bhhSRg",
* "filetype": "docx"
* }
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class CallBackResponse {
private String key;
private int status;
private String url;
@JsonProperty("changesurl")
private String changesUrl;
private History history;
private ListString> users;
private ListMapString, Object>> actions;
@JsonProperty("lastsave")
private Date lastSave;
@JsonProperty("forcesavetype")
private int forceSaveType;
private String token;
private String filetype;
// History 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class History {
private String serverVersion;
private ListChange> changes;
// Change 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class Change {
private Date created;
private User user;
// User 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class User {
private String id;
private String name;
}
}
}
}
- (5)ServiceImpl接口
# OnlyOfficeServiceImpl.java内容
@Service
public class OnlyOfficeServiceImpl {
// 文档关闭标志位(2和3均表示文档关闭)
// 强制保存文档标志位(6和7均表示强制保存文档)
private final static ListInteger> DOCUMENT_SAVE_STATUS_LIST = Arrays.asList(2, 3, 6, 7);
public void handleCallbackResponse(CallBackResponse callBackResponse){
String documentId = callBackResponse.getKey();
int status = callBackResponse.getStatus();
String url = callBackResponse.getUrl();
ListString> users = callBackResponse.getUsers();
//保存文档逻辑
if (
DOCUMENT_SAVE_STATUS_LIST.contains(status)
&&StringUtils.isNotEmpty(url)
&&!users.isEmpty()
&&StringUtils.isNotEmpty(documentId)
) {
// TODO 这里主要是根据onlyoffice服务器中响应的临时文件下载链接,去下载文件并做一些自己的业务处理
}
}
/*
* 文档下载业务
* 这个接口中文档需要通过HttpServletResponse返回文件
*/
public void downloadFile(String id, String versionId, HttpServletResponse response)
{
// TODO 这里主要是根据文档ID和文档版本ID提供文档下载的功能,并且需要保证下载文档时是以文档流的形式下载的
}
}
引用
onlyoffice官方文档