springboot+vue2整合onlyoffice实现文档在线协同编辑

Springboot+Vue2整合onlyoffice实现文档在线协同编辑

目录

  1. docker部署onlyoffice镜像
  2. vue2整合onlyoffice
  3. 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文件内容
<template>
  <div style="overflow: scroll;height: calc(100vh - 84px);">
    <div :id="onlineEditorId"></div>
  </div>
</template>
<script>
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;
    }
  }
}
</script>
<style scoped lang="scss">

</style>

  • (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.inversoft</groupId>
    <artifactId>prime-jwt</artifactId>
    <version>1.3.1</version>
</dependency>
# OnlyOfficeController.java内容

/**
* onlyoffice接口类
*/
@RestController
@RequestMapping("/only/office")
public class OnlyOfficeController {

    @Resource
    private OnlyOfficeConfig onlyOfficeConfig;
    
    @Resource
    private OnlyOfficeServiceImpl onlyOfficeService;
    
    private static final HashMap<String, List<String>> 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();
        HashMap<String, 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 List<String> users;
    private List<Map<String, 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 List<Change> 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 List<Integer> 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();
        List<String> 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官方文档

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乌拉队长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值