做后台系统久了,迟早会遇到一个需求:让用户在网页里直接在线编辑 Office 文档,而不是下载下来改完再传回去。这个需求一旦出现,选型往往落在 OnlyOffice 上,原因是它开源、兼容性好,而且部署起来不像某些商业方案那样费劲。我这次是在 ThinkPHP 8.x 项目里接的 OnlyOffice,前后踩了不少坑,这里把整个集成过程和排查记录完整写一遍,给后面接同类的朋友做个参考。
这个方案的核心价值很明确:ThinkPHP 负责业务逻辑、用户权限和文件存储,OnlyOffice Document Server 独立部署,负责文档渲染和在线编辑。两边通过 HTTP 回调、JWT 签名令牌协作,前端只需嵌入一个编辑器容器页面。整套东西做下来,用户能在浏览器里像用本地 Office 一样编辑 Word、Excel、PPT,改动实时同步回服务器。适合有自研 OA、CRM、网盘或者教务系统的团队,尤其是已经用了 ThinkPHP 8.x 又不想为了一个编辑器换技术栈的场景。
1. 整体设计与思路拆解
1.1 OnlyOffice 的工作机制
先把 OnlyOffice 的架构讲明白,不然后面做集成会一头雾水。OnlyOffice 分两个部分:一个是 Document Server,也就是真正干活的文档服务,负责把 docx、xlsx、pptx 渲染成网页可交互的编辑器;另一个是集成端,也就是你自己的业务后端加前端。Document Server 本身完全不关心你的用户系统、权限模型,它只认你通过编辑器配置传过来的内容。
它采用的是一种“前端渲染 + 后端回调”的模式。你的后端生成一段配置对象(通常叫 config),里面包含文档 URL、用户信息、编辑权限、回调地址等,前端拿到这段配置后,通过 OnlyOffice 提供的 JavaScript API 把编辑器渲染进页面。用户编辑过程中,Document Server 定时把文档数据发给你的回调接口,通常是文档打开、保存中、已保存、出错等状态变化。你的后端在收到“保存”事件时,把新的文档内容写回自己的存储系统,完成一次在线编辑闭环。
从集成方角度看,你只需要做三件事:提供文档访问地址、生成配置对象、接收保存回调。难点不在代码量,而在参数正确性和部署连通性。
1.2 为什么选择“服务端签名 + 前端渲染”这套组合
我在项目里采用的是 ThinkPHP 服务端生成带 JWT 签名的 config,前端 Notown 渲染页面直接消费。没有用官方推荐的外网集成示例,也没有把密钥写死在 JS 里。
原因是安全考虑占大头。OnlyOffice 的 Document Server 和业务后端不在同一台机器时,如果 config 中任何参数可以被客户端篡改,攻击者就能伪造文档地址、伪造用户身份,甚至把回调地址改成自己控制的服务器,导致数据泄露。使用服务端签名的 JWT 令牌后,Document Server 会对收到的回调请求和 config 内容做签名校验,任何被篡改的请求都会被直接拒绝。
另外,把 config 生成逻辑全部放在服务端,也为后续扩展留了空间。比如以后要接多租户、自定义水印、动态权限,只需在后端改数组结构,前端代码基本不用动。这套做法在 Spring Boot、Node.js 集成 OnlyOffice 时也是主流通用方案,换到 ThinkPHP 上只是语言差异,架构不变。
1.3 模块划分与代码组织
我在项目里没有把逻辑堆在控制器里,而是按职责拆了几个模块:
- 配置类,专门管理 OnlyOffice Document Server 地址、JWT 密钥、存储路径等环境参数;
- 签名服务,负责生成和验证 JWT 令牌;
- 文档接口服务,负责根据文件 ID 生成访问 URL、构建编辑器 config 数组;
- 回调控制器,负责接收 Document Server 的各类事件并写回文件;
- 前端控制器 + 模板,负责渲染编辑器页面。
这样拆的好处是一旦出问题,排查范围很清晰。比如编辑器报 token 相关错误,先查签名服务;回调保存失败,直接看回调控制器日志。一个文件搞定所有逻辑虽然看起来简单,但后续加功能、改权限、加日志都会变得很痛苦,不建议图省事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖安装
2.1 ThinkPHP 8.x 的环境要求
ThinkPHP 8.x 对运行环境有两个硬性要求:PHP 8.0 及以上版本,以及必须安装 ext-json 扩展。官方在安装说明里写得很明确,但实践中有不少人在部署时踩了没装 json 扩展的坑,导致 Composer 安装依赖直接失败。建议先跑一条命令行确认环境:
bash复制php -v
php -m | grep json
composer --version
如果 php -m 看不到 json 扩展,Debian/Ubuntu 系统可以用 apt install php8.1-json 方式安装,CentOS 系用户用 yum install php-json。PHP 8.0 以上版本默认已经内置 json 扩展,旧系统或者手动编译的 PHP 容易漏掉。
另外,ThinkPHP 8 默认启用了 MultiApp 多应用模式,如果你同时使用多应用,需要在 config/app.php 里确认 auto_multi_app 配置。官方 8.0 版本默认是 false,如果之前是从 6.x 升级过来的项目要特别留意,否则路由全部短路,回调接口怎么都访问不到。
2.2 使用 Docker 部署 OnlyOffice Document Server
OnlyOffice Document Server 的部署方式有很多种,官方推荐的是 Docker 方式,一条命令就能跑起来,维护成本最低。我的服务器是 Ubuntu 22.04,Docker 部署时要注意端口选择,编辑器实际工作时占用两个端口:80 和 443。如果你不想直接用这两个端口做端口映射,可以映射到别的宿主机端口,但要注意后续配置里的地址必须带上映射后的端口。
我用的是这种启动方式:
bash复制docker run -i -t -d -p 8080:80 -p 8443:443 \
-e JWT_ENABLED=true \
-e JWT_SECRET=your-random-secret-key \
-v /data/onlyoffice/logs:/var/log/onlyoffice \
-v /data/onlyoffice/data:/var/www/onlyoffice/Data \
-v /data/onlyoffice/lib:/var/lib/onlyoffice \
-v /data/onlyoffice/db:/var/lib/postgresql \
--restart=always \
onlyoffice/documentserver
注意 JWT_ENABLED 和 JWT_SECRET 必须配置,否则编辑器会直接拒绝请求。JWT_SECRET 要足够长且随机,推荐用 openssl rand -base64 32 生成,然后记下来,后面 ThinkPHP 配置里要用同一个密钥。
另外 Document Server 依赖 PostgreSQL 和 RabbitMQ,这些都是容器内部自动处理的,不需要你额外安装。启动后,访问 http://你的服务器IP:8080 能看到一个欢迎页面,说明安装成功。如果是云服务器,记得在安全组放行对应端口,不然页面加载不出来,排查半天还以为是代码问题。
2.3 安装 PHP 依赖与项目初始化
在 ThinkPHP 项目里,我没有为了 OnlyOffice 额外引入太多 composer 包,JWT 的实现可以用官方 firebase/php-jwt,也可以自己写一个简单的 HMAC-SHA256 类。考虑到项目后续可能还有其它地方用到 JWT,我选择了官方包:
bash复制composer require firebase/php-jwt
这个包很轻量,支持 HS256、RS256 等常见签名算法。OnlyOffice 默认用的配置是 JWT 开头为 Bearer 的请求头传递,但回调请求里也有可能出现 JWT 载荷直接 POST 到链接中的情况,这个后面回调部分再说。安装完依赖后,我在 config/onlyoffice.php 里新建了配置文件,集中管理参数,不直接写在控制器里。具体配置内容放到下一章展开。
3. 核心实现:服务端令牌与文档接口
3.1 配置文件与环境变量
OnlyOffice 集成涉及几个可变参数:Document Server 地址、JWT 密钥、回调访问地址、文件访问地址前缀。这些在不同环境(本地、测试、生产)大概率不一样,所以应该放到环境配置里。我的 config/onlyoffice.php 内容长这样:
php复制<?php
return [
'server_url' => env('ONLYOFFICE_SERVER', 'http://127.0.0.1:8080'),
'jwt_secret' => env('ONLYOFFICE_JWT_SECRET', ''),
'jwt_expire' => env('ONLYOFFICE_JWT_EXPIRE', 3600),
'callback_url' => env('ONLYOFFICE_CALLBACK', 'http://127.0.0.1:8000/onlyoffice/callback'),
];
注意回调地址 ONLYOFFICE_CALLBACK 不能写成内网地址。文档服务器所在的那台机器必须要能访问到这个地址,否则保存操作会失败。如果你把 ThinkPHP 项目跑在 Docker 容器里,容器内网 IP 和宿主机 IP 不是一回事,建议直接配置成公网域名,或者至少保证 Document Server 能通过内部网络安全访问到。
文件访问地址我这里没有硬编码,而是根据文件 ID 动态生成一个临时 URL,这样文件路径怎么变都不影响外部访问。
3.2 生成 JWT 签名
OnlyOffice 的 JWT 规则比较特殊,它既可以放在 Authorization 请求头里,也可以当作 POST Body 中的一个字段 token。Document Server 校验的时候会先尝试从请求头取,取不到再从请求体里取,两者只要通过校验都算通过。
生成令牌的代码我已经抽成一个服务类,核心逻辑很简单,用 firebase/php-jwt:
php复制<?php
declare(strict_types=1);
namespace app\common\service;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class OnlyOfficeJwtService
{
protected string $secret;
public function __construct()
{
$this->secret = (string) config('onlyoffice.jwt_secret');
}
public function sign(array $payload): string
{
$payload['iat'] = time();
$payload['exp'] = time() + (int) config('onlyoffice.jwt_expire');
return JWT::encode($payload, $this->secret, 'HS256');
}
public function verify(string $token): ?array
{
try {
return (array) JWT::decode($token, new Key($this->secret, 'HS256'));
} catch (\Throwable $e) {
return null;
}
}
}
这里有个细节:OnlyOffice 文档服务器要求必须带上过期时间字段 iat 和 exp,不然有些版本会校验失败报 token 相关错误。即使你的业务场景想把这个令牌做成长期有效,也建议把过期时间设得长一些,而不是完全省略。
3.3 根据文件 ID 构建编辑器 config
这是整个集成最核心的部分,也是坑最多的部分。config 结构长什么样,直接决定 OnlyOffice 能不能正常打开文档。我提供了一个方法,传入文件 ID 返回完整的 config 数组,供前端直接使用:
php复制public function buildConfig(int $fileId, string $userId, string $userName, bool $editable = true): array
{
$file = FileModel::find($fileId);
if (!$file) {
throw new \RuntimeException('文件不存在');
}
$fileUrl = $this->generateFileUrl($file->id, $file->name);
$key = $this->generateKey($file->updated_at);
$config = [
'document' => [
'fileType' => strtolower(pathinfo($file->name, PATHINFO_EXTENSION)),
'key' => $key,
'title' => $file->name,
'url' => $fileUrl,
'permissions' => [
'edit' => $editable,
'download' => true,
'print' => true,
],
],
'documentType' => $this->detectDocumentType($file->name),
'editorConfig' => [
'callbackUrl' => config('onlyoffice.callback_url'),
'mode' => $editable ? 'edit' : 'view',
'lang' => 'zh-CN',
'user' => [
'id' => $userId,
'name' => $userName,
],
],
'height' => '100%',
'width' => '100%',
];
$config['token'] = $this->jwtService->sign($config);
return $config;
}
这里几个点需要解释为什么这么做:
key 是文档缓存标识,Document Server 拿它来区分文档版本。如果 key 不变化,即使用户修改了内容,编辑器也可能从缓存加载旧版本。我这里用文件的 updated_at 时间戳来生成 key,这样每次文件内容变化后,key 也会随着变化。注意 key 只能包含英文字母、数字和点号,不能用中文和特殊符号。
fileType 必须是小写扩展名,且不能带点。Document Server 支持 docx、xlsx、pptx、odt、ods、odp、csv、txt 等格式,但不同格式的编辑支持程度不一样。比如 txt 在服务端只是当成文本文件处理,不是真正的 OOXML 格式。
documentType 用来告诉 Document Server 用 word、cell 还是 slide 模式渲染,只能是 word、cell、slide 三个值。检测方法就是根据扩展名判断,docx、doc、odt、txt 走 word,xlsx、xls、ods、csv 走 cell,pptx、ppt、odp 走 slide。
callbackUrl 必须真实可达,且要与 Document Server 能访问到的地址一致。很多时候前端编辑器能打开,但一保存就失败,十有八九就是这里配的地址只对浏览器可见,Document Server 访问不通。
最后一个细节,整个 config 数组在传给前端之前,要用 JWT 签名并塞到 config 里的 token 字段。这个步骤不是可选项,而是必做的,因为 Document Server 在收到编辑器请求时会校验 token,发现没有就会直接拒绝渲染。
3.4 回调接口的实现要点
Document Server 保存文档时会向 callbackUrl 发送 POST 请求,请求体是一个 JSON 结构,里面最关键的有两个字段:status 和 url。status 为 1 表示文档已准备好编辑,2 表示文档已保存,3 表示编辑会话出错。
我的回调控制器重点关注 status 为 2 的场景,因为这时才需要把新内容写回文件存储。下面是一个简化版实现:
php复制public function callback(Request $request)
{
$body = $request->getContent();
$data = json_decode($body, true);
if (!$data) {
return json(['error' => 1]);
}
$token = $data['token'] ?? $request->header('Authorization', '');
$token = str_replace('Bearer ', '', $token);
$payload = $this->jwtService->verify($token);
if (!$payload) {
return json(['error' => 1]);
}
$status = (int) ($data['status'] ?? 0);
if ($status === 2 && !empty($data['url'])) {
$fileId = (int) ($payload['fileId'] ?? 0);
$file = FileModel::find($fileId);
if ($file) {
$content = file_get_contents($data['url']);
if ($content !== false) {
file_put_contents($file->getFullPath(), $content);
$file->updated_at = time();
$file->save();
}
}
}
return json(['error' => 0]);
}
回调处理有两个很容易踩的坑。第一个是 token 校验,Document Server 发送的请求可能带 Authorization: Bearer xxx,也可能把 token 放在 JSON 体里,两种都要处理。第二个是保存内容时,从 data['url'] 下载新文档后,一定要直接覆盖原文件,同时更新数据库里的更新时间,否则下一次编辑时 key 不变,Document Server 会从缓存加载旧内容。
也不要忘记,回调返回的 JSON 格式严格规定为 {"error": 0},多余字段没关系,但 error 必须存在且为 0。返回其它结构会导致 Document Server 认为保存失败,用户在页面上可能看到“无法保存文档”的提示。
4. 前端集成 OnlyOffice 编辑器
4.1 引入 OnlyOffice API 并渲染编辑器
前端渲染 OnlyOffice 编辑器,本质就是在页面里创建一个 div 容器,然后加载 https://你的DocumentServer地址/web-apps/apps/api/documents/api.js 这个 JS 文件,调用全局 DocsAPI.DocEditor 函数,把服务端生成的 config 作为参数传入。
我项目的模板里做了两步:一个是编辑器页面 edit.html,专门负责渲染编辑器;另一个是列表页到编辑器页的跳转,带上文件 ID。模板关键代码如下:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线文档</title>
<style>
html, body, #placeholder {
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="placeholder"></div>
<script type="text/javascript">
const config = <?= json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
new DocsAPI.DocEditor('placeholder', config);
</script>
<script src="<?= $serverUrl ?>/web-apps/apps/api/documents/api.js"></script>
</body>
</html>
这里有一个重要细节:先加载 api.js,再调用 DocsAPI.DocEditor。如果你把初始化代码放在引入 api.js 之前,浏览器会直接抛 DocsAPI is not defined 错误。虽然在某些网络延迟情况下可能碰巧成功,但这是请求时序竞争问题,不建议依赖运气。更稳妥的做法是监听 DOMContentLoaded 事件,或者在 window.onload 里再初始化。
4.2 动态生成 config 的前端逻辑
由于服务端已经生成好完整 config,前端不需要做任何逻辑判断。但从控制器拿到 config 时,要注意 JSON 编码的问题。我在模板里直接用了 json_encode 输出,但需要注意 JSON_UNESCAPED_SLASHES,否则文档 URL 里的 / 会被转义成 \/,虽然 JS 能正确解析,但某些老版本浏览器可能出现异常解析。
对于 ThinkPHP,控制器返回模板数据时把 config 传进去即可:
php复制public function edit(Request $request, int $id)
{
$file = FileModel::findOrFail($id);
$config = $this->documentService->buildConfig(
$file->id,
(string) session('user_id'),
(string) session('user_name'),
true
);
return view('onlyoffice/edit', [
'config' => $config,
'serverUrl' => config('onlyoffice.server_url'),
]);
}
如果你的项目不是用模板渲染,而是前后端分离开发,接口返回 config JSON,前端直接用 fetch 请求后调用 DocsAPI.DocEditor,效果一样。不过需要注意跨域问题:页面所在域名和 config 中 url 对应的域名不一致时,Document Server 访问文档时会有 CORS 限制,需要提前配置允许跨域,否则编辑器能打开但文档加载不出来。
4.3 编辑器 config 里几个值得关注的高级参数
很多博客只会把基础配置展示一遍,然后不管了。但实际业务中,编辑器能否满足最终用户的需求,往往取决于这些附加参数。
editorConfig.customization:可以设置自定义水印、关闭品牌标识、调整工具栏显示等。做企业内网系统时,可以用customization.customer设置项目自己的名称和 logo。editorConfig.lang:设为zh-CN可以让编辑器界面显示中文。不设置默认英文,很多用户会不习惯。editorConfig.coediting:mode为fast时开启实时协作编辑,strict时则在保存时才合并。如果文档同时被多个人编辑,建议用fast,体验更流畅。document.permissions:可以单独控制edit、download、print、comment权限。比如财务文件只允许查看不允许下载,可以在这里设download: false。
这些参数官方文档都有,但实际项目里没人会全部看一遍,建议先按需查阅。
5. 文件存储与版本管理策略
5.1 文件路径设计
在线编辑场景下,文件存储路径要遵循一个原则:容易定位、容易备份、不容易被覆盖。我的项目把用户上传的原文和 OnlyOffice 编辑后的临时文件分开存储。
原文统一放在 storage/editor/ 目录下,文件名保持用户上传时的真实名称,但数据库里额外记录一个存储路径字段。每次 OnlyOffice 保存时,回调接口直接用新内容覆盖这个路径下的文件。初看好像没有历史版本,但用户文档本来就是修改覆盖,后期再通过扩展方式加版本管理即可,第一步先保证能保存成功。
需要注意权限问题,PHP 进程要对存储目录有写权限。我这里遇到过目录权限不对导致保存失败的情况,排查时发现是 storage/editor 属于 root 用户,而 PHP-FPM 使用 www-data 用户运行,直接导致写入失败。解决方法是把目录属主改为 www-data 或使用 chmod -R 775。
5.2 保存回调的幂等性处理
OnlyOffice 回调保存时可能出现重复请求,比如网络重试、文档服务器内部集群重试。如果回调接口不做幂等处理,同一个版本可能被写回两次,但两次内容一样倒也没关系,真正要担心的是并发场景:用户在 A 窗口编辑了内容保存完后,又在 B 窗口打开同一文档,B 窗口保存时会覆盖 A 窗口的内容。
我在项目里简单处理为:保存时使用 file_put_contents 的原子写逻辑,先写入临时文件然后 rename 替换旧文件。这样即使并发写回,也不会出现文件在写入过程中被读到半个文件的问题。更严格一点的方案是加锁,比如在文件缓存目录里创建一个锁文件,保存期间其它写操作等待。但实际业务中多人同时编辑同一个文件是少数场景,先把原子写做到位即可。
5.3 文档 key 的生成规则
前面说过,config 里的 key 字段是 Document Server 识别文档版本的唯一依据。我使用的是 md5($fileId . '-' . $file->updated_at),这样不同文件不同更新时间,生成的 key 都不同。
这里有个细节要特别强调:key 只能包含数字、字母、点号、下划线和连字符。我在集成时踩过坑,用 md5 生成完全没问题,但如果你图方便直接用文件 ID 加时间戳的字符串,中间有冒号、斜杠这样的符号,Editor 可能直接拒绝加载。
还要注意,同一份文件在打开期间 key 不能变化。假设文件在编辑过程中被另一个接口更新了 updated_at,再用旧 config 打开编辑器时,Document Server 会认为这是一份新的文档,原来的编辑器就会失效。所以生成 key 的时机必须准确,建议在生成 config 时读取并锁定当时的文件属性,而不是每次请求都实时查库。
6. 常见问题与排查技巧实录
6.1 端口映射导致 302 问题
很多人把 Document Server 的端口映射成非标准端口,比如用 8080 映射 80,这时访问 http://IP:8080 会出现 302 跳转,跳到 443 端口,导致编辑器加载不出来。这个问题的根源是 Document Server 内部配置的端口与外部访问端口不一致。
解决办法分两类。如果你对网络环境有完全控制权,建议直接用默认的 80 和 443 端口,最省心。如果必须映射到其它端口,需要在 Document Server 的配置文件中调整 service.url 参数,让它知道自己被外部访问时带的是什么端口。具体方法是进入容器修改 /etc/onlyoffice/documentserver/local.json,把 service.url 设置为 http://你的域名:8080,然后重启容器。否则它做内部重定向时总是跳到 443,浏览器自然 302。
6.2 手机端无法切换工作表
OnlyOffice 在手机上的体验本来就不如桌面端,因为 Excel 工具栏和标签页都经过大量折叠。常见的“手机上怎么换到另一个工作表”问题,多数情况是因为页面在 iframe 里显示不全,底部的工作表标签被裁剪了。
解决办法有两个方向:一是把编辑器页面的 iframe 高度调成 100% 并且允许滚动,确保手指可以滑动窗口;二是在配置里把 editorConfig.customization 的 forceSave 设为 true,这样用户可以手动触发保存,避免在手机上找不到自动保存入口导致数据丢失。部分魔改版 OnlyOffice 对移动端的支持会好一些,但前提是确认来源可靠,不推荐随意使用未经审计的第三方版本。
6.3 ext-json 导致 Composer 安装失败
ThinkPHP 8 安装依赖时,一个经典报错就是“PHP 扩展 ext-json 不存在”。遇到这种情况先别着急改代码,先检查你的 PHP 版本。如果你用的是 PHP 8.0 以上,json 扩展默认内置,不应该报错;如果报错,最大的可能是命令行使用的 PHP 和 PHP-FPM 不是同一个版本,或者这个 PHP 是手动编译且禁用了 json。
我的排查步骤是:php -v 查看 PHP 版本,php -m | grep json 查看 json 扩展,which php 查看命令路径。如果发现是多个 PHP 版本共存导致的问题,直接用全路径指定 PHP 版本执行 Composer,或者在 Docker 容器里统一 PHP 环境。
6.4 编辑器白屏或一直转圈
白屏的原因有很多种,但最常见的是三种:api.js 地址错误、config 里的 token 校验失败、Document Server 无法访问文件 URL。
排查思路是用浏览器开发者工具看 Network 面板:如果 api.js 加载失败,看请求路径和 Document Server 地址是否一致;如果 api.js 加载成功但编辑器一直转圈,看 console 里是否有 token 报错,有的话检查 JWT 密钥配置;如果一切正常但文档区域空白,直接复制档案 URL 到浏览器里访问一下,看 Document Server 是否能正常拉取文档。曾经遇到一种情况是 ThinkPHP 的调试模式关闭后,URL 生成为伪静态路径,Document Server 拿这个路径请求时没有带查询参数导致 404,后来我把生成文档 URL 时用的控制器和方法明确指定,才恢复正常。
6.5 回调保存后内容没更新
保存接口返回 {"error": 0},但实际文件内容没变化。先确认回调是不是真的执行了,最简单的方法是在回调处理函数里加日志,记录收到的 status 和文件路径。如果日志里显示保存成功但文件没变,很可能是写到了错误路径,或者 Document Server 里 data['url'] 下载的内容不是最新版本。
这里有一个我踩过几次的坑:Document Server 保存回调的 data['url'] 是临时的,需要立即下载,不能等。如果处理时间长或者下载失败,这个临时连接会过期。另外,下载时要设置足够长的超时时间,大文件下载可能会超过 PHP 默认的 30 秒超时限制。我的做法是在 file_get_contents 前用 stream_context_create 设置超时为 120 秒,确保 100MB 级别的大文档也能完整下载。
6.6 如何设置 Document Server 的 SSL
如果业务系统已经用了 HTTPS,但 Document Server 还在跑 HTTP,则浏览器会有混合内容拦截,页面上的编辑器初始化会被浏览器直接阻断。最好的方案是给 Document Server 配置证书,弄成 HTTPS。没有独立域名不方便签证书的话,可以在反向代理层面解决。
我在项目里用 Nginx 做反向代理,把 https://onlyoffice.你的域名.com 转发到内网的 http://127.0.0.1:8080,同时把大文件上传和下载的超时时间都调到 300 秒。这种部署方式下,ThinkPHP 生成的 config 里所有 URL 都填 HTTPS 域名,Document Server 内部生成的回调请求也会用 HTTPS,问题就解决了。
写在最后
如果你在集成 OnlyOffice 的过程中遇到问题,我的建议是先不要抓代码,动手确认三个连通性:第一个是 Document Server 本身能不能正常打开,第二个是 ThinkPHP 生成的文档 URL 能否直接访问,第三个是 Document Server 所在机器能不能访问你的回调地址。三个连通性全通了,剩下的配置细节问题基本都能在日志里找到线索。
个人实操下来,这个集成的难点不在 ThinkPHP,毕竟 ThinkPHP 在整个链路里只负责生成 config 和接收回调,真正的复杂度集中在 OnlyOffice 的部署和参数语义理解上。先把官方 API 文档里 config 每个字段的含义过一遍,再动手写代码,效率会翻倍。如果只是想快速跑通一个 Demo,你完全可以把我的代码复制过去,改改配置就能跑,但务必要把 key、callbackUrl、JWT 签名这三个点想清楚,否则上线后迟早会被线上问题折腾回来。
