阅读视图

发现新文章,点击刷新页面。

业务系统深度集成:基于OnlyOffice中国版连接器实现合同生成、AI写作与报表自动化

业务系统深度集成:基于OnlyOffice中国版连接器实现合同生成、AI写作与报表自动化

一、为什么需要连接器

在大多数企业系统中,文档编辑器只是一个"嵌入式组件"——用户打开、编辑、保存,仅此而已。但真实业务场景中,我们往往需要从外部系统控制文档内容:

  • 合同系统需要将业务数据自动填入合同模板
  • AI写作系统需要将生成的内容插入到光标位置
  • 报表系统需要将统计数据写入Excel并自动生成图表
  • 审批系统需要在文档中自动插入审批意见和签章

这些需求的共同特点是:操作文档的主体不是用户,而是外部系统

OnlyOffice 提供了 连接器(Connector) 机制来满足这类需求。中国版完整实现了官方连接器的全部功能,兼容官方 JSAPI,并可与用户只读模式动态权限切换等增强功能配合使用,构建出更强大的业务集成方案。

中国版连接器增强能力

  • 兼容官方 Automation API,支持 Word/Excel/PPT 全文档类型操作
  • 可与用户只读模式配合:用户无法手动编辑,但连接器可操作文档
  • 支持动态权限切换:运行时通过连接器修改用户权限
  • 支持细粒度文档操作:段落、Run、样式、图表等均可操控

二、连接器基础

2.1 什么是连接器

连接器是 OnlyOffice 文档编辑器提供的 JavaScript API 接口,允许外部代码(宿主页面)对正在编辑的文档执行操作。它与插件(Plugin)拥有相同的底层接口,但使用方式更灵活:

  • 插件:需要打包部署到 documentserver 内部,通过编辑器内的插件菜单激活
  • 连接器:直接在宿主页面的 JavaScript 中调用,无需部署任何文件

对于业务系统集成来说,连接器是更合适的选择

2.2 创建连接器

在初始化编辑器后,通过 createConnector 方法获取连接器实例:

// 初始化编辑器
const docEditor = new DocsAPI.DocEditor("placeholder", config);

// 创建连接器
const connector = docEditor.createConnector();

2.3 核心方法

连接器提供两个核心方法:

callCommand —— 在文档上下文中执行代码:

connector.callCommand(function () {
    // 这里的代码运行在文档编辑器内部
    // 可以使用 Api 对象操作文档
    var oDocument = Api.GetDocument();
    // ...
});

executeMethod —— 调用编辑器提供的方法:

connector.executeMethod("InsertTextToCursor", ["Hello World"]);

两者的区别在于:callCommand 内的函数运行在编辑器沙箱中,可以调用完整的文档操作 API;executeMethod 是对常用操作的封装,调用更简洁。

注意callCommand 中的函数是序列化后传递到编辑器内部执行的,因此不能引用外部变量。需要传递数据时,可以通过函数返回值或事件机制。

三、场景一:合同模板自动填充

3.1 业务需求

某企业合同管理系统的需求:

  • 合同使用标准 Word 模板,包含固定条款和可变字段
  • 业务人员在系统中填写合同要素(甲乙方、金额、期限等)
  • 系统自动将数据填入模板对应位置
  • 用户在编辑器中只能查看结果,不能手动编辑

3.2 模板设计

在 Word 模板中,使用特定格式的占位符标记可变内容,例如:

甲方:{{partyA}}
乙方:{{partyB}}
合同金额:人民币 {{amount}} 元整
合同期限:{{startDate}} 至 {{endDate}}

3.3 技术实现

第一步:配置编辑器

使用用户只读模式,确保用户不能手动编辑,但连接器可以操作文档:

const config = {
  document: {
    fileType: "docx",
    key: contractKey,
    title: "采购合同-2026-0412",
    url: templateDownloadUrl,
    permissions: {
      edit: true,
      copy: true,
      copyOut: false,
      print: true
    }
  },
  editorConfig: {
    mode: "edit",
    customization: {
      readOnly: true,  // 用户只读模式
      waterMark: {
        value: `${currentUser.name}\\n合同预览`,
        fillstyle: "rgba(192, 192, 192, 0.2)",
        font: "14px SimHei",
        rotate: -30,
        opacity: 0.2
      }
    }
  }
};

第二步:获取业务数据并填充

当用户在业务表单中填写完合同要素后,通过连接器将数据写入文档:

// 业务数据
const contractData = {
  partyA: "北京某某科技有限公司",
  partyB: "上海某某信息技术有限公司",
  amount: "壹佰贰拾叁万肆仟伍佰陆拾柒",
  amountNum: "1,234,567.00",
  startDate: "2026年04月12日",
  endDate: "2027年04月11日",
  signDate: "2026年04月12日"
};

// 通过连接器填充数据
function fillContract(data) {
  const connector = docEditor.createConnector();

  // 将数据序列化后传入
  const jsonData = JSON.stringify(data);

  connector.callCommand(function () {
    // 在文档上下文中执行
    var oDocument = Api.GetDocument();
    var aElements = oDocument.GetAllContentControls();

    // 如果使用内容控件方式
    for (var i = 0; i < aElements.length; i++) {
      var tag = aElements[i].GetTag();
      // 根据 tag 匹配字段并替换
    }
  });

  // 也可以使用搜索替换方式
  connector.callCommand(function () {
    var oDocument = Api.GetDocument();

    // 使用 SearchAndReplace 方法
    var oSearchData = {
      searchString: "{{partyA}}",
      replaceString: "北京某某科技有限公司",
      matchCase: true
    };

    oDocument.SearchAndReplace(oSearchData);
  });
}

第三步:逐字段替换的完整实现

实际项目中,建议封装一个通用的模板填充方法:

function fillTemplate(connector, fieldMap) {
  const entries = Object.entries(fieldMap);

  // 由于 callCommand 内部不能引用外部变量
  // 需要逐个字段调用,或者将数据编码到函数体中
  entries.forEach(([placeholder, value]) => {
    // 动态构造函数字符串
    const script = `
      var oDocument = Api.GetDocument();
      oDocument.SearchAndReplace({
        searchString: "{{${placeholder}}}",
        replaceString: "${value.replace(/"/g, '\\"')}",
        matchCase: true
      });
    `;

    connector.callCommand(new Function(script));
  });
}

// 使用
fillTemplate(connector, {
  partyA: contractData.partyA,
  partyB: contractData.partyB,
  amount: contractData.amount,
  amountNum: contractData.amountNum,
  startDate: contractData.startDate,
  endDate: contractData.endDate
});

3.4 用户只读模式详解

用户只读模式是中国版特有的功能,可以实现"用户不可编辑,但连接器可操作文档"的效果。

与普通只读模式的区别

模式 用户能否编辑 连接器能否操作 适用场景
普通只读(mode: view) 纯预览场景
用户只读(readOnly: true) 合同生成、公文套打等

配置要点

{
  "editorConfig": {
    "customization": {
      "readOnly": true
    },
    "permissions": {
      "edit": true
    },
    "mode": "edit"
  }
}

三个字段必须同时配置:mode 设为 editpermissions.edit 设为 truecustomization.readOnly 设为 true

注意:用户只读模式为高级版功能,目前仅支持 Word/Excel/PPT 的 PC 模式

3.5 关键注意事项

  • 模板占位符应使用不易与正文冲突的格式(如 {{fieldName}}
  • 替换操作完成后,建议调用保存接口生成最终文档
  • 用户只读模式保证了模板结构和法律条款不会被手动修改
  • 结合防截图水印,可以在合同预览阶段保护内容安全

四、场景二:AI辅助写作集成

4.1 业务需求

某内容管理平台需要集成 AI 写作能力:

  • 用户在文档中编辑时,可以通过侧边栏调用 AI 功能
  • AI 生成的内容可以插入到当前光标位置
  • 支持 AI 润色:选中文本 → 调用 AI 改写 → 替换原文
  • 支持 AI 续写:在光标位置根据上下文续写内容

4.2 架构设计

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   业务前端    │────→│  AI 服务端    │────→│  大语言模型   │
│  (侧边栏)    │←────│  (API网关)    │←────│  (LLM)       │
└──────┬───────┘     └──────────────┘     └──────────────┘
       │
       │ connector.callCommand()
       ↓
┌──────────────┐
│  OnlyOffice  │
│  编辑器      │
└──────────────┘

4.3 核心实现

获取选中文本,发送给AI处理

// 获取当前选中的文本
function getSelectedText(connector) {
  return new Promise((resolve) => {
    connector.callCommand(
      function () {
        var oDocument = Api.GetDocument();
        var selectedText = oDocument.GetSelectedText();
        return selectedText;
      },
      false, // isNoCalc
      function (result) {
        resolve(result);
      }
    );
  });
}

// AI润色流程
async function aiPolish() {
  const selectedText = await getSelectedText(connector);

  if (!selectedText) {
    alert("请先选中需要润色的文本");
    return;
  }

  // 调用后端AI接口
  const response = await fetch("/api/ai/polish", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: selectedText })
  });

  const { result } = await response.json();

  // 将AI结果替换选中内容
  connector.callCommand(function () {
    var oDocument = Api.GetDocument();
    // 在当前选区位置插入新文本
    var oParagraph = Api.CreateParagraph();
    oParagraph.AddText(result);
    oDocument.InsertContent([oParagraph], true); // true 表示替换选区
  });
}

在光标位置插入AI生成的内容

async function aiGenerate(prompt) {
  const response = await fetch("/api/ai/generate", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt })
  });

  const { result } = await response.json();

  // 将生成的内容插入光标位置
  connector.callCommand(function () {
    var oParagraph = Api.CreateParagraph();
    var oRun = Api.CreateRun();

    // 设置字体样式与文档保持一致
    oRun.AddText(result);
    oRun.SetFontFamily("SimSun");
    oRun.SetFontSize(24); // 单位是半磅,24 = 12pt

    oParagraph.AddElement(oRun);

    var oDocument = Api.GetDocument();
    oDocument.InsertContent([oParagraph]);
  });
}

4.4 流式输出的处理

如果AI接口支持流式输出(SSE),可以实现逐字显示效果。但需要注意,频繁调用 callCommand 会有性能开销。建议的处理方式:

  • 在侧边栏先完成AI内容的流式展示
  • 用户确认后一次性插入到文档中
  • 或者每积累一定长度(如一个段落)后批量插入
// 推荐:在侧边栏展示完整结果后,一次性插入
function insertAiResult(text) {
  const paragraphs = text.split("\n").filter(p => p.trim());

  connector.callCommand(function () {
    var aContent = [];

    for (var i = 0; i < paragraphs.length; i++) {
      var oParagraph = Api.CreateParagraph();
      oParagraph.AddText(paragraphs[i]);
      aContent.push(oParagraph);
    }

    var oDocument = Api.GetDocument();
    oDocument.InsertContent(aContent);
  });
}

五、场景三:Excel报表自动生成

5.1 业务需求

某数据分析平台需要将统计数据自动填入Excel模板并生成图表:

  • 每月自动生成销售报表
  • 将数据库中的统计数据写入对应的单元格
  • 根据数据自动更新图表
  • 生成后的报表可以供用户在线查看和下载

5.2 写入表格数据

// 销售数据
const salesData = [
  { month: "1月", revenue: 125000, cost: 89000, profit: 36000 },
  { month: "2月", revenue: 138000, cost: 92000, profit: 46000 },
  { month: "3月", revenue: 156000, cost: 98000, profit: 58000 },
  // ...
];

function fillExcelReport(connector, data) {
  // 将数据转为JSON字符串,嵌入到函数中
  const jsonStr = JSON.stringify(data);

  connector.callCommand(function () {
    var data = JSON.parse(jsonStr);
    var oWorksheet = Api.GetActiveSheet();

    // 写入表头
    oWorksheet.GetRange("A1").SetValue("月份");
    oWorksheet.GetRange("B1").SetValue("收入(元)");
    oWorksheet.GetRange("C1").SetValue("成本(元)");
    oWorksheet.GetRange("D1").SetValue("利润(元)");

    // 设置表头样式
    var headerRange = oWorksheet.GetRange("A1:D1");
    headerRange.SetBold(true);
    headerRange.SetFillColor(Api.CreateColorFromRGB(68, 114, 196));
    headerRange.SetFontColor(Api.CreateColorFromRGB(255, 255, 255));

    // 写入数据
    for (var i = 0; i < data.length; i++) {
      var row = i + 2;
      oWorksheet.GetRange("A" + row).SetValue(data[i].month);
      oWorksheet.GetRange("B" + row).SetValue(data[i].revenue);
      oWorksheet.GetRange("C" + row).SetValue(data[i].cost);
      oWorksheet.GetRange("D" + row).SetValue(data[i].profit);
    }

    // 设置数字格式
    var dataRows = data.length;
    oWorksheet.GetRange("B2:D" + (dataRows + 1)).SetNumberFormat("#,##0.00");

    // 自动调整列宽
    oWorksheet.GetRange("A1:D1").SetColumnWidth(15);
  });
}

5.3 自动创建图表

function createChart(connector, dataRowCount) {
  connector.callCommand(function () {
    var oWorksheet = Api.GetActiveSheet();

    // 创建柱状图
    var oChart = oWorksheet.AddChart(
      "'" + oWorksheet.GetName() + "'!$A$1:$D$" + (dataRowCount + 1),
      true,  // 按行
      "bar", // 图表类型
      2,     // 样式
      200 * 36000,   // 宽度(EMU)
      150 * 36000    // 高度(EMU)
    );

    oChart.SetTitle("月度销售报表", 12);
    oChart.SetLegendPos("bottom");

    // 将图表放置在数据下方
    oChart.SetPosition(oWorksheet, dataRowCount + 3, 0, 0, 0);
  });
}

5.4 完整工作流

async function generateMonthlyReport() {
  // 1. 从后端获取数据
  const response = await fetch("/api/reports/monthly-sales");
  const salesData = await response.json();

  // 2. 创建连接器
  const connector = docEditor.createConnector();

  // 3. 填充数据
  fillExcelReport(connector, salesData);

  // 4. 生成图表
  createChart(connector, salesData.length);

  // 5. 通知用户
  showNotification("报表生成完成");
}

六、连接器开发的最佳实践

6.1 数据传递

由于 callCommand 中的函数在编辑器沙箱中执行,不能直接引用外部变量。推荐的数据传递方式:

// 方式一:将数据序列化后拼接到函数体中
function setValueByConnector(connector, cellRef, value) {
  const safeValue = JSON.stringify(value);
  connector.callCommand(
    new Function(`
      var oSheet = Api.GetActiveSheet();
      oSheet.GetRange("${cellRef}").SetValue(${safeValue});
    `)
  );
}

// 方式二:使用 callCommand 的回调获取返回值
connector.callCommand(
  function () {
    return Api.GetDocument().GetStatistics();
  },
  false,
  function (stats) {
    console.log("文档统计:", stats);
  }
);

6.2 错误处理

function safeCallCommand(connector, fn, callback) {
  try {
    connector.callCommand(fn, false, function (result) {
      if (callback) callback(null, result);
    });
  } catch (error) {
    console.error("连接器调用失败:", error);
    if (callback) callback(error, null);
  }
}

6.3 性能优化

  • 批量操作:将多个操作合并到一次 callCommand 调用中,减少通信开销
  • 避免频繁调用:不要在循环中逐次调用 callCommand,应在单次调用中完成所有操作
  • 异步处理callCommand 是异步的,注意操作顺序的控制
// 不推荐:逐行调用
for (let i = 0; i < 1000; i++) {
  connector.callCommand(function () {
    // 写入一行数据
  });
}

// 推荐:一次性写入所有数据
connector.callCommand(function () {
  var oSheet = Api.GetActiveSheet();
  for (var i = 0; i < 1000; i++) {
    oSheet.GetRange("A" + (i + 1)).SetValue("data" + i);
  }
});

6.4 动态权限切换(中国版特有)

中国版自 9.3.0 版本开始支持通过连接器动态修改用户权限,无需重新打开文档即可实时生效。

使用场景

  • 审批流程中,审批人点击"开始审批"后自动切换为只读模式
  • 文档状态变化时,动态调整用户的编辑/复制/打印权限
  • 根据业务规则,在特定条件下限制用户操作

实现示例

// 创建连接器
const connector = docEditor.createConnector();

// 审批人点击"开始审批"按钮时,切换为只读+可评论
function onStartReview() {
  connector.callCommand(function () {
    Api.changePermissions({
      edit: false,
      comment: true,
      copy: true,
      copyOut: false,
      print: false
    });
  });
}

// 审批通过后,进入签署阶段,完全禁止操作
function onApproved() {
  connector.callCommand(function () {
    Api.changePermissions({
      edit: false,
      comment: false,
      copy: false,
      copyOut: false,
      print: false
    });
  });
}

// 审批驳回,退回给起草人编辑
function onRejected() {
  connector.callCommand(function () {
    Api.changePermissions({
      edit: true,
      comment: true,
      copy: true,
      copyOut: true,
      print: true
    });
  });
}

支持的权限字段

字段 说明 类型
comment 是否允许评论 Boolean
copy 是否允许复制 Boolean
copyOut 是否允许复制到外部(中国版特有) Boolean
edit 是否允许编辑 Boolean
print 是否允许打印 Boolean

注意:动态权限切换为高级版功能,目前仅支持 Word/Excel/PPT 的 PC 模式

6.5 与中国版增强功能的配合

连接器可以与中国版的多个增强功能组合使用,构建更强大的业务场景:

组合方式 典型场景 关键配置
连接器 + 用户只读模式 合同制作、公文套打 customization.readOnly: true
连接器 + 动态权限切换 审批流程中的权限流转 Api.changePermissions()
连接器 + 防截图水印 安全环境下的自动文档生成 customization.waterMark
连接器 + 内部剪切板 敏感数据填充后防止用户复制到外部 permissions.copyOut: false
连接器 + 迷你工具栏 简化用户编辑体验 customization.miniToolbar: true

七、与WPS JSSDK的对比

对于有国内办公套件集成经验的开发者,可能更熟悉 WPS 的 JSSDK。以下是两者的关键差异:

对比维度 OnlyOffice 连接器 WPS JSSDK
API丰富度 与插件接口相同,覆盖面广 提供标准化接口,覆盖常用场景
文档操作深度 可操作到段落、Run、样式等细粒度 以高层封装为主
私有化部署 完全支持 需要商业授权
学习成本 需了解 OOXML 模型 接口设计更面向业务
扩展性 插件 + 连接器双通道 SDK标准接口

OnlyOffice 连接器的优势在于更深的文档操作能力和完全的私有化支持,适合需要深度定制的企业级场景。

八、总结

OnlyOffice 中国版的连接器为业务系统与文档编辑器之间架起了一座桥梁。通过 JSAPI,外部系统可以像操作数据库一样操作文档内容——读取、写入、格式化、生成图表,一切都可以通过代码完成。

核心价值:

  • 合同生成:模板 + 数据 = 标准合同,告别手工填写
  • AI写作:大模型生成的内容无缝融入文档编辑流程
  • 报表自动化:数据驱动的文档生成,取代重复的手工操作
  • 流程驱动:文档操作与业务流程深度绑定,实现真正的自动化

连接器让 OnlyOffice 不再只是一个编辑器,而是业务系统中可编程的文档引擎。

相关资源

BaseMetas Fileview 在线文件预览服务部署对接指南

本文面向需要将文件预览能力集成到自有系统的开发人员,覆盖从 Docker 部署、反向代理配置、API 对接到生产环境最佳实践的完整流程。读完本文,你将能够在自己的业务系统中完成 Fileview 的部署与集成。


一、了解 Fileview

1.1 它是什么

BaseMetas Fileview 是一款通用型在线文档预览引擎,支持超过 200 种文件格式的在线预览,覆盖 Office 文档、PDF、OFD、CAD 图纸、3D 模型、代码文件、流程图、思维导图、压缩包、音视频、图片等全格式类型。

它的设计目标是:作为独立的预览服务,通过标准 HTTP 接口集成到任意业务系统中。你的系统只需要把"文件地址"告诉 Fileview,它负责下载、转换、渲染,最终在浏览器中呈现预览结果。

1.2 架构概览

Fileview 内部由两个服务组成,Docker 镜像已将它们打包在一起,对外只暴露一个 HTTP 端口:

┌─────────────────────────────────────────┐
│           Docker 容器 (端口 80)           │
│                                         │
│  ┌─────────────┐    ┌─────────────────┐ │
│  │  预览服务     │───▶│   转换服务       │ │
│  │ (HTTP API)  │    │ (格式转换引擎)    │ │
│  └──────┬──────┘    └────────┬────────┘ │
│         │                    │          │
│     ┌───┴────────────────────┴───┐      │
│     │   RocketMQ + Redis + 存储   │      │
│     └────────────────────────────┘      │
└─────────────────────────────────────────┘
  • 预览服务:对外提供 /preview/api/** 等 HTTP 接口,负责请求编排、文件下载、权限校验、结果缓存、长轮询响应
  • 转换服务:内部服务,通过订阅 MQ 事件被动触发,负责文件格式转换(Office → PDF/HTML、CAD → SVG 等)
  • Redis:统一的状态与缓存中心,存储下载/转换任务状态、缓存预览结果
  • RocketMQ:事件总线,承载预览相关事件,负责下载任务和转换任务的异步投递

作为集成方,你不需要关心内部实现,只需要:部署 → 配置代理 → 调用 API

1.3 集成交互流程

你的业务系统                        Fileview 预览服务
    │                                    │
    │  1. 用户点击文件                      │
    │──────────────────────────────────▶ │
    │  2. 构造预览URL                      │
    │     (包含文件下载地址)                  │
    │  3. 浏览器打开预览URL                  │
    │  ─────────────────────────────────▶│
    │                                    │ 4. 下载文件
    │                                    │ 5. 格式转换
    │                                    │ 6. 返回渲染结果
    │  ◀─────────────────────────────────│
    │  7. 用户看到预览效果                    │

二、环境准备

2.1 硬件要求

规格 最低配置 推荐配置 高并发场景
CPU 2 核 4 核 8 核+
内存 2GB 4GB 8GB+
磁盘 10GB 50GB 100GB+(取决于文件量)

磁盘空间主要用于存储下载的源文件和转换后的结果文件。Fileview 内置临时文件清理机制,会定期清理过期文件。

2.2 软件要求

  • Docker:20.10+(必需)
  • Nginx:生产环境推荐使用反向代理(非必需,但强烈建议)

2.3 支持的 CPU 架构

  • AMD64 (x86_64)
  • ARM64 (aarch64)

三、部署

3.1 拉取镜像

# Docker Hub(首选)
docker pull basemetas/fileview:latest

# 国内镜像加速
docker pull docker.1ms.run/basemetas/fileview:latest
# 或
docker pull dockerproxy.net/basemetas/fileview:latest

3.2 启动服务

最简启动(开发/测试):

docker run -itd \
    --name fileview \
    -p 9000:80 \
    --restart=always \
    basemetas/fileview:latest

容器内部监听 80 端口,映射到宿主机 9000 端口。

挂载数据目录(生产推荐):

docker run -itd \
    --name fileview \
    -p 9000:80 \
    -v /data/fileview/storage:/opt/fileview/data \
    -v /data/fileview/logs:/opt/fileview/logs \
    --restart=always \
    basemetas/fileview:latest

这样可以将文件存储和日志持久化到宿主机,避免容器重建后数据丢失。

3.3 验证部署

浏览器访问 http://<你的服务器IP>:9000/,如果看到 Fileview 欢迎页,说明部署成功。

也可以通过命令行验证:

curl -I http://localhost:9000/preview/welcome
# 应该返回 HTTP 200

四、反向代理配置(生产环境必做)

生产环境中不建议直接暴露 Docker 端口,应通过 Nginx 反向代理统一管理。以下提供两种典型部署模式。

4.1 独立域名部署

Fileview 使用一个独立的域名或子域名:

server {
    listen 443 ssl;
    server_name preview.example.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:9000;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
    }
}

此时预览服务的 Base URL 为:https://preview.example.com

4.2 子路径部署(与业务系统同域)

Fileview 部署在业务系统的一个子路径下(如 /fileview/),这种方式可以避免跨域问题:

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # 你的业务系统
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Fileview 预览服务
    location /fileview/ {
        proxy_pass http://127.0.0.1:9000/;

        # 关键:告知 Fileview 自己处于子路径下
        proxy_set_header X-Forwarded-Prefix /fileview;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
    }
}

X-Forwarded-Prefix 是子路径部署的关键请求头。Fileview 会根据它自动调整生成的预览 URL 前缀,确保返回给浏览器的地址是正确可访问的。

此时预览服务的 Base URL 为:https://app.example.com/fileview

4.3 代理头说明

请求头 作用 是否必需
X-Forwarded-Proto 告知原始协议(http/https) 是(HTTPS 场景)
X-Forwarded-Host 告知原始访问域名 推荐
X-Forwarded-Prefix 告知子路径前缀 子路径部署时必需
X-Forwarded-Port 告知原始访问端口 推荐
REMOTE-HOST 告知原始客户端 IP 推荐
Host 标准 Host 头

Fileview 会按优先级解析这些请求头,动态生成预览 URL 的 Base URL。详细机制参见预览地址生成机制


五、API 集成

Fileview 的集成核心只有一件事:构造正确的预览 URL

5.1 预览 URL 格式

{baseUrl}/preview/view?url={fileUrl}&fileName={fileName}

其中:

  • {baseUrl}:Fileview 服务的访问地址
  • {fileUrl}:文件的网络下载地址(需 URL 编码)
  • {fileName}:文件名(需 URL 编码,用于文件类型判断)

5.2 预览网络文件(最常用)

方式一:query 参数(简单直接)

// 你的系统中,生成文件的下载链接
const fileDownloadUrl = "https://app.example.com/api/files/download?id=12345";
const fileName = "年度报告.docx";

// 构造预览 URL
const previewUrl = `https://app.example.com/fileview/preview/view?url=${encodeURIComponent(fileDownloadUrl)}&fileName=${encodeURIComponent(fileName)}`;

// 打开预览
window.open(previewUrl, "_blank");

参数说明:

参数 类型 必填 说明
url string 文件的网络下载地址,支持 http/https/ftp
fileName string 条件必填 真实文件名(含后缀),用于类型判断。如果 url 中已包含正确后缀(如 .docx),可不传
displayName string 在标题栏显示的文件名,不影响类型判断
watermark string 文字水印内容,支持 \n 换行,建议不超过两行
mode string 显示模式:normal(默认,带菜单栏)或 embed(嵌入模式,无菜单栏)

方式二:data 参数(Base64 编码,隐藏参数)

对于不希望在 URL 中暴露文件地址的场景,可以使用 data 参数将所有参数 Base64 编码后传递:

// 安装 js-base64:npm install js-base64
import { Base64 } from "js-base64";

const opts = {
    url: "https://app.example.com/api/files/download?id=12345",
    fileName: "年度报告.docx",
    displayName: "2025年度报告"
};

// Base64 编码
const base64Data = encodeURIComponent(Base64.encode(JSON.stringify(opts)));

// 构造预览 URL
const previewUrl = `https://app.example.com/fileview/preview/view?data=${base64Data}`;
window.open(previewUrl, "_blank");

CDN 引入方式:

<script src="https://cdn.jsdelivr.net/npm/js-base64@3.7.8/base64.min.js"></script>
<script>
    const opts = {
        url: "https://app.example.com/api/files/download?id=12345",
        fileName: "年度报告.docx"
    };
    const base64Data = encodeURIComponent(Base64.encode(JSON.stringify(opts)));
    const previewUrl = `https://app.example.com/fileview/preview/view?data=${base64Data}`;
    window.open(previewUrl, "_blank");
</script>

5.3 预览本地文件

如果文件已存在于 Fileview 容器可访问的磁盘路径上(比如通过 Docker 卷挂载共享目录),可以使用 path 参数替代 url

const filePath = "/opt/fileview/data/shared/report.docx";
const fileName = "report.docx";

const previewUrl = `https://app.example.com/fileview/preview/view?path=${encodeURIComponent(filePath)}&fileName=${encodeURIComponent(fileName)}`;
window.open(previewUrl, "_blank");

注意:path 是 Fileview 容器内部的文件路径,不是宿主机路径。如果使用 Docker 挂载 -v /host/files:/opt/fileview/data/shared,则对应容器内路径为 /opt/fileview/data/shared/xxx

5.4 两种传参方式对比

特性 query 参数方式 data 参数方式
实现复杂度 中(需 Base64 库)
URL 可读性 文件地址在 URL 中可见 参数被 Base64 编码,不直接可见
参数安全性 一般 有一定隐藏作用
适用场景 内部系统、开发调试 对外系统、安全要求较高场景
后端集成 简单字符串拼接 需 JSON 序列化 + Base64 编码

六、前端集成模式

6.1 新窗口打开(最简单)

function previewFile(fileUrl, fileName) {
    const url = encodeURIComponent(fileUrl);
    const name = encodeURIComponent(fileName);
    const previewUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${url}&fileName=${name}`;
    window.open(previewUrl, "_blank");
}

适合:快速集成、对 UI 无特殊要求的场景。

6.2 iframe 嵌入(推荐)

<iframe
    id="file-preview"
    src=""
    width="100%"
    height="600px"
    frameborder="0"
    allowfullscreen
></iframe>

<script>
function previewInIframe(fileUrl, fileName) {
    const url = encodeURIComponent(fileUrl);
    const name = encodeURIComponent(fileName);
    // 使用 embed 模式去除 Fileview 自带的菜单栏
    const previewUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${url}&fileName=${name}&mode=embed`;
    document.getElementById('file-preview').src = previewUrl;
}
</script>

mode=embed 参数会隐藏 Fileview 的顶部菜单栏,使预览内容更好地嵌入你的页面。

适合:文件详情页、审批流程页面、文档管理系统等需要"内嵌预览"的场景。

6.3 弹窗/抽屉预览

// 以弹窗模式预览(示例使用原生 JS)
function previewInModal(fileUrl, fileName) {
    const url = encodeURIComponent(fileUrl);
    const name = encodeURIComponent(fileName);
    const previewUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${url}&fileName=${name}&mode=embed`;

    // 创建遮罩层
    const overlay = document.createElement('div');
    overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;';
    
    // 创建 iframe 容器
    const container = document.createElement('div');
    container.style.cssText = 'width:90%;height:90%;background:#fff;border-radius:8px;overflow:hidden;position:relative;';
    
    // 关闭按钮
    const closeBtn = document.createElement('button');
    closeBtn.innerText = '关闭';
    closeBtn.style.cssText = 'position:absolute;top:8px;right:12px;z-index:10;padding:4px 12px;cursor:pointer;';
    closeBtn.onclick = () => document.body.removeChild(overlay);
    
    // iframe
    const iframe = document.createElement('iframe');
    iframe.src = previewUrl;
    iframe.style.cssText = 'width:100%;height:100%;border:none;';
    
    container.appendChild(closeBtn);
    container.appendChild(iframe);
    overlay.appendChild(container);
    overlay.onclick = (e) => { if (e.target === overlay) document.body.removeChild(overlay); };
    document.body.appendChild(overlay);
}

适合:列表页"快速预览"、不离开当前页面查看文件的场景。


七、后端集成示例

7.1 Java (Spring Boot)

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Service
public class FilePreviewService {

    @Value("${fileview.base-url}")
    private String fileviewBaseUrl;  // 如 https://app.example.com/fileview

    /**
     * 生成文件预览 URL
     * @param fileDownloadUrl 文件下载地址
     * @param fileName 文件名
     * @return 预览 URL
     */
    public String buildPreviewUrl(String fileDownloadUrl, String fileName) {
        String encodedUrl = URLEncoder.encode(fileDownloadUrl, StandardCharsets.UTF_8);
        String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
        return String.format("%s/preview/view?url=%s&fileName=%s",
                fileviewBaseUrl, encodedUrl, encodedName);
    }

    /**
     * 生成带水印的预览 URL
     */
    public String buildPreviewUrlWithWatermark(String fileDownloadUrl, String fileName, String watermark) {
        String encodedUrl = URLEncoder.encode(fileDownloadUrl, StandardCharsets.UTF_8);
        String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
        String encodedWatermark = URLEncoder.encode(watermark, StandardCharsets.UTF_8);
        return String.format("%s/preview/view?url=%s&fileName=%s&watermark=%s",
                fileviewBaseUrl, encodedUrl, encodedName, encodedWatermark);
    }

    /**
     * 生成嵌入模式的预览 URL(无菜单栏)
     */
    public String buildEmbedPreviewUrl(String fileDownloadUrl, String fileName) {
        return buildPreviewUrl(fileDownloadUrl, fileName) + "&mode=embed";
    }
}

在 Controller 中使用:

@RestController
@RequestMapping("/api/files")
public class FileController {

    @Autowired
    private FilePreviewService previewService;

    @GetMapping("/{fileId}/preview-url")
    public Map<String, String> getPreviewUrl(@PathVariable String fileId) {
        // 从你的业务中获取文件信息
        FileInfo file = fileService.getById(fileId);
        String downloadUrl = fileService.generateDownloadUrl(fileId);

        String previewUrl = previewService.buildPreviewUrl(downloadUrl, file.getName());
        return Map.of("previewUrl", previewUrl);
    }
}

7.2 Python (Flask/Django)

from urllib.parse import quote

FILEVIEW_BASE_URL = "https://app.example.com/fileview"

def build_preview_url(file_download_url: str, file_name: str, 
                      watermark: str = None, embed: bool = False) -> str:
    """构造 Fileview 预览 URL"""
    encoded_url = quote(file_download_url, safe='')
    encoded_name = quote(file_name, safe='')
    
    preview_url = f"{FILEVIEW_BASE_URL}/preview/view?url={encoded_url}&fileName={encoded_name}"
    
    if watermark:
        preview_url += f"&watermark={quote(watermark, safe='')}"
    
    if embed:
        preview_url += "&mode=embed"
    
    return preview_url

7.3 Go

package preview

import (
    "fmt"
    "net/url"
)

const FileviewBaseURL = "https://app.example.com/fileview"

func BuildPreviewURL(fileDownloadURL, fileName string) string {
    return fmt.Sprintf("%s/preview/view?url=%s&fileName=%s",
        FileviewBaseURL,
        url.QueryEscape(fileDownloadURL),
        url.QueryEscape(fileName),
    )
}

func BuildEmbedPreviewURL(fileDownloadURL, fileName string) string {
    return BuildPreviewURL(fileDownloadURL, fileName) + "&mode=embed"
}

7.4 PHP

<?php
define('FILEVIEW_BASE_URL', 'https://app.example.com/fileview');

function buildPreviewUrl(string $fileDownloadUrl, string $fileName, 
                         ?string $watermark = null, bool $embed = false): string {
    $params = [
        'url' => $fileDownloadUrl,
        'fileName' => $fileName,
    ];
    
    if ($watermark !== null) {
        $params['watermark'] = $watermark;
    }
    
    if ($embed) {
        $params['mode'] = 'embed';
    }
    
    return FILEVIEW_BASE_URL . '/preview/view?' . http_build_query($params);
}

// 使用
$previewUrl = buildPreviewUrl(
    'https://app.example.com/download/report.docx',
    'report.docx',
    "内部文件\n仅供预览",
    true
);

八、关键功能:文字水印

Fileview 支持在预览时添加文字水印,适用于所有支持水印的文件格式,可用于防止截屏泄露。

// 水印内容支持 \n 换行,建议不超过两行
const watermark = encodeURIComponent("内部文件 仅供预览\n2026-04-12");

const previewUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${fileUrl}&fileName=${fileName}&watermark=${watermark}`;

水印是在预览时动态叠加的,不会修改原始文件。


九、关键功能:嵌入模式

嵌入模式(mode=embed)会隐藏 Fileview 自带的顶部菜单栏(包含文件名、工具按钮等),使预览内容区域全屏展示。

// 普通模式(默认)- 带菜单栏
const normalUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${fileUrl}&fileName=${fileName}`;

// 嵌入模式 - 无菜单栏
const embedUrl = `${FILEVIEW_BASE_URL}/preview/view?url=${fileUrl}&fileName=${fileName}&mode=embed`;

适用于将预览嵌入到你自己的 UI 框架中,由你的系统提供统一的顶栏和操作按钮。


十、安全配置

10.1 可信站点白名单(生产环境必须配置)

Fileview 在预览网络文件时,会先从指定 URL 下载文件。为防止 SSRF 攻击和非授权访问,应配置可信站点白名单,确保 Fileview 只从你的系统域名下载文件。

白名单配置写在 Fileview 的 application.yml 中。对于 Docker 部署,通过环境变量传入:

docker run -itd \
    --name fileview \
    -p 9000:80 \
    -e FILEVIEW_NETWORK_SECURITY_TRUSTED_SITES="app.example.com, *.internal.example.com" \
    --restart=always \
    basemetas/fileview:latest

或在配置文件中:

fileview:
  network:
    security:
      # 仅允许从这些域名下载文件
      trusted-sites: app.example.com, *.internal.example.com
      # 明确禁止的域名(优先级高于白名单)
      untrusted-sites: ""

规则说明:

  • 配置 example.com 会自动匹配其所有子域名
  • 支持通配符:*.example.com
  • 黑名单优先级高于白名单
  • 大小写不敏感
  • 多个规则用逗号分隔

推荐策略:

环境 配置建议
开发/测试 可不配置白名单(允许所有域名)
生产环境 必须配置白名单,仅允许你的系统域名
内网隔离 配置内网域名/IP

10.2 文件下载地址的安全性

你的系统生成的文件下载 URL 应具备基本的安全防护:

  • 临时链接:设置过期时间(如 30 分钟)
  • 签名校验:URL 包含签名参数,防止伪造
  • 权限校验:确保只有授权用户才能生成下载链接
// 推荐:生成带签名和过期时间的临时下载链接
const downloadUrl = generateSignedUrl(fileId, {
    expiresIn: 1800,  // 30 分钟
    signature: computeHmac(fileId + timestamp, secretKey)
});

10.3 加密文件处理

Fileview 支持预览带密码的 Office 文档(docx/xlsx/pptx)和加密压缩包(zip/rar/7z)。

处理流程:

  1. Fileview 检测到文件加密,返回 PASSWORD_REQUIRED 状态
  2. 前端弹出密码输入框
  3. 用户输入密码后,Fileview 验证并解密预览
  4. 密码加密存储在 Redis 中,30 分钟内同一客户端再次访问无需重复输入

十一、文件 URL 的对接要求

这是集成中最关键的一点:你的系统需要提供一个可供 Fileview 服务端下载文件的 URL

11.1 基本要求

  1. 可达性:Fileview 容器能够通过网络访问该 URL
  2. 直接下载:URL 访问后直接返回文件二进制流(而非 HTML 页面)
  3. Content-Type:建议返回正确的 MIME 类型(非必需,Fileview 主要靠 fileName 参数判断类型)

11.2 典型场景

场景 A:文件服务有公开下载接口

https://app.example.com/api/files/download?id=12345&token=xxx

直接将此 URL 作为 url 参数传给 Fileview 即可。

场景 B:文件存储在 OSS/S3

https://your-bucket.oss-cn-hangzhou.aliyuncs.com/files/report.docx?OSSAccessKeyId=xxx&Signature=xxx&Expires=xxx

使用预签名 URL,注意设置合理的过期时间。

场景 C:文件接口需要 Cookie/Token 认证

Fileview 的文件下载是服务端行为(不是浏览器行为),所以不会携带用户的 Cookie。

解决方案:

  • 方案一:生成不需要认证的临时下载链接(推荐)
  • 方案二:将 Token 作为 URL 参数传递(如 ?token=xxx
  • 方案三:使用 Fileview 的本地文件预览,让你的系统先把文件写入共享目录

场景 D:Fileview 和业务系统在同一内网

可以使用内网地址:

http://192.168.1.100:8080/api/files/download?id=12345

但需注意 Docker 网络。如果 Fileview 在 Docker 中,需确保容器能访问宿主机或其他容器的网络:

# 方案一:使用 host.docker.internal 访问宿主机(Docker Desktop 支持)
http://host.docker.internal:8080/api/files/download?id=12345

# 方案二:使用 Docker 网络
docker network create my-net
docker run --network my-net --name fileview ...
docker run --network my-net --name my-app ...
# 此时可用容器名访问:http://my-app:8080/api/files/download?id=12345

十二、Docker Compose 部署(推荐)

对于与业务系统联合部署的场景,推荐使用 Docker Compose:

version: '3.8'

services:
  fileview:
    image: basemetas/fileview:latest
    container_name: fileview
    ports:
      - "9000:80"
    volumes:
      - fileview-data:/opt/fileview/data
      - fileview-logs:/opt/fileview/logs
    restart: always
    networks:
      - app-network

  # 你的业务系统(示例)
  my-app:
    image: your-app:latest
    container_name: my-app
    ports:
      - "8080:8080"
    restart: always
    networks:
      - app-network

  # Nginx 反向代理
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - fileview
      - my-app
    restart: always
    networks:
      - app-network

volumes:
  fileview-data:
  fileview-logs:

networks:
  app-network:
    driver: bridge

在此配置下,Nginx 的代理配置可以使用容器名(fileviewmy-app)作为上游地址:

# nginx/conf.d/default.conf
server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    # 业务系统
    location / {
        proxy_pass http://my-app:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Fileview
    location /fileview/ {
        proxy_pass http://fileview:80/;
        proxy_set_header X-Forwarded-Prefix /fileview;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
    }
}

十三、集成自检清单

部署和集成完成后,按以下清单逐项验证:

部署验证

  • 访问 Fileview 欢迎页正常显示
  • 如使用反向代理,通过代理地址访问欢迎页正常
  • 如使用子路径部署,确认 {baseUrl}/preview/welcome 可访问

预览功能验证

  • 使用 url 参数预览一个公开可下载的 PDF 文件
  • 使用 url 参数预览一个 DOCX 文件(需经过格式转换)
  • 使用 url 参数预览一个图片文件
  • 如有本地文件场景,使用 path 参数预览本地文件
  • 验证 mode=embed 嵌入模式生效(无菜单栏)
  • 验证 watermark 水印参数生效

安全验证

  • 配置可信站点白名单后,尝试预览非白名单域名的文件(应被拒绝)
  • 确认文件下载 URL 有适当的鉴权和过期机制

网络验证

  • Fileview 容器能够访问你的文件下载地址
  • 如在 Docker 内,确认容器间网络互通
  • HTTPS 场景下,确认预览 URL 生成为 HTTPS

十四、常见问题排查

问题 1:预览页面白屏或长时间加载

排查步骤:

  1. 查看浏览器 Network 面板,确认预览 URL 请求是否成功(HTTP 200)
  2. 检查 Fileview 容器日志:docker logs fileview
  3. 确认 Fileview 能否下载文件:进入容器测试 curl <文件下载URL>
  4. 对于 Office 文件,首次预览需要格式转换,可能需要几秒到十几秒

问题 2:预览 URL 中的地址不正确

原因: 反向代理未正确传递 X-Forwarded-* 请求头。

解决: 检查 Nginx 配置中是否包含完整的代理头设置,特别是 X-Forwarded-ProtoX-Forwarded-HostX-Forwarded-Prefix

问题 3:文件下载失败

排查步骤:

  1. 确认文件下载 URL 在 Fileview 容器内可访问
  2. 检查白名单配置是否包含文件所在域名
  3. 检查文件下载 URL 是否已过期
  4. 如使用 Docker,检查容器网络配置(DNS 解析、网络连通性)

问题 4:中文文件名乱码

解决: 确保所有参数都经过 encodeURIComponent / URLEncoder.encode 编码。

问题 5:OFD 文件中文显示为方框

原因: 缺少中文字体(思源字体)。

解决: 参考常见问题文档中的字体安装说明。


十五、生产环境最佳实践

15.1 必做事项

  1. 配置反向代理:不直接暴露 Docker 端口
  2. 启用 HTTPS:在 Nginx 层终止 SSL
  3. 配置可信站点白名单:限制文件下载来源
  4. 挂载数据卷:持久化文件存储和日志
  5. 设置容器自动重启--restart=always

15.2 建议事项

  1. 使用子路径部署:避免跨域问题
  2. 生成带签名和过期时间的文件下载 URL
  3. 监控容器资源:关注 CPU、内存、磁盘使用率
  4. 定期查看日志:及时发现转换失败等异常

15.3 性能优化

对于高并发场景,可参考性能调优指南进行以下优化:

  • JVM 堆内存调整(通过 Docker 环境变量)
  • 转换线程池并发度控制
  • Redis 连接池调优
  • 长轮询策略调整

十六、支持的文件格式速查

类别 格式
Office doc, docx, wps, rtf, txt, md 等文档类;xls, xlsx, csv, ods 等表格类;ppt, pptx, odp 等演示类
版式文档 pdf, ofd
图片 jpg, png, gif, bmp, svg, webp, psd, tif, tga, emf, wmf
CAD dwg, dxf
3D 模型 gltf, glb, obj, stl, fbx, ply, dae, wrl, 3ds, 3mf, 3dm
流程图/思维导图 vsd, vsdm, vsdx, vssm, vssx, vstm, vstx, bpmn, drawio, xmind
压缩包 zip, jar, rar, 7z, tar, tar.gz
代码 java, kotlin, scala, python, go, rust, c, cpp, js, ts, vue, php, ruby, shell 等 100+ 语言
音视频 mp4, mp3, webm, wav, aac, ogg, flac, avi, mkv, mov, flv
电子书 epub
文本 html, xml, json, yaml, toml, ini, conf

完整列表参见 支持格式


相关资料

❌