网站表单

Odoo 网站表单为什么不是“提交一下就写库”:字段白名单、附件归档与默认字段主链路讲透

很多人把 Odoo 网站表单理解成一个通用 POST 入口,但 website 的表单链路实际同时处理模型准入、字段白名单、类型转换、自定义字段汇总、附件落库与默认字段回填,远比“收个表单”严格。

网站
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的网站表单,从来不是“前端把字段 POST 上来,后端顺手 create 一条记录”。

/home/ubuntu/odoo-temp/addons/website/controllers/form.pymodels/website_form.py 来看,这条链路至少做了五层控制:

  1. 模型是不是允许被网站表单调用
  2. 字段是不是白名单字段
  3. 输入值能不能按字段类型安全转换
  4. 非标准字段、元数据、附件该落到哪里
  5. 模型自身有没有机会再做一轮业务过滤

所以真正的设计思路是:

网站表单是一个“受限的录入管道”,不是开放写库接口。

这就是为什么很多项目里“页面上看着能填,提交却不生效”“附件传了但业务字段没更新”“自定义字段丢失”这些问题,根源都不在前端,而在这条管道的边界理解错了。


一、入口为什么看起来宽松,实际却比普通 create 更克制

表单主入口是:

  • /website/form/<string:model_name>

它是 auth="public",而且 csrf=False,这很容易让人误以为“它很开放”。

但源码接着马上补了两层现实约束:

1. 已登录会话仍做局部 CSRF 校验

控制器里会先取出 csrf_token。如果当前 session 已经登录,而 token 不合法,就直接报 Session expired

为什么不是一刀切强制 CSRF?

因为很多网站表单会被嵌入外部页面,浏览器的 SameSite 策略可能让 cookie 丢失。如果强制完整 CSRF,很多合法匿名表单反而会被误杀。

所以 Odoo 的做法是:

  • 匿名访客:允许更宽松提交
  • 已登录用户:仍然要求 session 级别安全校验

2. 整个写入包在 savepoint 里

website_form() 不是简单 try/except,而是把真正处理逻辑包进 request.env.cr.savepoint()

这意味着:

  • 表单内部出错时,不会把整个请求事务拖崩;
  • 但已经在这次 savepoint 内做的写入,也不会半成功半失败地留下脏数据。

这类设计很像“公共入口做局部隔离”,尤其适合高频前台提交。


二、模型为什么不是你写什么就能投什么

_handle_website_form() 第一件事,不是抽字段,而是先查 ir.model

  • model = model_name
  • website_form_access = True

也就是说,模型要想接网站表单,必须先显式开启 website_form_access

这一步很关键,因为它把风险从“所有模型默认暴露”改成了:

只有被明确授权的模型,才有资格作为表单落点。

如果没有这层开关,网站表单几乎等于给任何模型开了一个公共写入入口,风险会非常夸张。

website_form.py 里甚至进一步把 ir.model.fields.website_form_blacklisted 做成了“默认真”的白名单机制:

  • 新字段默认黑名单;
  • 只有被明确放开的字段,才会进入可写集合。

所以别被字段名里的 blacklisted 迷惑,实际治理思路是:

  • 默认不让写
  • 必要时逐个放开

这比“默认全开,再补黑名单”稳得多。


三、字段提取为什么是类型驱动,而不是原样塞进 ORM

extract_data() 是整条链路最值得读的部分。

它不会把 request 参数原样丢给 create(),而是先按字段类型走 _input_filters

  • integer -> int()
  • float/monetary -> float()
  • html -> plaintext2html()
  • many2one -> integer
  • one2many -> 逗号分隔转整型列表
  • many2many -> 组装 ORM 命令
  • binary -> base64
  • tags -> 特殊分隔解析

这里透露出一个很务实的原则:

网站表单不是 ORM 的裸 API,而是“浏览器输入 → 业务字段”之间的一层协议适配器。

这有三个直接后果。

1. 字段类型不对,错误会被提前拦下来

比如整数传了非数字、浮点传了错格式,错误会被收集进 error_fields,而不是放到更深层才炸。

2. 文件字段会分成“真正二进制字段”和“普通附件”两条路

如果上传字段本身就在白名单里,且目标字段类型真是 binary,它会直接转 base64 写入业务字段。

否则,它不会强塞,而是先记成附件,后面统一处理。

3. properties 字段不是普通字段

website_form.py 里专门扩展了 properties 类型,把项目里那种“动态属性定义”拆成伪字段,再按定义记录重新收拢。

这说明表单构建器并不只支持“硬编码字段”,它还在兼容 Odoo 里越来越多的元字段场景。


四、为什么自定义字段不会直接消失,而是被汇总进默认字段或消息

很多人第一次看这段源码会惊讶:

  • 白名单外的普通文本字段,并不会立即报错;
  • 它们会被当成 custom_fields 收集起来。

最后,系统会把这些内容拼成一段“Other Information”,再决定落点。

1. 如果模型配置了默认承接字段

也就是 website_form_default_field_id,这段自定义内容就会被写进指定文本字段。

典型理解方式是:

  • 结构化字段走结构化写入;
  • 非结构化补充说明,走一个统一备注字段。

2. 如果模型没有默认字段,但支持 chatter

就通过 _message_log() 把这些信息记成消息。

也就是说,Odoo 的态度不是“白名单外的一律丢弃”,而是:

不破坏结构化建模的前提下,尽量保留用户提交的上下文。

这对实施很重要。

很多官网询盘、售后工单、活动报名都会带一些临时补充项。如果要求每个字段都先建模,维护成本会很高;如果全当自由文本,又没法做流程自动化。Odoo 的折中方案正是“结构化字段 + 自定义尾注”。


五、元数据为什么默认不是每次都记,而是按参数开关决定

源码里还有一段经常被忽略:

  • website_form_enable_metadata

当这个参数开启时,系统会额外写入:

  • IP
  • User-Agent
  • Accept-Language
  • Referer

这类信息不会默认总是落库,而是要显式开启。

这背后的考虑很现实:

  1. 有些业务场景确实需要线索来源和环境证据;
  2. 但并不是每个表单都适合长期堆积这些元信息;
  3. 元数据越多,后续清理、审计、合规处理成本越高。

所以它被设计成“可选增强”,而不是强制基础项。


六、附件为什么不是一概挂到字段上

insert_attachment() 很值得实施同学仔细看。

它会先创建 ir.attachment,然后再判断:

  • 这个上传字段是否对应授权字段;
  • 对应字段是不是 many2one 或可挂附件的字段;
  • 如果对不上,是否需要作为孤儿附件记录到 chatter。

换句话说,Odoo 把附件分成三类:

1. 明确绑定业务字段的附件

这种会直接回写到目标字段。

2. 业务记录的补充附件

如果字段名不在授权字段里,但模型支持 _message_log(),附件会通过消息附带到记录上。

3. mail.mail 这种特殊模型

邮件模型没有常规业务 chatter 回退路径,所以孤儿附件会被挂进 attachment_ids

这说明 Odoo 很清楚:

上传文件不一定等于“某个字段的值”,很多时候它只是“这条业务沟通的证据材料”。

如果你在项目里把所有文件都强行映射成业务字段,最后往往会越做越别扭。


七、模型自己的 website_form_input_filter() 为什么是最后一道业务闸门

extract_data() 尾部,源码还留了一个很重要的扩展点:

  • website_form_input_filter(self, request, values)

这意味着即便前面已经做完:

  • 字段授权;
  • 类型转换;
  • 基础校验;

模型自己仍然可以在落库前改写值。

这一步特别适合做:

  • 默认 team / user / medium 注入;
  • 名称自动拼装;
  • 来源渠道补齐;
  • 一些不适合前端暴露、但又必须写入的业务默认值。

很多人做网站表单定制时,第一反应是改 controller。其实从 Odoo 的设计来看,更稳的做法往往是:

  • 让控制器保持通用;
  • 把业务差异放回模型自己的过滤方法。

这样复用性更好,也不容易在升级时整段冲突。


八、最容易误解的,不是“怎么提交”,而是“什么应该建模”

网站表单项目里,最常见的误解有三个。

误解一:能在页面放出来的字段,就一定能写进去

不对。

页面可见 ≠ 后端白名单允许。

误解二:上传了文件,就一定会进入某个业务字段

也不对。

很多附件只会成为记录的附加材料,不会变成结构化字段。

误解三:为了灵活,最好什么都走自定义字段

短期看很省事,长期通常会毁掉可搜索性、自动化和统计能力。

更合理的做法是:

  • 高频流程字段做结构化;
  • 低频补充信息走默认备注字段;
  • 文件按“业务字段 / 沟通证据”分层处理。

九、实战里该怎么设计 Odoo 网站表单

如果你要给官网询盘、售后申请、课程报名或下载申请做表单,我更建议按下面的顺序思考。

1. 先决定目标模型是否真的该开放给网站表单

不是所有模型都适合直接接前台写入。

2. 再决定哪些字段必须结构化

比如:

  • 联系方式
  • 产品/服务意向
  • 国家地区
  • 来源渠道
  • 优先级

这类后续要分派、统计、自动化的字段,应该进白名单。

3. 补一个默认文本字段承接自由输入

这样你既不会丢信息,也不至于为了每个备注项都改模型。

4. 明确附件语义

到底是:

  • 证件/简历这类主材料;
  • 还是补充截图、需求说明。

这会决定你应该挂字段,还是让它作为 chatter 附件存在。

5. 把业务规则留给模型扩展点

例如自动命名、默认销售团队、默认负责人、线索来源归因,都放在 website_form_input_filter() 会更顺手。


结语

Odoo 网站表单真正解决的,不是“网页怎么提交数据”,而是:

如何让一个公开入口,在尽量灵活的同时,仍然保持模型安全、字段可控、附件可追踪、补充信息不丢失。

所以理解这条链路的最佳方式,不是把它当成 HTML 表单控制器,而是把它看成:

  • 一个受限写入网关;
  • 一个结构化与非结构化并存的录入管道;
  • 一个把前台输入安全落到 Odoo 业务对象上的转换层。

当你用这个视角回头看很多实施问题,为什么某些字段总是“前台有、后台没”,为什么附件总“传了但没进字段”,答案往往都在这条主链路里。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。