找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1912

积分

0

好友

270

主题
发表于 2025-12-31 04:36:05 | 查看: 22| 回复: 0

Microsoft Word软件Logo
图1:Microsoft Word软件图标

在Web项目中,实现前端页面直接编辑Word文档并保存是一个常见的需求。经过调研,开源组件OnlyOffice是一个不错的选择。它功能强大,支持文档转化、多人协同编辑、文档打印等,我们此次主要应用其在线文档编辑功能。

1、OnlyOffice的部署

部署OnlyOffice主要有两种方式:Docker部署和本地直接安装。相比之下,Docker方式更为简便,只需拉取镜像并配置启动参数即可。由于笔者最初搜索到的是Ubuntu本地部署教程,因此采用了后者。以下提供两种部署方式的参考链接,供大家选择:

2、代码逻辑开发

项目技术栈为:前端使用Element UI + Vue,后端采用 Spring Boot

2.1、前端代码

前端开发主要参考OnlyOffice官方API文档:

首先,需要在页面中引入OnlyOffice的JS API文件,注意将 documentserver 替换为你部署OnlyOffice服务的实际地址。

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

核心初始化配置与编辑器创建代码如下:

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,其中 12 是业务ID(如会议ID)。
  • callbackUrl: 文档操作(如编辑保存)的回调地址,OnlyOffice服务器会向此地址推送状态。

2.2、后端代码

后端使用Spring Boot,主要提供两个接口:/getFile/{id} 用于提供文档流,/callback 用于接收文档操作状态并处理保存。

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
public class OnlyOfficeController {
    @Autowired
    private IMeetingTableService meetingTableService;

    //这里仅写死路径测试
    //    private String meetingMinutesFilePath = "C:\\Users\\qrs-ljy\\Desktop\\王勋\\c1f15837-d8b4-4380-8161-b85e970ad174\\123435_会议纪要(公开).docx"; //这里仅写死路径测试
    private String meetingMinutesFilePath;

    /**
     * 传入参数 会议id,得到会议纪要文件流,并进行打开
     *
     * @param response
     * @param meeting_id
     * @return
     * @throws IOException
     */
    @ApiOperation(value = "OnlyOffice")
    @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);
        FileInputStream fileInputStream = null;
        InputStream fis = null;
        try {
            fileInputStream = new FileInputStream(file);
            fis = new BufferedInputStream(fileInputStream);
            byte[] buffer = new byte[fis.available()];
            fis.read(buffer);
            fis.close();
            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 -> ", e);
        } finally {
            try {
                if (fis != null) fis.close();
            } catch (Exception e) {

            }
            try {
                if (fileInputStream != null) fileInputStream.close();
            } catch (Exception e) {

            }
        }

    }

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

        //状态监听
        //参见https://api.onlyoffice.com/editors/callback
        Integer status = callbackData.getStatus();
        switch (status) {
            case 1: {
                //document is being edited  文档已经被编辑
                break;
            }
            case 2: {
                //document is ready for saving,文档已准备好保存
                System.out.println("document is ready for saving");
                String url = callbackData.getUrl();
                try {
                    saveFile(url); //保存文件
                } catch (Exception e) {
                    System.out.println("保存文件异常");
                }
                System.out.println("save success.");
                break;
            }
            case 3: {
                //document saving error has occurred,保存出错
                System.out.println("document saving error has occurred,保存出错");
                break;
            }
            case 4: {
                //document is closed with no changes,未保存退出
                System.out.println("document is closed with no changes,未保存退出");
                break;
            }
            case 6: {
                //document is being edited, but the current document state is saved,编辑保存
                String url = callbackData.getUrl();
                try {
                    saveFile(url); //保存文件
                } catch (Exception e) {
                    System.out.println("保存文件异常");
                }
                System.out.println("save success.");
            }
            case 7: {
                //error has occurred while force saving the document. 强制保存文档出错
                System.out.println("error has occurred while force saving the document. 强制保存文档出错");
            }
            default: {

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

    }

    public void saveFile(String downloadUrl) throws URISyntaxException, IOException {

        HttpsKitWithProxyAuth.downloadFile(downloadUrl, meetingMinutesFilePath);

    }

    @Setter
    @Getter
    public static class CallbackData {
        /**
         * 用户与文档的交互状态。0:用户断开与文档共同编辑的连接;1:新用户连接到文档共同编辑;2:用户单击强制保存按钮
         */
//        @IsArray()
//        actions?:IActions[] =null;

        /**
         * 字段已在 4.2 后版本废弃,请使用 history 代替
         */
        Object changeshistory;

        /**
         * 文档变更的历史记录,仅当 status 等于 2 或者 3 时该字段才有值。其中的 serverVersion 字段也是 refreshHistory 方法的入参
         */
        Object history;

        /**
         * 文档编辑的元数据信息,用来跟踪显示文档更改记录,仅当 status 等于 2 或者 2 时该字段才有值。该字段也是 setHistoryData(显示与特定文档版本对应的更改,类似 Git 历史记录)方法的入参
         */
        String changesurl;

        /**
         * url 字段下载的文档扩展名,文件类型默认为 OOXML 格式,如果启用了 assemblyFormatAsOrigin(https://api.onlyoffice.com/editors/save<a href="javascript:;">#assemblyFormatAsOrigin</a>) 服务器设置则文件以原始格式保存
         */
        String filetype;

        /**
         * 文档强制保存类型。0:对命令服务(https://api.onlyoffice.com/editors/command/forcesave)执行强制保存;1:每次保存完成时都会执行强制保存请求,仅设置 forcesave 等于 true 时生效;2:强制保存请求由计时器使用服务器中的设置执行。该字段仅 status 等于 7 或者 7 时才有值
         */
        Integer forcesavetype;

        /**
         * 文档标识符,类似 id,在 Onlyoffice 服务内部唯一
         */
        String key;

        /**
         * 文档状态。1:文档编辑中;2:文档已准备好保存;3:文档保存出错;4:文档没有变化无需保存;6:正在编辑文档,但保存了当前文档状态;7:强制保存文档出错
         */
        Integer status;

        /**
         * 已编辑文档的链接,可以通过它下载到最新的文档,仅当 status 等于 2、3、6 或 7 时该字段才有值
         */
        String url;

        /**
         * 自定义参数,对应指令服务的 userdata 字段
         */
        Object userdata;

        /**
         * 打开文档进行编辑的用户标识列表,当文档被修改时,该字段将返回最后编辑文档的用户标识符,当 status 字段等于 2 或者 6 时有值
         */
        String[] users;

        /**
         * 最近保存时间
         */
        String lastsave;

        /**
         * 加密令牌
         */
        String token;
    }
}

代码中用到的 HttpsKitWithProxyAuth(用于下载文件)和 JsonUtil(JSON工具类)是参考其他博客实现的辅助类,因其代码较长,此处不全文贴出,可参考相关资源实现。

3、问题总结

以下是集成过程中遇到的关键问题及解决方案。

3.1、访问示例失败

部署完成后,访问 example 可能失败。首先检查相关服务是否正常启动:

systemctl status ds*

3.2、加载Word文档失败

如果遇到文档加载或保存权限问题,通常需要修改OnlyOffice的配置文件,关闭令牌验证并调整网络过滤设置。

配置文件位于 /etc/onlyoffice/documentserver,主要修改 local.jsondefault.json

  • local.json 中,将 token 相关配置设为 false
    "token": {
    "enable": {
    "request": {
      "inbox": false,
      "outbox": false
     },
    "browser": false
    },
  • default.json 中,进行三处修改:
    1. 允许私有和元IP地址:
      "request-filtering-agent" : {
        "allowPrivateIPAddress": true,
        "allowMetaIPAddress": true
          },
    2. 关闭令牌验证:
      "token": {
        "enable": {
          "browser": false,
          "request": {
            "inbox": false,
            "outbox": false
            }
         },
    3. 关闭SSL证书验证(内网环境可设):
      "rejectUnauthorized": false

      修改后重启OnlyOffice服务。

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

如果你的Spring Boot应用本身使用了JWT等Token验证机制,需要确保OnlyOffice回调(/callback)和文档下载接口(/getFile/*)不被安全拦截。在Spring Security配置中将其设为免认证路径:

// 在Security配置类中
requests.antMatchers("/callback", "/getFile/*", "/login", "/register", "/captchaImage").permitAll()

3.4、使用文档地址直接访问

除了通过后端接口动态提供文档流,也可以直接提供文档的静态URL(例如通过Nginx代理静态文件)。你可以使用一个在线的DOCX链接测试OnlyOffice服务是否部署成功:
https://d2nlctn12v279m.cloudfront.net/assets/docs/samples/zh/demo.docx

将此链接替换到前端配置的 url 字段中即可测试。

4、后记

集成OnlyOffice实现在线编辑功能的过程可能会遇到一些挑战,但一旦打通,将为Web应用带来强大的文档处理能力。希望本文的实践总结能为你提供清晰的路径参考。如果你在 前端框架/工程化 或其他技术集成方面有更多心得,欢迎在 云栈社区 交流分享。




上一篇:MyBatis-Plus updateById线上数据覆盖问题详解与避坑指南
下一篇:Linux驱动面试手册:设备模型、GPIO、Pinctrl与I2C/SPI子系统核心八股文解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:29 , Processed in 0.280632 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表