SpringBoot 秒实现在线 Word 编辑、协同、转化等功能

最近有个项目需求是实现前端页面可以对word文档进行编辑,并且可以进行保存,于是一顿搜索,找到开源第三方onlyoffice,实际上onlyOffice有很多功能,例如文档转化、多人协同编辑文档、文档打印等,我们只用到了文档编辑功能。

01

OnlyOffice的部署

部署分为docker部署方式和本地直接安装的方式,比较两种部署方式,docker是比较简单的一种,因为只要拉取相关镜像,然后启动时配置好对应的配置文件即可。

由于搜索的时候先看到的是linux本地部署,所以采用了第二种方式,下面我将给出两个参考博客:

Docker部署方式(未尝试)

https://blog.csdn.net/wangchange/article/details/140185623

Ubuntu部署方式(已验证可行)

https://blog.csdn.net/qq_36437991/article/details/139859247

02

代码逻辑开发

前端使用的element框架vue版本,后端采用springboot

2.1、前端代码

参考官方文档API:

首先记得添加下面的js文件:

复制代码
<div id="placeholder"></div>
<script type="text/javascript" src="https://documentserver/web-apps/apps/api/documents/api.js"></script>

注意 :记得将 documentserver 替换为部署onlyoffice的地址。

配置代码:

复制代码
constconfig= {
    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%',
}

newwindow.DocsAPI.DocEditor('onlyoffice', config)

说明 :

import.meta.env.VITEAPPAPI_URL

为你实际的后端地址,格式: http://ip:端口号/访问路径

例如: http://192.168.123.123:8089/getFile/12 ,其中12为会议号,用于得到文件地址

callbackUrl

为回调函数,即文档有什么操作后,都会通过这个函数进行回调,例如:编辑保存操作。

2.2、后端代码

POM依赖

复制代码
<!-- httpclient start -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
</dependency>

OnlyOfficeController

复制代码
@Api(value = "OnlyOfficeController")
@RestController
publicclass OnlyOfficeController {

    @Autowired
    private IMeetingTableService meetingTableService;

    private String meetingMinutesFilePath;

    /**
     * 传入参数 会议id,得到会议纪要文件流,并进行打开
     */
    @ApiOperation(value = "OnlyOffice")
    @GetMapping("/getFile/{meeting_id}")
    public ResponseEntity<byte[]> getFile(HttpServletResponse response, @PathVariable Long meeting_id) 
            throws IOException {
        MeetingTablemeetingTable= meetingTableService.selectMeetingTableById(meeting_id);
        meetingMinutesFilePath = meetingTable.getMeetingMinutesFilePath();

        if (meetingMinutesFilePath == null || "".equals(meetingMinutesFilePath)) {
            returnnull;   // 当会议纪要文件为空的时候,就返回null
        }

        Filefile=newFile(meetingMinutesFilePath);
        FileInputStreamfileInputStream=null;
        InputStreamfis=null;

        try {
            fileInputStream = newFileInputStream(file);
            fis = newBufferedInputStream(fileInputStream);
            byte[] buffer = newbyte[fis.available()];
            fis.read(buffer);
            fis.close();

            HttpHeadersheaders=newHttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", 
                URLEncoder.encode(file.getName(), "UTF-8"));

            returnnew ResponseEntity<>(buffer, headers, HttpStatus.OK);
        } catch (Exception e) {
            thrownew RuntimeException("e -> ", e);
        } finally {
            try {
                if (fis != null) fis.close();
            } catch (Exception e) {
                // ignore
            }
            try {
                if (fileInputStream != null) fileInputStream.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    @CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS})
    @PostMapping("/callback")
    public ResponseEntity<Object> handleCallback(@RequestBody CallbackData callbackData) {

        // 状态监听
        // 参见 https://api.onlyoffice.com/editors/callback
        Integerstatus= callbackData.getStatus();

        switch (status) {
            case1: {
                // document is being edited  文档已经被编辑
                break;
            }
            case2: {
                // document is ready for saving, 文档已准备好保存
                System.out.println("document is ready for saving");
                Stringurl= callbackData.getUrl();
                try {
                    saveFile(url); // 保存文件
                } catch (Exception e) {
                    System.out.println("保存文件异常");
                }
                System.out.println("save success.");
                break;
            }
            case3: {
                // document saving error has occurred, 保存出错
                System.out.println("document saving error has occurred, 保存出错");
                break;
            }
            case4: {
                // document is closed with no changes, 未保存退出
                System.out.println("document is closed with no changes, 未保存退出");
                break;
            }
            case6: {
                // document is being edited, but the current document state is saved, 编辑保存
                Stringurl= callbackData.getUrl();
                try {
                    saveFile(url); // 保存文件
                } catch (Exception e) {
                    System.out.println("保存文件异常");
                }
                System.out.println("save success.");
            }
            case7: {
                // error has occurred while force saving the document. 强制保存文档出错
                System.out.println("error has occurred while force saving the document. 强制保存文档出错");
            }
            default: {
                // ignore
            }
        }

        // 返回响应
        return ResponseEntity.ok(Collections.singletonMap("error", 0));
    }

    publicvoidsaveFile(String downloadUrl)throws URISyntaxException, IOException {
        HttpsKitWithProxyAuth.downloadFile(downloadUrl, meetingMinutesFilePath);
    }

    @Setter
    @Getter
    publicstaticclass CallbackData {
        /** 用户与文档的交互状态 */
        Object changeshistory;
        Object history;
        String changesurl;
        String filetype;
        Integer forcesavetype;
        String key;
        /** 文档状态。1:编辑中;2:准备保存;3:保存出错;4:无变化;6:编辑保存;7:强制保存出错 */
        Integer status;
        String url;
        Object userdata;
        String[] users;
        String lastsave;
        String token;
    }
}

HttpsKitWithProxyAuth工具类

这是一个HTTP请求工具类,主要用于文件下载等操作。由于代码较长,这里只列出核心的 downloadFile 方法:

复制代码
/**
 * 下载文件到本地
 * @param downloadUrl 下载地址
 * @param savePathAndName 保存路径和文件名
 */
publicstaticvoiddownloadFile(String downloadUrl, String savePathAndName) {
    HttpGethttpGet=newHttpGet(downloadUrl);
    httpGet.setHeader("User-Agent", USER_AGENT);
    httpGet.setConfig(requestConfig);

    CloseableHttpResponseresponse=null;
    InputStreamin=null;

    try {
        response = getHttpClient().execute(httpGet, HttpClientContext.create());
        HttpEntityentity= response.getEntity();

        if (entity != null) {
            in = entity.getContent();
            FileOutputStreamout=newFileOutputStream(newFile(savePathAndName));
            IOUtils.copy(in, out);
            out.close();
        }
    } catch (IOException e) {
        logger.error("error", e);
    } finally {
        try {
            if (in != null) in.close();
        } catch (IOException e) {
            logger.error("error", e);
        }
        try {
            if (response != null) response.close();
        } catch (IOException e) {
            logger.error("error", e);
        }
    }
}

完整代码 :HttpsKitWithProxyAuth.java 和 JsonUtil.java 完整代码请参考原文。

03

问题总结

3.1、访问案例失败

部署完成后,可能存在很多问题,例如:访问example访问失败,那么使用以下命令查看服务状态:

复制代码
systemctl status ds*

查看有没有启动对应的服务。

3.2、加载word文档失败

修改启动的配置文件,将token去除,配置文件位置:

复制代码
/etc/onlyoffice/documentserver

修改 local.json

将参数token都改为false,去除token:

复制代码
"token": {
   "enable": {
     "request": {
       "inbox": false,
       "outbox": false
     },
     "browser": false
   }
}

修改 default.json

将参数 request-filtering-agent 改为true,token也改为false,rejectUnauthorized改为false:

复制代码
"request-filtering-agent": {
    "allowPrivateIPAddress": true,
    "allowMetaIPAddress": true
},
"token": {
    "enable": {
        "browser": false,
        "request": {
            "inbox": false,
            "outbox": false
        }
    }
},
"rejectUnauthorized": false

修改了以上配置参数后, 重启服务 ,再次测试。

以上更改基本能解决:

  • 报错文档权限问题

  • 文档保存失败问题

  • 文档下载问题等报错信息

3.3、系统后端有token验证问题

如果你访问的地址需要携带token,进行token验证(自己的系统后台,并非onlyoffice的token),那么可以通过配置下面代码的形式进行解决。

例如:我的访问路径为 http://192.168.123.123:8089/getFile/12 ,去除token验证:

复制代码
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity)throws Exception {
    return httpSecurity
            // CSRF禁用,因为不使用session
            .csrf(csrf -> csrf.disable())
            // 禁用HTTP响应标头
            .headers((headersCustomizer) -> {
                headersCustomizer.cacheControl(cache -> cache.disable())
                    .frameOptions(options -> options.sameOrigin());
            })
            // 认证失败处理类
            .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
            // 基于token,所以不需要session
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // 注解标记允许匿名访问的url
            .authorizeHttpRequests((requests) -> {
                permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                // ⭐ 重点:这里添加 /callback 和 /getFile/* 的匿名访问权限
                requests.antMatchers("/callback", "/getFile/*", "/login", "/register", "/captchaImage")
                        .permitAll()
                        // 静态资源,可匿名访问
                        .antMatchers(HttpMethod.GET, "/", "/*.html", "/ **/*.html", "/** /*.css", 
                            "/ **/*.js", "/profile/** ").permitAll()
                        .antMatchers("/swagger-ui.html", "/swagger-resources/ **", "/webjars/** ", 
                            "/*/api-docs", "/druid/**").permitAll()
                        // 除上面外的所有请求全部需要鉴权认证
                        .anyRequest().authenticated();
            })
            // 添加Logout filter
            .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
            // 添加JWT filter
            .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            // 添加CORS filter
            .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
            .addFilterBefore(corsFilter, LogoutFilter.class)
            .build();
}

在线测试链接

下面你可以直接使用一个在线的docx链接来测试你是否部署成功,在线链接:

复制代码
https://d2nlctn12v279m.cloudfront.net/assets/docs/samples/zh/demo.docx

即将前端代码中的 url 替换为上面的链接。

04

后记

如果你看到这里,那么代表你将要成功了,整个过程比较艰难,前后弄了两三天,还好最后结果是好的,所以简单总结一下,勉励自己。

状态码说明

OnlyOffice回调状态码含义:

相关推荐
洛文泽2 小时前
BigDecimal类型的数组转为字符串,并且去掉无效的0
java
小北方城市网2 小时前
微服务接口熔断降级与限流实战:保障系统高可用
java·spring boot·python·rabbitmq·java-rabbitmq·数据库架构
Remember_9932 小时前
【LeetCode精选算法】前缀和专题一
java·开发语言·数据结构·算法·leetcode·eclipse
孞㐑¥2 小时前
算法—双指针
开发语言·c++·经验分享·笔记·算法
承渊政道2 小时前
C++学习之旅【C++List类介绍—入门指南与核心概念解析】
c语言·开发语言·c++·学习·链表·list·visual studio
BlockChain8882 小时前
Spring Cloud实战:电商微服务系统从0到1(25000字终极实战指南)
spring·spring cloud·微服务
带土12 小时前
11. C++封装
开发语言·c++
多打代码2 小时前
2026.01.22 组合 &
算法·leetcode·深度优先
沛沛rh452 小时前
Rust入门一:从内存安全到高性能编程
开发语言·安全·rust