ONLYOFFICE Odoo 集成架构深度解析与实战手册(odoo文件预览方案)

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.pyeditor_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" 不会被滥用:

  1. JWT 签名 (Signature) :
    • 所有请求头必须携带 Authorization (或自定义 Header),内容为 JWT Token。
    • Token 由 Odoo 生成的 secret 签名。ONLYOFFICE 服务器必须配置相同的 Secret 才能发起有效回调。
  2. 临时令牌 (Security Token) :
    • URL 参数中包含 ?oo_security_token=...。这是 Odoo 为当前用户生成的短期令牌,绑定了用户身份 (user_id)。
  3. 访问控制列表 (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。
相关推荐
一只专注api接口开发的技术猿2 小时前
微服务架构下集成淘宝商品 API 的实践与思考
java·大数据·开发语言·数据库·微服务·架构
Huanlis2 小时前
电力系统故障录波技术与架构解析
架构·行业
乾元3 小时前
数据为王——安全数据集的清洗与特征工程
大数据·网络·人工智能·安全·web安全·机器学习·架构
木斯佳3 小时前
Vue2/Vue3 迁移头秃?Renderless 架构让组件 “无缝穿梭”
架构·前端框架
linweidong4 小时前
多个供应商模块如何集成到统一的AUTOSAR架构中?
架构·autosar
路人与大师4 小时前
[深度架构] 拒绝 Prompt 爆炸:LLM Skills 的数学本质与“上下文压缩”工程论
android·架构·prompt
技术摆渡人4 小时前
第一卷:【外设架构】嵌入式外设移植实战与连接性故障“考古级”排查全书
驱动开发·性能优化·架构·安卓
xiaobobo33305 小时前
STM32中HAL库接口函数的共性以及架构思想
stm32·单片机·架构·数据处理器
M宝可梦5 小时前
新一代Transformer 架构MAT: Engram-STEM-PLE
深度学习·架构·transformer·deepseek·记忆机制