ONLYOFFICE Odoo 集成架构深度解析与实战手册
Part 1: 架构设计白皮书 (Architecture Whitepaper)
1. 架构拓扑:通信三角 (The Communication Triangle)
理解本模块的核心在于理解其三方通信模型。这不是简单的 Client-Server 架构,而是 Client-Server-Server 架构。

关键架构设计:双 URL 策略
代码 res_config_settings.py 中定义了两个关键 URL:
- Public URL: 用户浏览器访问 ONLYOFFICE 的地址(必须公网可达或客户端可达)。
- Inner URL: Odoo 后端服务器访问 ONLYOFFICE 的地址。
架构意图: 解决复杂的网络环境问题(如 Docker 容器、NAT)。
- 场景: Odoo 和 ONLYOFFICE 都在 Docker 内部网络中。
- 问题 : 用户通过
https://office.example.com访问,但 Odoo 如果去访问这个域名可能需要绕公网一圈甚至被防火墙拦截。 - 解法 :
onlyoffice_urlopen方法会优先使用 Inner URL (如http://onlyoffice:80) 进行服务器间的高速通信,而前端 iframe 依然使用 Public URL。
2. 数据与存储架构:无状态中间件模式
本模块采用了一种无侵入式 的存储策略,将 ONLYOFFICE 视为一个无状态的计算/渲染节点。
2.1 存储模型
- Odoo : 唯一的"数据所有者"。利用
ir.attachment表存储物理文件(Binary),利用documents.document表存储业务元数据(Odoo 企业版)。 - ONLYOFFICE: 临时缓存。它只在编辑会话期间持有文件副本。会话结束(回调完成)后,数据即视为过期。
2.2 性能边界分析
在 controllers.py 的 editor_callback 方法中存在一个潜在的性能瓶颈:
python
# 代码片段分析
datas = onlyoffice_urlopen(file_url).read() # <--- 警告点
if attachment.res_model == "documents.document":
datas = base64.encodebytes(datas)
- 内存压力 : 文件回写是全量加载到内存 的 (
read())。如果用户编辑了一个 500MB 的 PPT,Odoo 进程的内存会瞬间飙升。 - 架构建议: 对于超大文件场景,需监控 Odoo 服务器内存,或考虑在反向代理层限制上传大小。
3. 安全架构:信任链机制
由于 ONLYOFFICE 需要回调 Odoo,且不携带 Odoo 的 session_id Cookie,因此模块开放了 auth="public" 的路由。这是架构上的受控开口。
3.1 防御机制 (Defense in Depth)
代码通过三层防御确保 auth="public" 不会被滥用:
- JWT 签名 (Signature) :
- 所有请求头必须携带
Authorization(或自定义 Header),内容为 JWT Token。 - Token 由 Odoo 生成的
secret签名。ONLYOFFICE 服务器必须配置相同的 Secret 才能发起有效回调。
- 所有请求头必须携带
- 临时令牌 (Security Token) :
- URL 参数中包含
?oo_security_token=...。这是 Odoo 为当前用户生成的短期令牌,绑定了用户身份 (user_id)。
- URL 参数中包含
- 访问控制列表 (ACL) 检查 :
- 即便是公网路由,控制器内部依然调用了
attachment.validate_access(access_token)和check_access_rights。
- 即便是公网路由,控制器内部依然调用了
Part 2: 开发者实战手册 (Developer Cookbook)
本部分为实操指南,指导如何在自定义模块中集成 ONLYOFFICE 功能。
场景一:对接自定义文件模型 (Integrate Custom Model)
假设你开发了一个名为 my.file.manager 的模型,你想让其中的文件支持在线编辑。
1. 基础要求
你的模型必须将文件存储在 Odoo 的 ir.attachment 中。
Model 定义 (my_file_manager.py):
python
from odoo import models, fields, api
from odoo.exceptions import UserError
class MyFileManager(models.Model):
_name = 'my.file.manager'
_description = '自定义文件管理'
name = fields.Char("文件名", required=True)
# 关键点:设置 attachment=True,Odoo 会自动在 ir.attachment 创建记录
file_data = fields.Binary("文件内容", attachment=True)
file_name = fields.Char("文件全名")
def get_attachment_id(self):
"""辅助方法:查找当前记录对应的 ir.attachment ID"""
self.ensure_one()
domain = [
('res_model', '=', self._name),
('res_id', '=', self.id),
('res_field', '=', 'file_data') # 对应上面的 Binary 字段名
]
attachment = self.env['ir.attachment'].search(domain, limit=1)
if not attachment:
raise UserError("未找到关联的附件文件,请先上传文件。")
return attachment.id
def action_edit_onlyoffice(self):
"""按钮动作:打开编辑器"""
attachment_id = self.get_attachment_id()
# 使用模块内置的客户端动作,体验最佳(嵌入在 Odoo 界面内)
return {
'type': 'ir.actions.client',
'tag': 'onlyoffice_editor', # 复用 onlyoffice_odoo 注册的动作
'params': {
'attachment_id': attachment_id,
},
'target': 'current', # current=当前窗口, fullscreen=全屏, new=新标签页
}
XML 视图 (my_file_manager_view.xml):
xml
<record id="view_my_file_manager_form" model="ir.ui.view">
<field name="name">my.file.manager.form</field>
<field name="model">my.file.manager</field>
<field name="arch" type="xml">
<form>
<header>
<!-- 添加编辑按钮 -->
<button name="action_edit_onlyoffice"
string="在线编辑"
type="object"
class="btn-primary"
invisible="not file_data"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="file_name" invisible="1"/>
<field name="file_data" filename="file_name"/>
</group>
</sheet>
</form>
</field>
</record>
2. 进阶:编辑保存后的回调逻辑
Odoo 的 Binary 字段机制会自动更新 file_data 的内容。但如果你想在保存后做额外操作(例如:修改状态为"已修订"),需要监听 ir.attachment 的写入。
python
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
def write(self, vals):
res = super().write(vals)
# 监听内容变更 (raw 或 datas 字段)
if 'raw' in vals or 'datas' in vals:
# 过滤出属于你自定义模型的附件
my_attachments = self.filtered(lambda a: a.res_model == 'my.file.manager')
for attachment in my_attachments:
# 获取你的业务记录
record = self.env['my.file.manager'].browse(attachment.res_id)
# 执行自定义逻辑
record.log_edit_history() # 举例:记录日志
return res
场景二:实现"只读预览"功能 (Preview Only)
如果你只想让用户看文件,禁止修改(哪怕用户有写权限),可以使用以下方案。
方案 A:后端路由跳转法 (最简单,新标签页打开)
使用 onlyoffice_odoo 提供的专用预览路由 /onlyoffice/preview。
Python 代码:
python
def action_preview_onlyoffice(self):
self.ensure_one()
attachment_id = self.get_attachment_id()
# 1. 获取基础 URL
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
# 2. 构造参数
# file_content_url: 告诉预览器去哪里下载文件
file_content_url = f"/onlyoffice/file/content/{attachment_id}"
# 3. 拼接最终 URL
# title: 预览页顶部显示的文件名
preview_url = f"{base_url}/onlyoffice/preview?url={file_content_url}&title={self.file_name or self.name}"
return {
'type': 'ir.actions.act_url',
'url': preview_url,
'target': 'new', # 必须在新标签页打开
}
方案 B:JS 弹窗预览法 (体验最佳,模态窗口)
如果你想在当前页面弹出一个黑色背景的预览层(类似图片预览),需要写一点点 JS。
1. 创建 JS 文件 (static/src/js/preview_button.js)
javascript
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { FormController } from "@web/views/form/form_controller";
import { OnlyofficePreview } from "@onlyoffice_odoo/views/preview/onlyoffice_preview"; // 引入模块组件
// 这里我们Patch了FormController来演示,实际开发中建议写一个专门的组件或Field
patch(FormController.prototype, {
async onOpenPreview() {
// 假设你从某个地方获取到了 attachmentId 和 URL
// 这里仅为逻辑演示
const attachmentId = this.model.root.data.attachment_id_field;
const fileName = this.model.root.data.file_name;
this.env.services.dialog.add(OnlyofficePreview, {
title: fileName,
url: `/onlyoffice/file/content/${attachmentId}`,
close: () => { /* 关闭时的回调 */ },
});
}
});
场景三:常见问题排查 (Troubleshooting)
Q1: 点击编辑后一直在加载,显示 "Loading..."
- 原因 : 前端 iframe 无法连接到
doc_server_public_url。 - 检查: 打开浏览器控制台 (F12) -> Network,看是否有请求超时 (Timeout) 或连接被拒绝 (Connection Refused)。
- 解决 : 确保你在 Odoo 设置里填写的 ONLYOFFICE Docs address 是你浏览器能访问到的地址。
Q2: 提示 "Download failed" 或保存失败
- 原因: ONLYOFFICE 服务器无法连接回 Odoo 服务器。
- 检查: 这是一个典型的 Docker 网络问题。
- 解决 : 在 Odoo 设置里配置 Server address for internal requests from ONLYOFFICE Docs 。填入 Odoo 在 Docker 内部的 IP 或服务名(例如
http://odoo:8069)。
Q3: 提示 "Token is invalid"
- 原因: Odoo 和 ONLYOFFICE 的 JWT Secret 不一致。
- 解决 : 检查 Odoo 设置里的 Secret Key 与 ONLYOFFICE 配置文件 (
local.json或环境变量JWT_SECRET) 是否完全一致。
Q4: HTTPS 混合内容错误 (Mixed Content)
- 现象 : 浏览器控制台报错
Blocked loading mixed active content。 - 原因: Odoo 使用 HTTPS,但 ONLYOFFICE 地址是 HTTP。
- 解决: 必须为 ONLYOFFICE 配置 SSL 证书,确保两者同为 HTTPS。