乐于分享
好东西不私藏

OnlyOffice 集成开发实战:Document Editor API 完全指南

OnlyOffice 集成开发实战:Document Editor API 完全指南

1. 集成架构全景回顾

在写任何代码之前,先把三方交互模型再清晰地过一遍——这是理解后续所有 API 行为的基础。

┌─────────────────────────────────────────────────────────────┐
│                        浏览器 / 客户端                        │
│                                                              │
│  1. 请求编辑页面          4. 初始化编辑器                    │
│  ─────────────>          ─────────────>                      │
│                                                              │
│  2. 返回含 config 的页面  5. WebSocket 实时通信               │
│  <─────────────          <────────────>                      │
└──────────┬──────────────────────────────────────────────────┘
           │                              ▲
           │ 3. 后端生成                   │
           │    JWT 签名的 config          │
           ▼                              │
┌──────────────────────┐       ┌──────────────────────────────┐
│                      │       │                              │
│    你的应用后端       │       │   OnlyOffice Document Server │
│                      │       │                              │
│  - 用户认证          │       │  6. 主动拉取文件             │
│  - 权限控制          │  ───> │     GET document.url         │
│  - 生成 config + JWT │       │                              │
│  - 接收保存回调      │  <─── │  7. 保存时回调               │
│  - 存储文件          │       │     POST callbackUrl         │
│                      │       │                              │
└──────────────────────┘       └──────────────────────────────┘
           │                              │
           │                              │
           ▼                              ▼
┌──────────────────────────────────────────────────────────────┐
│                        文件存储                               │
│          (本地磁盘 / S3 / OSS / 你的存储系统)                │
└──────────────────────────────────────────────────────────────┘

三个容易误解的关键点:

第一,文件由 Document Server 主动拉取,不是由你的后端推送。你只需要提供一个 Document Server 可以访问的文件 URL。这意味着你的文件存储服务必须对 Document Server 网络可达。

第二,文档保存通过回调触发,不是实时同步。用户关闭编辑器(或触发强制保存)后,Document Server 向你的 callbackUrl 发送 POST 请求,你从请求体中拿到新文件的下载链接,然后自行下载并保存。

第三,document.key 是 Document Server 判断是否使用缓存的唯一依据。同一个 key 对应的文档,Document Server 只拉取一次,后续打开直接用缓存。 如果文件内容已更新,必须更换 key,否则用户看到的是旧版本。


2. 最小化集成示例

在深入完整 API 之前,先用最少的代码跑通一个可用的集成,建立直觉。

2.1 后端:生成编辑器配置页面

// app.js(Node.js + Express)
const express = require('express');
const jwt     = require('jsonwebtoken');
const path    = require('path');
const app = express();
const DOCUMENT_SERVER_URL = 'https://docs.example.com';  // 你的 Document Server 地址
const JWT_SECRET          = process.env.JWT_SECRET;       // 与 Document Server 共享的密钥
app.get('/edit/:fileId', (req, res) => {
  const { fileId } = req.params;
  // 构造编辑器配置
  const config = {
    document: {
      fileType: 'docx',
      key:      fileId + '_v1',           // 文档唯一标识
      title:    '我的文档.docx',
      url:      `https://your-app.com/files/${fileId}`,   // 文件下载地址
    },
    editorConfig: {
      callbackUrl: `https://your-app.com/callback/${fileId}`,
      user: {
        id:   'user-001',
        name: '张三',
      },
      lang: 'zh',
      mode: 'edit',
    },
    documentType: 'word',
  };
  // 用 JWT 签名配置
  config.token = jwt.sign(config, JWT_SECRET, { algorithm: 'HS256' });
  // 渲染编辑器页面,将 config 传入前端
  res.send(`
</code></pre>            <pre><code>        html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; }</code></pre>            <pre><code>        #editor    { width: 100%; height: 100vh; }</code></pre>            <pre><code>      
</code></pre>            <pre><code>        const config = ${JSON.stringify(config)};</code></pre>            <pre><code>        const docEditor = new DocsAPI.DocEditor('editor', config);</code></pre>            <pre><code>      
  `);
});
app.listen(3000, () => console.log('Server running on port 3000'));

2.2 后端:接收保存回调

app.use(express.json());
app.post('/callback/:fileId', async (req, res) => {
  const { fileId } = req.params;
  const body       = req.body;
  // status 2 表示文档准备好保存(用户关闭编辑器)
  // status 6 表示强制保存(Force Save)触发
  if (body.status === 2 || body.status === 6) {
    // 从 Document Server 下载最新版本
    const response = await fetch(body.url);
    const buffer   = await response.arrayBuffer();
    // 保存到你的存储系统(示例为写入本地磁盘)
    const fs = require('fs');
    fs.writeFileSync(`./storage/${fileId}.docx`, Buffer.from(buffer));
    console.log(`文件 ${fileId} 保存成功`);
  }
  // 必须返回 {"error": 0},否则 Document Server 会认为回调失败并重试
  res.json({ error: 0 });
});

这个最简示例可以跑通完整的编辑 → 保存流程。接下来逐步展开每个环节的完整细节。


3. Document Editor API 完整参数详解

编辑器配置对象(config)是集成的核心,它决定了编辑器的几乎所有行为。以下按结构逐层展开。

3.1 顶层结构

const config = {
  documentType: 'word',        // 编辑器类型
  document:     { ... },       // 文档信息
  editorConfig: { ... },       // 编辑器行为配置
  events:       { ... },       // 前端事件回调
  token:        'jwt_string',  // JWT 签名(生产环境必须)
};

documentType 的可选值:

对应编辑器支持格式
wordDocument Editor.docx .doc .odt .txt .rtf .html
cellSpreadsheet Editor.xlsx .xls .ods .csv
slidePresentation Editor.pptx .ppt .odp

3.2 document 对象

document: {
  // ── 必填项 ──────────────────────────────────────────────
  fileType: 'docx',        // 文件扩展名(不含点),影响解析方式
  key:      'doc_abc_v3',  // 文档唯一标识,缓存控制的关键
  title:    '合同草稿.docx', // 显示在编辑器标题栏的文件名
  url:      'https://your-storage.com/files/abc.docx', // 文件下载地址
  // ── 权限控制 ─────────────────────────────────────────────
  permissions: {
    comment:          true,   // 允许添加批注
    copy:             true,   // 允许复制内容
    download:         true,   // 允许下载文件
    edit:             true,   // 允许编辑(false 则为只读模式)
    fillForms:        true,   // 允许填写表单
    modifyContentControl: true, // 允许修改内容控件
    modifyFilter:     true,   // 允许修改过滤器(表格)
    print:            true,   // 允许打印
    protect:          true,   // 允许修改文档保护设置
    rename:           false,  // 禁止在编辑器内重命名
    review:           true,   // 允许修订模式
    reviewGroups:     null,   // 修订组控制(高级用法)
    chat:             true,   // 允许聊天面板(需配置聊天后端)
    changeHistory:    true,   // 允许查看版本历史
    userInfoGroups:   null,   // 用户信息组(高级用法)
  },
  // ── 文档信息(可选)──────────────────────────────────────
  info: {
    author:    '张三',
    created:   '2024-01-15',    // 文档创建日期(显示用)
    owner:     '研发部',
    uploaded:  '2024-03-20',
    favorite:  null,             // 是否标记为收藏(true/false/null)
  },
  // ── 参考文档(只读,与主文档并列显示)────────────────────
  // referenceData: {
  //   fileKey:    'ref_doc_key',
  //   instanceId: 'your-app-id',
  // },
}

`document.key` 的管理策略

key 是集成中最容易出错的地方,规则很简单但必须严格执行:

  • 文件内容没有变化时,保持 key 不变(利用缓存,加快打开速度)
  • 文件内容已更新时(例如其他系统修改了文件),必须生成新 key
  • 推荐策略:使用 文件ID + 文件最后修改时间戳 的哈希值作为 key
const crypto = require('crypto');
function generateDocumentKey(fileId, lastModifiedAt) {
  return crypto
    .createHash('md5')
    .update(`${fileId}_${lastModifiedAt}`)
    .digest('hex')
    .substring(0, 20);
}

3.3 editorConfig 对象

editorConfig: {
  // ── 必填项 ──────────────────────────────────────────────
  callbackUrl: 'https://your-app.com/callback/file-id',
  // ── 用户信息 ─────────────────────────────────────────────
  user: {
    id:    'user-001',   // 用户唯一 ID,协同编辑时用于区分用户
    name:  '张三',        // 显示在编辑器中的用户名
    group: 'editors',    // 所属组(用于修订组权限控制)
    image: 'https://your-app.com/avatars/user-001.png', // 头像 URL
  },
  // ── 语言与地区 ───────────────────────────────────────────
  lang:   'zh',          // 界面语言(zh / en / de / fr 等)
  region: 'zh-CN',       // 地区格式(影响日期、数字格式)
  // ── 编辑模式 ─────────────────────────────────────────────
  // edit      编辑模式(默认)
  // view      只读预览模式(不占用连接数许可)
  // fillForms 表单填写模式
  mode: 'edit',
  // ── 协作配置 ─────────────────────────────────────────────
  coEditing: {
    // fast   快速模式(操作实时同步,默认)
    // strict 严格模式(段落锁定后提交)
    mode:   'fast',
    change: true,   // 允许用户切换协作模式
  },
  // ── 修订配置 ─────────────────────────────────────────────
  // auto     自动跟踪所有修改
  // disable  不跟踪修改
  // enable   启用修订跟踪
  // show     显示已有修订
  review: {
    hideReviewDisplay: false,
    showReviewChanges: false,
    reviewDisplay:     'original',  // original / markup / final
    trackChanges:      null,        // null 表示使用文档自身设置
  },
  // ── 自定义菜单(在编辑器内嵌入你自己系统的入口)───────────
  customization: {
    // 控制内置 UI 元素显示
    autosave:         true,    // 是否显示自动保存提示
    comments:         true,    // 是否显示批注面板
    compactHeader:    false,   // 使用紧凑型标题栏
    compactToolbar:   false,   // 使用紧凑型工具栏
    compatibleFeatures: false, // 兼容模式(禁用部分新特性)
    forcesave:        false,   // 是否显示"强制保存"按钮
    help:             true,    // 是否显示帮助菜单
    hideRightMenu:    false,   // 是否隐藏右侧菜单
    hideRulers:       false,   // 是否隐藏标尺
    integrationMode:  'embed', // 集成模式:embed(内嵌)/ full(独立)
    logo: {
      // 替换编辑器左上角 Logo
      image:       'https://your-app.com/logo.png',
      imageDark:   'https://your-app.com/logo-dark.png',
      url:         'https://your-app.com',
      visible:     true,
    },
    macros:          true,     // 是否允许运行宏
    macrosMode:      'warn',   // 宏运行策略:warn / enable / disable
    mentionShare:    true,     // @提及时是否提示分享
    mobileForceView: true,     // 移动端强制使用查看模式
    plugins:         true,     // 是否启用插件
    spellcheck:      true,     // 是否开启拼写检查
    submitForm:      false,    // 是否显示"提交表单"按钮
    toolbarHideFileName: false, // 是否在工具栏隐藏文件名
    toolbarNoTabs:    false,   // 是否使用无标签页工具栏样式
    uiTheme:         'theme-light',  // 主题:theme-light / theme-dark
    unit:            'cm',     // 标尺单位:cm / pt / inch
    zoom:            100,      // 初始缩放比例(百分比)
  },
  // ── 菜单按钮自定义 ──────────────────────────────────────
  // 在 File 菜单中添加自定义按钮,点击时触发 onRequestSaveAs 等事件
  createUrl: 'https://your-app.com/new-document',  // "创建新文档"按钮链接
  // ── 插入外部图片 / 文档 ─────────────────────────────────
  // 当用户点击"插入图片"时,是否允许从外部 URL 插入
  // 通过 events.onRequestInsertImage 配合使用
  // ── 最近文档列表 ─────────────────────────────────────────
  recent: [
    {
      title:   '上周的报告.docx',
      url:     'https://your-app.com/edit/doc-456',
      folder:  '我的文档',
    },
  ],
  // ── 模板列表 ─────────────────────────────────────────────
  templates: [
    {
      title:  '合同模板',
      image:  'https://your-app.com/templates/contract-thumb.png',
      url:    'https://your-app.com/edit/template-contract',
    },
  ],
}

3.4 events 对象:前端事件回调

events 是集成深度的重要体现。通过监听编辑器事件,可以在用户执行特定操作时注入你自己的系统逻辑。

events: {
  // ── 生命周期事件 ─────────────────────────────────────────
  onAppReady: function() {
    // 编辑器初始化完成,此后可安全调用 docEditor 的方法
    console.log('编辑器已就绪');
  },
  onDocumentReady: function() {
    // 文档内容加载完成(文件已渲染到编辑器)
    console.log('文档已加载');
  },
  onDocumentStateChange: function(event) {
    // 文档修改状态变化
    // event.data === true  → 有未保存的修改
    // event.data === false → 已保存
    if (event.data) {
      document.title = '* 合同草稿.docx(已修改)';
    } else {
      document.title = '合同草稿.docx';
    }
  },
  onError: function(event) {
    // 编辑器发生错误
    console.error('编辑器错误:', event.data);
    // event.data 包含错误代码和描述
  },
  onWarning: function(event) {
    console.warn('编辑器警告:', event.data);
  },
  onInfo: function(event) {
    // 编辑器信息通知(如文档被其他用户修改)
  },
  // ── 保存相关事件 ─────────────────────────────────────────
  onRequestSave: function() {
    // 用户点击"保存"按钮(需在 customization 中启用保存按钮)
    // 调用强制保存接口
    docEditor.forceSave();
  },
  onRequestSaveAs: function(event) {
    // 用户点击"另存为"
    // event.data.fileType → 目标文件类型
    // event.data.title    → 用户输入的文件名
    // event.data.url      → 当前文档的下载链接
    console.log('另存为:', event.data.title, event.data.fileType);
    // 在你的系统中创建新文件
  },
  // ── 文件操作事件 ─────────────────────────────────────────
  onRequestOpen: function(event) {
    // 用户点击"打开文档"或最近文件列表中的文档
    // event.data.url → 要打开的文档 URL
    window.open(event.data.url, '_blank');
  },
  onRequestClose: function() {
    // 用户点击"关闭"按钮
    // 此处可跳转回文件列表页面
    window.location.href = '/files';
  },
  onRequestCreateNew: function() {
    // 用户点击"新建文档"
    window.open('/new-document', '_blank');
  },
  // ── 用户与协作事件 ───────────────────────────────────────
  onCollaborativeChanges: function() {
    // 其他协作者对文档进行了修改(快速模式下不触发,严格模式下触发)
  },
  onRequestUsers: function(event) {
    // 编辑器请求用户列表(用于 @提及功能)
    // event.data.c === 'mention' → 获取可提及的用户列表
    // event.data.c === 'protect' → 获取可设置文档保护的用户
    if (event.data.c === 'mention') {
      // 从你的系统获取用户列表后,通过 setUsers 方法返回
      docEditor.setUsers({
        c: 'mention',
        users: [
          { id: 'user-002', name: '李四', email: 'lisi@company.com' },
          { id: 'user-003', name: '王五', email: 'wangwu@company.com' },
        ],
      });
    }
  },
  onRequestSendNotify: function(event) {
    // 用户在批注中 @提及了某人
    // event.data.emails  → 被提及者的邮箱列表
    // event.data.message → 提及的消息内容
    // 在此处触发你的通知系统(邮件/消息推送)
    sendMentionNotification(event.data.emails, event.data.message);
  },
  // ── 文档内容事件 ─────────────────────────────────────────
  onRequestInsertImage: function(event) {
    // 用户点击"从存储插入图片"(需配置 editorConfig.customization.features.spellcheck)
    // 展示你的图片选择器,选择后调用 insertImage
    showImagePicker(function(imageUrl) {
      docEditor.insertImage({
        c:   event.data.c,
        images: [{ fileType: 'png', url: imageUrl }],
      });
    });
  },
  onRequestCompareFile: function() {
    // 用户请求"比较文档"功能
    // 展示文件选择器,选择后调用 setRevisedFile
    showFilePicker(function(fileInfo) {
      docEditor.setRevisedFile({
        fileType: fileInfo.fileType,
        url:      fileInfo.url,
        token:    fileInfo.jwt,
      });
    });
  },
  onRequestHistory: function() {
    // 用户请求版本历史列表
    // 从你的系统获取历史版本后,调用 refreshHistory
    fetchVersionHistory(currentDocId).then(versions => {
      docEditor.refreshHistory({
        currentVersion: versions.currentVersion,
        history:        versions.list,
      });
    });
  },
  onRequestHistoryData: function(event) {
    // 用户点击了某个历史版本,请求该版本的文件内容
    // event.data.version → 请求的版本号
    const version = event.data.version;
    fetchVersionData(currentDocId, version).then(data => {
      docEditor.setHistoryData({
        fileType:  data.fileType,
        key:       data.key,
        url:       data.url,
        version:   version,
        // 如果需要显示与上一版本的差异:
        // changesUrl: data.changesUrl,
        // previous: { fileType, key, url }
      });
    });
  },
  onRequestHistoryClose: function() {
    // 用户关闭版本历史面板
    docEditor.refreshHistory({ currentVersion: 1, history: [] });
  },
  onRequestRename: function(event) {
    // 用户在编辑器内重命名了文件(需 permissions.rename: true)
    // event.data.title → 新文件名
    renameFile(currentDocId, event.data.title);
  },
  onRequestRestore: function(event) {
    // 用户请求恢复到某个历史版本
    // event.data.version → 要恢复的版本号
    // event.data.url     → 该版本文件的下载链接
    restoreFileVersion(currentDocId, event.data.version, event.data.url);
  },
}

4. 回调机制:处理文档保存

回调(Callback)是集成中逻辑最复杂、最容易出错的环节,需要完整理解每种状态码的含义及其处理方式。

4.1 回调请求体结构

Document Server 向 callbackUrl 发送 POST 请求,请求体为 JSON:

{
  "actions":   [{ "type": 0, "userid": "user-001" }],
  "changesurl": "https://docs.example.com/cache/files/doc_key/changes.zip",
  "history": {
    "changes":       [...],
    "serverVersion": "7.5.0"
  },
  "key":    "doc_abc_v3",
  "status": 2,
  "url":    "https://docs.example.com/cache/files/doc_key/output.docx",
  "users":  ["user-001", "user-002"]
}

4.2 status 状态码完整说明

这是回调处理的核心,必须正确处理每种状态:

status含义是否包含 url推荐处理方式
0文档未找到或错误记录错误,返回 {"error": 1}
1文档正在被编辑中仅记录在线用户,返回 {"error": 0}
2文档准备好保存(所有用户已关闭)下载并保存文件
3文档保存时发生错误记录错误,通知管理员
4文档已关闭,无修改无需操作,返回 {"error": 0}
6强制保存(Force Save)触发下载并保存文件
7强制保存时发生错误记录错误

最重要的规则: 无论处理成功还是失败,回调接口必须在 15 秒内返回 HTTP 200 + {"error": 0}(成功)或 {"error": 1}(失败)。如果超时或返回非 200,Document Server 会认为回调失败并重试,最多重试 48 次,期间文档处于锁定状态无法重新编辑。

4.3 完整的回调处理实现

// Node.js + Express
const https  = require('https');
const fs     = require('fs');
const path   = require('path');
const jwt    = require('jsonwebtoken');
const stream = require('stream');
const { promisify } = require('util');
const pipeline = promisify(stream.pipeline);
const JWT_SECRET = process.env.JWT_SECRET;
app.post('/callback/:fileId', async (req, res) => {
  const { fileId } = req.params;
  // ── 第一步:验证 JWT 签名 ────────────────────────────────
  const authHeader = req.headers['authorization'];
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    console.error(`[Callback] 缺少 JWT Token,fileId=${fileId}`);
    return res.json({ error: 1 });
  }
  try {
    jwt.verify(authHeader.slice(7), JWT_SECRET);
  } catch (err) {
    console.error(`[Callback] JWT 验签失败,fileId=${fileId}:`, err.message);
    return res.json({ error: 1 });
  }
  const body   = req.body;
  const status = body.status;
  console.log(`[Callback] fileId=${fileId}, status=${status}, users=${JSON.stringify(body.users)}`);
  try {
    switch (status) {
      case 1:
        // 文档编辑中,记录在线用户,无需其他操作
        await updateOnlineUsers(fileId, body.users || []);
        break;
      case 2:   // 文档准备保存
      case 6: { // 强制保存
        if (!body.url) {
          console.error(`[Callback] status=${status} 但缺少 url,fileId=${fileId}`);
          return res.json({ error: 1 });
        }
        // 下载并保存文件
        await downloadAndSave(fileId, body.url);
        // 更新数据库中的文件版本信息
        await updateFileVersion(fileId, {
          savedAt:  new Date(),
          editedBy: body.users || [],
        });
        // 清除在线用户缓存
        if (status === 2) {
          await clearOnlineUsers(fileId);
        }
        console.log(`[Callback] 文件 ${fileId} 保存成功`);
        break;
      }
      case 3:
        console.error(`[Callback] 文档保存错误,fileId=${fileId}`, body);
        await notifyAdminSaveError(fileId, body);
        break;
      case 4:
        // 文档已关闭,无修改,清除在线用户
        await clearOnlineUsers(fileId);
        break;
      case 7:
        console.error(`[Callback] 强制保存错误,fileId=${fileId}`, body);
        break;
      default:
        console.warn(`[Callback] 未知状态码 status=${status},fileId=${fileId}`);
    }
    // 必须返回 {"error": 0}
    res.json({ error: 0 });
  } catch (err) {
    console.error(`[Callback] 处理异常,fileId=${fileId}:`, err);
    // 即使处理失败,也返回 error: 0 阻止重试(已记录日志,可人工处理)
    // 如果希望 Document Server 重试,返回 error: 1
    res.json({ error: 0 });
  }
});
// 下载文件并保存到本地存储
async function downloadAndSave(fileId, downloadUrl) {
  const tempPath  = path.join('./storage/tmp', `${fileId}_${Date.now()}.docx`);
  const finalPath = path.join('./storage', `${fileId}.docx`);
  // 先下载到临时文件,下载完成后原子替换(防止下载中断导致文件损坏)
  await downloadFile(downloadUrl, tempPath);
  fs.renameSync(tempPath, finalPath);
}
function downloadFile(url, destPath) {
  return new Promise((resolve, reject) => {
    const file = fs.createWriteStream(destPath);
    https.get(url, (response) => {
      if (response.statusCode !== 200) {
        file.close();
        fs.unlinkSync(destPath);
        return reject(new Error(`下载失败,HTTP ${response.statusCode}`));
      }
      response.pipe(file);
      file.on('finish', () => file.close(resolve));
      file.on('error',  (err) => { fs.unlinkSync(destPath); reject(err); });
    }).on('error', reject);
  });
}

4.4 强制保存(Force Save)

强制保存允许在用户仍在编辑时触发中间保存,有两种触发方式:

方式一:通过 Document Server 命令接口触发

// 向 Document Server 发送 forcesave 命令
async function triggerForceSave(documentKey) {
  const command = {
    c:   'forcesave',
    key: documentKey,
  };
  // 对命令签名
  const token = jwt.sign(command, JWT_SECRET);
  const response = await fetch(
    `${DOCUMENT_SERVER_URL}/coauthoring/CommandService.ashx`,
    {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ ...command, token }),
    }
  );
  const result = await response.json();
  // result.error === 0 表示成功触发
  return result;
}

方式二:在编辑器前端触发

// 通过编辑器 API 触发(用户在当前标签页编辑时)
docEditor.forceSave();

5. 文件转换 API

文件格式转换是 Document Server 的独立能力,不需要打开编辑器即可调用。

5.1 同步转换

async function convertFile(fileUrl, fromType, toType, fileKey) {
  const requestBody = {
    async:      false,          // 同步模式:等待转换完成返回结果
    filetype:   fromType,       // 源文件格式(不含点,如 'docx')
    key:        fileKey,        // 转换任务唯一标识
    outputtype: toType,         // 目标格式(如 'pdf')
    title:      `output.${toType}`,
    url:        fileUrl,        // 源文件的可访问 URL
  };
  const token = jwt.sign(requestBody, JWT_SECRET);
  const response = await fetch(
    `${DOCUMENT_SERVER_URL}/ConvertService.ashx`,
    {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ ...requestBody, token }),
    }
  );
  const result = await response.json();
  // result.endConvert === true  → 转换完成
  // result.fileUrl              → 转换结果下载链接
  // result.error                → 错误码(0 表示无错误)
  if (result.endConvert && result.fileUrl) {
    return result.fileUrl;
  } else {
    throw new Error(`转换失败,错误码:${result.error}`);
  }
}
// 使用示例:将 docx 转换为 pdf
const pdfUrl = await convertFile(
  'https://your-storage.com/files/report.docx',
  'docx',
  'pdf',
  'convert_task_001'
);

5.2 异步转换(适用于大文件)

async function convertFileAsync(fileUrl, fromType, toType, fileKey) {
  const requestBody = {
    async:      true,           // 异步模式:立即返回,通过 key 轮询结果
    filetype:   fromType,
    key:        fileKey,
    outputtype: toType,
    url:        fileUrl,
  };
  const token = jwt.sign(requestBody, JWT_SECRET);
  // 发起转换请求
  const response = await fetch(
    `${DOCUMENT_SERVER_URL}/ConvertService.ashx`,
    {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ ...requestBody, token }),
    }
  );
  let result = await response.json();
  // 如果尚未完成,轮询进度(最多等待 5 分钟)
  const maxAttempts = 60;
  let   attempts    = 0;
  while (!result.endConvert && attempts < maxAttempts) {
    await new Promise(r => setTimeout(r, 5000));  // 每 5 秒轮询一次
    const pollBody  = { async: true, filetype: fromType, key: fileKey, outputtype: toType, url: fileUrl };
    const pollToken = jwt.sign(pollBody, JWT_SECRET);
    const pollResponse = await fetch(
      `${DOCUMENT_SERVER_URL}/ConvertService.ashx`,
      {
        method:  'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${pollToken}` },
        body:    JSON.stringify({ ...pollBody, token: pollToken }),
      }
    );
    result = await pollResponse.json();
    attempts++;
    console.log(`转换进度:${result.percent || 0}%`);
  }
  if (result.endConvert && result.fileUrl) {
    return result.fileUrl;
  } else {
    throw new Error(`转换超时或失败,最后状态:${JSON.stringify(result)}`);
  }
}

5.3 支持的转换格式矩阵

常用的格式转换方向:

源格式可转换的目标格式
docxpdf, odt, rtf, txt, html, epub, fb2
xlsxpdf, ods, csv
pptxpdf, odp
odtdocx, pdf, rtf, txt
odsxlsx, pdf, csv
odppptx, pdf
txtdocx, pdf
csvxlsx
htmldocx, pdf

6. 协同会话管理

6.1 查询当前编辑文档的用户

通过 CommandService 接口可以查询当前在编辑某个文档的用户列表:

async function getEditingUsers(documentKey) {
  const command = { c: 'info', key: documentKey };
  const token   = jwt.sign(command, JWT_SECRET);
  const response = await fetch(
    `${DOCUMENT_SERVER_URL}/coauthoring/CommandService.ashx`,
    {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ ...command, token }),
    }
  );
  const result = await response.json();
  // result.users → 当前在线编辑的用户 ID 列表
  return result.users || [];
}

6.2 强制关闭文档编辑会话

某些场景下需要强制结束所有用户的编辑会话(如文件权限变更、文件被删除):

async function forceCloseDocument(documentKey) {
  const command = { c: 'drop', key: documentKey, users: [] };
  // users 为空数组表示关闭所有用户的会话
  // users 传入用户 ID 列表表示仅关闭特定用户的会话
  const token = jwt.sign(command, JWT_SECRET);
  const response = await fetch(
    `${DOCUMENT_SERVER_URL}/coauthoring/CommandService.ashx`,
    {
      method:  'POST',
      headers: {
        'Content-Type':  'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ ...command, token }),
    }
  );
  return await response.json();
}

注意: 调用 drop 命令后,Document Server 会触发文档的保存回调(status 2),确保保存回调逻辑正确处理这种情况。

6.3 文档 key 的更新(强制刷新缓存)

当外部系统修改了文件内容(如批量替换内容、合并分支),需要通知 Document Server 放弃缓存,下次打开时重新拉取文件:

正确做法是更新传给前端的 config 中的 `document.key`,而不是调用任何 API。Document Server 看到新 key 时,会自动识别这是一个新版本,重新拉取文件。

如果有用户正在编辑旧版本,处理方式取决于业务策略:

// 策略一:先强制关闭旧会话,再提供新 key(数据安全优先)
await forceCloseDocument(oldKey);
// 之后用新 key 重新打开文档
// 策略二:用旧 key 触发强制保存,保存完成后更新 key(减少数据丢失)
await triggerForceSave(oldKey);
// 在回调处理中检测到 status === 6 后,更新数据库中的 key

7. 完整集成示例:从上传到保存

将前面所有环节串联起来,给出一个接近生产可用的完整示例。

7.1 项目结构

onlyoffice-integration/
├── app.js              # 主应用
├── routes/
│   ├── editor.js       # 编辑器页面路由
│   └── callback.js     # 回调处理路由
├── services/
│   ├── jwt.js          # JWT 工具
│   ├── storage.js      # 文件存储服务
│   └── onlyoffice.js   # OnlyOffice API 封装
├── views/
│   └── editor.html     # 编辑器页面模板
└── .env

7.2 JWT 工具模块

// services/jwt.js
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
if (!SECRET || SECRET.length < 32) {
  throw new Error('JWT_SECRET 未配置或长度不足 32 位');
}
module.exports = {
  sign(payload) {
    return jwt.sign(payload, SECRET, { algorithm: 'HS256' });
  },
  verify(token) {
    return jwt.verify(token, SECRET);
  },
  verifyFromHeader(authHeader) {
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new Error('缺少 Authorization Header 或格式不正确');
    }
    return this.verify(authHeader.slice(7));
  },
};

7.3 编辑器路由

// routes/editor.js
const express   = require('express');
const router    = express.Router();
const jwtHelper = require('../services/jwt');
const storage   = require('../services/storage');
const crypto    = require('crypto');
const DOCUMENT_SERVER_URL = process.env.DOCUMENT_SERVER_URL;
const APP_BASE_URL        = process.env.APP_BASE_URL;
// 根据文件 ID 和最后修改时间生成 document.key
function makeDocKey(fileId, updatedAt) {
  return crypto
    .createHash('sha256')
    .update(`${fileId}_${updatedAt.getTime()}`)
    .digest('hex')
    .substring(0, 20);
}
router.get('/:fileId', async (req, res) => {
  try {
    const { fileId } = req.params;
    const userId     = req.user.id;    // 来自你的认证中间件
    const userName   = req.user.name;
    // 从数据库查询文件信息
    const file = await storage.getFileById(fileId);
    if (!file) return res.status(404).send('文件不存在');
    // 检查用户权限
    const canEdit = await storage.checkPermission(userId, fileId, 'edit');
    const config = {
      documentType: getDocumentType(file.extension),
      document: {
        fileType: file.extension,
        key:      makeDocKey(fileId, file.updatedAt),
        title:    file.name,
        url:      `${APP_BASE_URL}/api/files/${fileId}/download`,
        permissions: {
          edit:     canEdit,
          comment:  true,
          download: true,
          print:    true,
          review:   canEdit,
        },
      },
      editorConfig: {
        callbackUrl: `${APP_BASE_URL}/api/callback/${fileId}`,
        user: { id: String(userId), name: userName },
        lang:        'zh',
        mode:        canEdit ? 'edit' : 'view',
        coEditing:   { mode: 'fast', change: true },
        customization: {
          forcesave:      true,
          autosave:       true,
          logo:           { visible: false },
          integrationMode: 'embed',
        },
      },
    };
    // 签名
    config.token = jwtHelper.sign(config);
    res.render('editor', {
      config:            JSON.stringify(config),
      documentServerUrl: DOCUMENT_SERVER_URL,
    });
  } catch (err) {
    console.error('[Editor] 路由错误:', err);
    res.status(500).send('服务器内部错误');
  }
});
function getDocumentType(ext) {
  if (['doc', 'docx', 'odt', 'rtf', 'txt'].includes(ext)) return 'word';
  if (['xls', 'xlsx', 'ods', 'csv'].includes(ext))         return 'cell';
  if (['ppt', 'pptx', 'odp'].includes(ext))                return 'slide';
  return 'word';
}
module.exports = router;

7.4 回调路由

// routes/callback.js
const express   = require('express');
const router    = express.Router();
const jwtHelper = require('../services/jwt');
const storage   = require('../services/storage');
const https     = require('https');
const fs        = require('fs');
const path      = require('path');
router.post('/:fileId', async (req, res) => {
  const { fileId } = req.params;
  // 1. 验签
  try {
    jwtHelper.verifyFromHeader(req.headers['authorization']);
  } catch (err) {
    console.error(`[Callback] 验签失败 fileId=${fileId}:`, err.message);
    return res.json({ error: 1 });
  }
  const { status, url, users, actions, key } = req.body;
  console.log(`[Callback] fileId=${fileId} status=${status} key=${key}`);
  try {
    if (status === 1) {
      await storage.updateOnlineUsers(fileId, users || []);
    } else if (status === 2 || status === 6) {
      await saveFile(fileId, url);
      if (status === 2) {
        await storage.updateOnlineUsers(fileId, []);
      }
    } else if (status === 3 || status === 7) {
      console.error(`[Callback] 保存错误 fileId=${fileId} status=${status}`);
    } else if (status === 4) {
      await storage.updateOnlineUsers(fileId, []);
    }
    res.json({ error: 0 });
  } catch (err) {
    console.error(`[Callback] 处理失败 fileId=${fileId}:`, err);
    res.json({ error: 0 });
  }
});
async function saveFile(fileId, downloadUrl) {
  const tmpPath   = path.join('./tmp', `${fileId}_${Date.now()}`);
  const finalPath = await storage.getFilePath(fileId);
  await new Promise((resolve, reject) => {
    const file = fs.createWriteStream(tmpPath);
    https.get(downloadUrl, res => {
      if (res.statusCode !== 200) {
        file.close();
        return reject(new Error(`HTTP ${res.statusCode}`));
      }
      res.pipe(file);
      file.on('finish', () => file.close(resolve));
      file.on('error',  reject);
    }).on('error', reject);
  });
  fs.renameSync(tmpPath, finalPath);
  await storage.updateFileTimestamp(fileId);
}
module.exports = router;

8. 多语言 SDK 参考

OnlyOffice 官方提供了多语言集成 SDK,以下是各语言 JWT 签名的关键实现。

PHP:

use Firebase\JWT\JWT;
$jwtSecret = getenv('JWT_SECRET');
function generateEditorConfig($fileId, $userId, $userName) {
    global $jwtSecret;
    $config = [
        'documentType' => 'word',
        'document' => [
            'fileType' => 'docx',
            'key'      => $fileId . '_' . time(),
            'title'    => '文档.docx',
            'url'      => 'https://your-app.com/files/' . $fileId,
        ],
        'editorConfig' => [
            'callbackUrl' => 'https://your-app.com/callback/' . $fileId,
            'user'        => ['id' => $userId, 'name' => $userName],
            'lang'        => 'zh',
            'mode'        => 'edit',
        ],
    ];
    $config['token'] = JWT::encode($config, $jwtSecret, 'HS256');
    return $config;
}

Java(Spring Boot):

@Service
public class OnlyOfficeService {
    @Value("${onlyoffice.jwt.secret}")
    private String jwtSecret;
    @Value("${onlyoffice.server.url}")
    private String serverUrl;
    public Map generateConfig(String fileId,
                                               String userId,
                                               String userName) {
        Map document = new LinkedHashMap<>();
        document.put("fileType", "docx");
        document.put("key",      fileId + "_" + System.currentTimeMillis());
        document.put("title",    "文档.docx");
        document.put("url",      "https://your-app.com/files/" + fileId);
        Map user = new LinkedHashMap<>();
        user.put("id",   userId);
        user.put("name", userName);
        Map editorConfig = new LinkedHashMap<>();
        editorConfig.put("callbackUrl", "https://your-app.com/callback/" + fileId);
        editorConfig.put("user",        user);
        editorConfig.put("lang",        "zh");
        editorConfig.put("mode",        "edit");
        Map config = new LinkedHashMap<>();
        config.put("documentType", "word");
        config.put("document",     document);
        config.put("editorConfig", editorConfig);
        String token = Jwts.builder()
            .setClaims(config)
            .signWith(SignatureAlgorithm.HS256,
                      jwtSecret.getBytes(StandardCharsets.UTF_8))
            .compact();
        config.put("token", token);
        return config;
    }
}

Python(Django / Flask):

import jwt
import hashlib
import time
import os
JWT_SECRET = os.environ.get('JWT_SECRET')
def generate_config(file_id: str, user_id: str, user_name: str) -> dict:
    key = hashlib.md5(f"{file_id}_{int(time.time())}".encode()).hexdigest()[:20]
    config = {
        'documentType': 'word',
        'document': {
            'fileType': 'docx',
            'key':      key,
            'title':    '文档.docx',
            'url':      f'https://your-app.com/files/{file_id}',
            'permissions': {
                'edit':    True,
                'comment': True,
            },
        },
        'editorConfig': {
            'callbackUrl': f'https://your-app.com/callback/{file_id}',
            'user':        {'id': user_id, 'name': user_name},
            'lang':        'zh',
            'mode':        'edit',
        },
    }
    config['token'] = jwt.encode(config, JWT_SECRET, algorithm='HS256')
    return config

9. 常见陷阱与调试技巧

陷阱一:document.key 未更新导致用户看到旧文档

现象: 文件内容已更新,但用户打开编辑器看到的仍是旧版本。

原因:document.key 没有随文件内容更新而更换,Document Server 使用了缓存。

解决: 使用文件内容哈希或最后修改时间戳生成 key,确保文件变化时 key 一定变化。

陷阱二:回调地址使用了 localhost

现象: 用户关闭编辑器后,文件未保存,日志显示 connection refused 或 connection timeout

原因: Document Server 是从容器内部访问 callbackUrl 的,容器内的 localhost 指向容器自身,不是宿主机。

解决: 回调地址使用宿主机的内网 IP 或域名,不要使用 localhost 或 127.0.0.1

陷阱三:回调响应超时导致文档锁定

现象: 文档保存成功,但此后同一文档无法再次编辑,始终显示"正在保存"。

原因: 回调处理耗时超过 15 秒(如文件下载慢、数据库操作慢),Document Server 判定回调失败,文档进入重试锁定状态。

解决: 回调接口应立即返回 {"error": 0},将实际的文件下载和保存操作异步处理。

app.post('/callback/:fileId', async (req, res) => {
  // 立即响应,不等待文件保存完成
  res.json({ error: 0 });
  // 异步处理文件保存(不阻塞响应)
  if (req.body.status === 2 || req.body.status === 6) {
    saveFileAsync(req.params.fileId, req.body.url)
      .catch(err => console.error('异步保存失败:', err));
  }
});

陷阱四:多实例部署时协同编辑异常

现象: 多用户协同时,有用户看不到他人的修改,或出现内容回滚。

原因: 负载均衡将同一文档的不同用户路由到了不同的 Document Server 实例,各实例独立维护会话状态。

解决: 在负载均衡器上配置基于 document.key 的会话亲和(Sticky Session),确保同一文档的所有请求路由到同一实例。

陷阱五:iframe 中 WebSocket 连接被拦截

现象: 编辑器在 iframe 中加载正常,但协同编辑不工作,浏览器控制台显示 WebSocket 连接失败。

原因: 父页面的 CSP(Content Security Policy)策略没有允许 Document Server 域名的 WebSocket 连接。

解决: 在父页面的 HTTP 响应头中添加:

Content-Security-Policy: connect-src 'self' wss://docs.example.com;
                         frame-src 'self' https://docs.example.com;

调试技巧:使用浏览器开发者工具

集成问题的 80% 可以通过浏览器开发者工具定位:

  • Network 标签:查找对 api.js 的请求是否 200,查找 WebSocket 连接状态是否 101
  • Console 标签:OnlyOffice 的错误会通过 onError 事件输出,确保你的 events.onError 做了日志记录
  • Application 标签:检查是否有跨域 Cookie 问题影响认证

10. 集成安全核查

在集成上线前,逐项确认:

JWT 安全

  • JWT Secret 长度不少于 32 位,使用随机生成的字符串
  • Document Server 侧 JWT_ENABLED=true
  • 集成方后端对回调请求进行 JWT 验签,拒绝无效 Token 的请求
  • document.url 和 callbackUrl 均为 HTTPS

文件访问控制

  • 提供给 Document Server 的 document.url 需要携带有效的访问凭证(Token 参数或 Header)
  • 该访问凭证应为短期有效(建议不超过 1 小时),防止链接被滥用
  • 回调处理中验证 fileId 是否属于当前用户,防止越权覆盖他人文件

网络隔离

  • Document Server 的非 HTTP 端口(PostgreSQL、RabbitMQ、Redis)不对外暴露
  • 如果是内网部署,通过白名单限制 Document Server 可访问的回调地址范围

权限控制

  • 在生成 config 时从你的系统查询权限,不信任前端传来的权限参数
  • permissions.edit 等字段在服务端生成,不允许客户端修改

小结

本文从最小化集成示例出发,逐步展开了 Document Editor API 的完整参数体系、回调处理的状态机逻辑、文件转换接口,以及协同会话管理的常用操作。

集成工作中最容易出问题的三个环节是:document.key 的缓存管理、回调响应的超时处理、以及 JWT 签名的双向验证。把这三个地方做扎实,集成稳定性就有了基本保证。

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-04-12 08:58:46 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/511259.html
  2. 运行时间 : 0.206866s [ 吞吐率:4.83req/s ] 内存消耗:4,896.66kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=2c20e48f1562ae1520382fb87713645b
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.80 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000870s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001359s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000621s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000626s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001190s ]
  6. SELECT * FROM `set` [ RunTime:0.000553s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001358s ]
  8. SELECT * FROM `article` WHERE `id` = 511259 LIMIT 1 [ RunTime:0.001174s ]
  9. UPDATE `article` SET `lasttime` = 1775955526 WHERE `id` = 511259 [ RunTime:0.002688s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000608s ]
  11. SELECT * FROM `article` WHERE `id` < 511259 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001090s ]
  12. SELECT * FROM `article` WHERE `id` > 511259 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000934s ]
  13. SELECT * FROM `article` WHERE `id` < 511259 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001828s ]
  14. SELECT * FROM `article` WHERE `id` < 511259 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.003060s ]
  15. SELECT * FROM `article` WHERE `id` < 511259 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.006306s ]
0.209829s