Spring-boot+Vue 前后端快速集成onlyoffice

只介绍集成 不介绍搭建环境

一、整体流程

onlyoffice服务使用docker部署
在这里插入图片描述
onlyoffice-documentserver: 文档服务 文件转换服务 编辑器服务
rabbitmq: 消息队列
postgres:9.5: 数据库


docker-compose.yml 文件

version: '2'
services:
  onlyoffice-documentserver:
    container_name: onlyoffice-documentserver
    image: sgcc-dky-smartky-onlyoffice_onlyoffice-documentserver
    depends_on:
      - onlyoffice-postgresql
      - onlyoffice-rabbitmq
    environment:
      - DB_TYPE=postgres
      - DB_HOST=onlyoffice-postgresql
      - DB_PORT=5432
      - DB_NAME=onlyoffice
      - DB_USER=onlyoffice
      - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq
      # Uncomment strings below to enable the JSON Web Token validation.
      #- JWT_ENABLED=true
      #- JWT_SECRET=secret
      #- JWT_HEADER=Authorization
      #- JWT_IN_BODY=true
    ports:
      - '8088:80'
      - '443:443'
    stdin_open: true
    restart: always
    stop_grace_period: 60s
    volumes:
      - /var/www/onlyoffice/Data
      - /var/log/onlyoffice
      - /var/lib/onlyoffice/documentserver/App_Data/cache/files
      - /var/www/onlyoffice/documentserver-example/public/files
      - /usr/share/fonts

  onlyoffice-rabbitmq:
    container_name: onlyoffice-rabbitmq
    image: rabbitmq
    restart: always
    expose:
      - '5672'
    ports:
      - '5672:5672'

  onlyoffice-postgresql:
    container_name: onlyoffice-postgresql
    image: postgres:9.5
    environment:
      - POSTGRES_DB=onlyoffice
      - POSTGRES_USER=onlyoffice
      - POSTGRES_HOST_AUTH_METHOD=trust
    restart: always
    expose:
      - '5432'
    ports:
      - '5432:5432'
    volumes:
      - postgresql_data:/var/lib/postgresql

volumes:
  postgresql_data:


访问文档时序图

在这里插入图片描述

文档保存时序图

在这里插入图片描述



二、前端集成

前端集成过程

OnlyOffice前端由onlyOffice-documentServer提供,后端会生成api.js文件,前端引入这个js就可以集成onlyOffice的编辑器。


前端项目结构

在这里插入图片描述
env.js 是配置文件 配置onlyoffice编辑器以及文档服务的地址

var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
    NODE_ENV: '"development"',
    BASE_URL: '"/api"',
    // onlyoffice document-server 服务端口
    // 编辑地址
    ONLYOFFICE_DOCUMENT_HOST: '"http://127.0.0.1"',
    // 预览地址
    ONLYOFFICE_DOCUMENT_PREVIEW_HOST: '"http://127.0.0.1"',
    ONLYOFFICE_DOCUMENT_PORT: 8080,
    // onlyoffice前端编辑器地址
    ONLYOFFICE_DOCUMENT_URL: '"/web-apps/apps/api/documents/api.js"'
});

onlyofficeView是onlyoffice 基础组件 封装了核心的onlyoffice编辑器

<template>
  <div id="monitorOffice"></div>
</template>
<script>
import {handleDocType} from "./docType";

export default {
  name: "onlyofficeView",
  ...省略,
  methods: {
    initEditor() {
      // 加入 onlyoffice api.js 脚本
      const apiScriptDom = document.getElementById("onlyoffice-document-api");
     
      const script = document.createElement("script");
      const {protocol} = window.location;

      const ONLYOFFICE_DOCUMENT_PORT=process.env.ONLYOFFICE_DOCUMENT_PORT;
      
      const ONLYOFFICE_DOCUMENT_URL=process.env.ONLYOFFICE_DOCUMENT_URL;
      
      script.setAttribute("id", "onlyoffice-document-api");
      script.setAttribute(
        "src",
        `${this.documentUrl +
        ":" +
        ONLYOFFICE_DOCUMENT_PORT +
        ONLYOFFICE_DOCUMENT_URL}`
      );
      document.body.appendChild(script);
      const _self = this;
      script.onload = () => {
        _self.scriptReady = true;
        console.log("initEditor 初始化组件完成!");
        if (_self.option.url) {
          _self.setEditor(_self.option);
        }
      };
    },
    setEditor(option) {
      const {
        $store: {
          state: {
            user: {userId, nickname}
          }
        }
      } = this;
      this.doctype = handleDocType(option.fileType);
    }
  },
  watch: {
    option: {
      handler: function (n, o) {
        this.doctype = handleDocType(n.fileType);
        if (this.scriptReady) {
          this.setEditor(n);
        }
      },
      deep: true
    }
  }
};
</script>

onlineCat onlineEdit 是集成了onlyoffice编辑器的编辑和预览 主要是根据各种业务场景初始化的配置

<template>
  <div style="height:100%">
    <onlyoffice-view ref="onlyOffcieView" :option="option" :document-url="documentUrl"></onlyoffice-view>
  </div>
</template>



三、后端集成

在这里插入图片描述

1.核心服务接口

核心方法
下载 download()
保存 save()
获取onlyoffice Token getToken()
校验token verifyToken()

/**
 * onlyoffice 服务接口
 *
 * @author: colagy
 * 2021-03-26 16:06
 */
public interface IOnlyofficeService {

    /**
     * onlyoffice document server 下载文件
     * @param id
     * @param scene
     * @param userId
     * @param timeStamp
     * @param token
     * @param response
     * @throws IOException
     */
    void download(String id, Enum scene, String userId, Long timeStamp, String token, HttpServletResponse response) throws IOException;

    /**
     * 文件回写
     *
     * @param request onlyoffce document server 请求
     * @param eClass  文档场景枚举 类对象
     * @param <E>     onlyofficeScene 文档场景枚举
     * @return
     * @throws IOException
     */
    <E extends Enum> JSONObject save(HttpServletRequest request, Class<E> eClass) throws IOException;

    /**
     * @return
     */
    OnlyofficeToken getToken();

    boolean verifyToken(String userId, Long timeStamp, String token, Long expire);
}

2.服务基类

定义了两个 抽象方法 子类需要实现并返回 存储方法 和 token过期校验

@Service
public abstract class SimpleOnlyofficeService implements IOnlyofficeService {
    private final static Logger log = LoggerFactory.getLogger(SimpleOnlyofficeService.class);

    private static final String tokenPrefix = "onlyoffice_token_profix_node_server";

    public abstract IOnlyofficeStorage getOnlyofficeStorage();

    public abstract long getTokenExpire();

	...
	         
}

默认实现的download()方法
调用getOnlyofficeStorage 获取存储的实现类 可以是oss 或者本地文件存储
调用onlyofficeStorage.get()方法 返回值是inputStream写入响应流返回给onlyofficeDocumentServer

	@Override
    public void download(String id, Enum scene, String userId, Long timeStamp, String token, HttpServletResponse response) throws IOException {
        // token过期时间默认10分钟
        long tokenExpire = getTokenExpire();
        if (tokenExpire <= 0) {
            tokenExpire = 10 * 60 * 1000L;
        }
        if (!verifyToken(userId, timeStamp, token, tokenExpire)) {
            throw new CustomGenericException("授权已过期或授权失败");
        }

        IOnlyofficeStorage onlyofficeStorage = getOnlyofficeStorage();
        InputStream inputStream = onlyofficeStorage.get(id, scene);

        FileUtil.nioTransferTo(inputStream, response.getOutputStream());
        // TODO 需要storage 返回更多信息
        FileUtil.parseDownloadResponse(response, String.valueOf(timeStamp), "application/octet-stream");
    }

默认实现的save()方法 需要返回 {“error”:0} 给onlyoffice确认
调用 onlyofficeStorage.put(id, scene, inputStream, bytes.length)方法 将onlyOfficeServer传入的流保存

	@Override
    public <E extends Enum> JSONObject save(HttpServletRequest request, Class<E> eClass) throws IOException {
        Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A");
        String bodyStr = scanner.hasNext() ? scanner.next() : "";

        Map<String, String[]> parameterMap = request.getParameterMap();
        String[] ids = parameterMap.get("id");
        String[] scenes = parameterMap.get("scene");

        String id = null;
        if (ArrayUtils.isNotEmpty(ids)) {
            id = ids[0];
        }
        if (Objects.isNull(id)) {
            JSONObject res = new JSONObject();
            res.put("error", -1);
            return res;
        }

        Enum scene = null;
        if (ArrayUtils.isNotEmpty(scenes) && EnumUtils.isValidEnum(eClass, scenes[0])) {
            try {
                scene = Enum.valueOf(eClass, scenes[0]);
            } catch (Exception e) {
                String msg = "onlyoffice save方法 未指定scene";
                log.error(ErrorUtil.getErrorStack(e, msg));
            }
        }
        if (Objects.isNull(scene)) {
            JSONObject res = new JSONObject();
            res.put("error", -1);
            return res;
        }

        JSONObject body = JSONObject.parseObject(bodyStr);

        int status = body.getInteger("status");

        if (status == 2) {

            String downloadUrl = body.getString("url");
            if (StringUtils.isNotBlank(downloadUrl)) {
                IOnlyofficeStorage onlyofficeStorage = getOnlyofficeStorage();

                byte[] bytes = HttpUtil.downloadBytes(downloadUrl);
                InputStream inputStream = new ByteArrayInputStream(bytes);

				// 默认方法 可以实现beforeSave
                beforeSave(id, scene, parameterMap, body, inputStream);
                onlyofficeStorage.put(id, scene, inputStream, bytes.length);
                afterSave(id, scene, parameterMap, body, inputStream);

            }
        }

        // 正常处理需要返回 {"error":0} 给onlyoffice确认
        JSONObject res = new JSONObject();
        res.put("error", 0);
        return res;
    }



四、权限验证

服务基类定义getUserId()方法 实现类需要返回一个用户唯一标识

	/**
     * 必须实现返回当前登录用户id的方法 用于token获取以及校验
     *
     * @return
     */
    protected abstract String getUserId();

    /**
     * 获取token
     *
     * @return
     */
    @Override
    public OnlyofficeToken getToken() {
        String userId = getUserId();

        long timeStamp = System.currentTimeMillis();
        String tokenStr = parseToken(userId, timeStamp);

        OnlyofficeToken token = new OnlyofficeToken(userId, timeStamp, tokenStr);

        return token;
    }

    private String parseToken(String userId, Long timeStamp) {
        String tokenStr = "使用一些加密的方法将userId timeStamp 加密返回一个加密后的token,可以参加jwt";
        return tokenStr;
    }

    /**
     * 校验token 并判断过期时间
     *
     * @param token  token
     * @param expire 过期时间(ms) 默认 10*60*000ms 10分钟
     * @return
     */
    @Override
    public boolean verifyToken(String userId, Long timeStamp, String token, Long expire) {
        if (StringUtils.isBlank(userId)) {
            log.warn("onlyofficeToken 验证失败 userId为空");
            return false;
        }
        if (Objects.isNull(timeStamp) || timeStamp == 0) {
            log.warn("onlyofficeToken 验证失败 timeStamp为空");
            return false;
        }
        if (Objects.isNull(expire)) {
            expire = 10 * 60 * 1000L;
        }


        long now = System.currentTimeMillis();
        if (now - timeStamp > expire) {
            log.error("onlyoffice token已过期 , userId:" + userId + ", token:" + token + ", timeStamp:" + timeStamp + ", now:" + now + ", expire:" + expire);
            return false;
        }

        String realToken = parseToken(userId, timeStamp);
        boolean isValid = StringUtils.equals(token, realToken);
        if (!isValid) {
            log.error("onlyoffice token验证失败! " + " token:" + token + ", realToken:" + realToken + ", userId:" + userId + ", timeStamp:" + timeStamp + ", expire:" + expire);
        }
        return StringUtils.equals(token, realToken);
    }




五、储存集成

1.存储接口

主要是两个方法
get() 返回字节流
put() 传入字节流
可以用oss实现 也可以用本地文件实现 只要能返回字节流和写入字节流就可以

/**
 * onlyoffice 在线编制的 储存方式需要实现这个接口
 *
 * @author: colagy
 * 2021-03-29 11:03
 */
public interface IOnlyofficeStorage {
    /**
     * 获取文件字节流
     *
     * @param id    文件唯一标识
     * @param scene 使用场景
     * @return
     * @throws IOException
     */
    InputStream get(String id, Enum scene) throws IOException;

    /**
     * 写入文件字节流
     *
     * @param id          文件唯一标识
     * @param scene       使用场景
     * @param inputStream 输入流
     * @param inSize      流大小(用于nio拷贝)
     * @throws IOException
     */
    void put(String id, Enum scene, InputStream inputStream, long inSize) throws IOException;
}
2.本地文件基类

子类需要实现 getFilePath() 根据资源唯一标识返回文件的储存路径

/**
 * 文件系统存储抽象类 子类需要实现通过id获取存储路径的方法
 *
 * @author: colagy
 * 2021-03-29 11:07
 */
public abstract class FsStorage implements IOnlyofficeStorage {
    public abstract String getFilePath(String id, Enum scene);

    @Override
    public InputStream get(String id, Enum scene) throws IOException {
        String saveFilePath = getFilePath(id, scene);
        Assert.notBlank(saveFilePath);

        File file = new File(saveFilePath);
        File parentFile = file.getParentFile();
        if (Objects.nonNull(parentFile)) {
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            }
        }
        if (!file.exists()) {
            file.createNewFile();
        }

        Assert.isTrue(file.exists());

        FileInputStream fileInputStream = new FileInputStream(file);

        return fileInputStream;
    }

    @Override
    public void put(String id, Enum scene, InputStream inputStream, long inSize) throws IOException {
        String filePath = getFilePath(id, scene);
        FileUtil.nioTransferTo(inputStream, inSize, filePath);
    }

}



六、场景集成

目前文件存储需要集成文件场景
不同的场景可以有不同的方式获取文件路径

/**
 * 场景需要实现这个接口
 *
 * @author: colagy
 * 2021-06-11 17:10
 */
public interface IFsScene {
    String getFilePath(String id);
}

文件场景默认实现类 base64方式 只做示例不推荐使用

/**
 * 文件路径base64
 *
 * @author: colagy
 * 2021-06-15 12:03
 */
@Service(value = "FS_BASE64")
public class Base64FsScene implements IFsScene {
    /**
     * id为filePath的base64值 把base64解析就是filePath
     * TODO 前端base64需要使用 window.btoa(window.encodeURIComponent("/filepath/文档.doc")) 转base64
     *
     * @param filePathBase64 filePath的base64值
     * @return file path
     */
    @Override
    public String getFilePath(String filePathBase64) {
        if (StringUtils.isBlank(filePathBase64)) {
            return "";
        }

        try {
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] decode = decoder.decode(filePathBase64);
            String urlEncodeFilePath = new String(decode);
            String filePath = URLDecoder.decode(urlEncodeFilePath, "utf-8");
            return filePath;
        } catch (Exception e) {
            return "";
        }
    }
}




评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值