先说结论
Odoo 的网站表单,从来不是“前端把字段 POST 上来,后端顺手 create 一条记录”。
从 /home/ubuntu/odoo-temp/addons/website/controllers/form.py 与 models/website_form.py 来看,这条链路至少做了五层控制:
- 模型是不是允许被网站表单调用;
- 字段是不是白名单字段;
- 输入值能不能按字段类型安全转换;
- 非标准字段、元数据、附件该落到哪里;
- 模型自身有没有机会再做一轮业务过滤。
所以真正的设计思路是:
网站表单是一个“受限的录入管道”,不是开放写库接口。
这就是为什么很多项目里“页面上看着能填,提交却不生效”“附件传了但业务字段没更新”“自定义字段丢失”这些问题,根源都不在前端,而在这条管道的边界理解错了。
一、入口为什么看起来宽松,实际却比普通 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_namewebsite_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 -> integerone2many -> 逗号分隔转整型列表many2many -> 组装 ORM 命令binary -> base64tags -> 特殊分隔解析
这里透露出一个很务实的原则:
网站表单不是 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
这类信息不会默认总是落库,而是要显式开启。
这背后的考虑很现实:
- 有些业务场景确实需要线索来源和环境证据;
- 但并不是每个表单都适合长期堆积这些元信息;
- 元数据越多,后续清理、审计、合规处理成本越高。
所以它被设计成“可选增强”,而不是强制基础项。
六、附件为什么不是一概挂到字段上
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
评论区