很多人第一次看 Odoo 企业版 Website Appointment,会把它想成一个很简单的动作:
- 后台建一个 appointment type;
- 网站上给它一个链接;
- 用户选时间提交。
这个理解只覆盖了“前台表单”层面,没覆盖“网站内容对象”层面。
把 website_appointment 的 model、controller 和测试串起来看,会发现官方真正做的是:
把 appointment.type 同时做成“可预约对象”和“网站发布对象”,再围绕公开访客场景补上一整套兜底识别逻辑。
这里最值得看的有四条线:
- 预约类型的发布语义;
- 网站前台的操作员/资源选择流;
- 公开访客的时区、国家、客户识别 fallback;
- 多公司/多网站下的前台域名与公司边界。
一、appointment type 在这里不只是业务对象,还是 website 内容对象
website_appointment 对 appointment.type 做了多重继承:
website.seo.metadatawebsite.published.multi.mixinwebsite.searchable.mixin
这说明它不只是后台预约配置,而是能被:
- 发布到网站;
- 进入站内搜索;
- 挂 SEO 元数据;
- 生成 website_url。
_compute_website_url() 直接把它映射成:
/appointment/<id>
所以 appointment type 在网站场景里,已经不只是“可被预约”,而是“可被公开浏览、搜索、索引、分享”。
这就是为什么 Website Appointment 不是普通业务表单,而是内容+业务混合对象。
二、为什么复制 appointment type 要强制取消发布
copy_data() 在复制时会强制把 is_published=False。
这点很关键。
因为如果复制一个已经上线的预约类型,而系统默认让副本继续公开:
- 很容易把还没调好的副本暴露到网站;
- 也可能制造重复入口或错误预约页。
官方在这里的态度非常保守:
复制是后台编辑动作,不应自动继承前台可见性。
这和很多 CMS/电商对复制内容默认下线的思路一致。
三、为什么预约首页不是简单列表,而会按筛选结果直接跳转
controller 的 appointment_type_index() 会:
- 先按网站域、搜索词、invite token 等拼 domain;
- 再过滤 private appointment type;
- 如果最后只剩一个可用类型且没有搜索词,直接跳详情页。
这说明首页的职责不是永远给你一个目录,而是:
- 多个可选时做选择页;
- 只有一个可选时,直接缩短用户路径。
这很像典型网站漏斗优化,而不是后台菜单思维。
四、为什么前台会多一个“操作员选择页”
_get_appointment_type_page_view() 会根据条件决定是不是先展示 operator selection 页面。
触发条件大意是:
- 当前 appointment type 不是 auto assign;
- 不是 date-first;
- 还没选定 user/resource;
- 且可选用户或资源不止一个。
这说明 Odoo 不是默认把“选谁服务”塞进一个下拉框,而是:
在更适合的网站场景下,把它升级成一张专门的前台选择页。
尤其是基于 users 调度时,前台可以展示:
- 头像;
- 职位;
- 网站描述。
这已经明显是网站转化体验设计,而不只是业务表单字段渲染。
五、为什么还要支持 skip_resource_selection
虽然有专门的操作员选择页,但系统又保留了 skip_resource_selection。
这代表官方并没有把“先选人再选时段”当成唯一正解,而是承认两种漏斗:
- 先选人 / 资源,再看空档;
- 先看全局可预约时段,再决定由谁承接。
如果用户选择跳过,controller 会把:
user_selectedresource_selecteduser_defaultresource_default
清成空 recordset,让页面回到“看全量可用时间”的模式。
所以 Website Appointment 的前台流程是可分流的,不是固定单路径。
六、为什么公开访客没有账号时,时区还不会乱掉
create_and_get_website_url() 有一个很值得读的兜底:
- 如果没传
appointment_tz; - 先看当前用户时区;
- 没有的话再看
website.visitor.timezone; - 再不行才退回
UTC。
测试也明确覆盖了这三层 fallback。
这说明 Odoo 很清楚网站预约常见场景是:
- 访客未登录;
- 公共用户没有稳定个人配置;
- 但预约时间展示仍必须尽量贴近访客所在地时区。
所以前台时区不是“依赖登录用户”,而是“依赖 visitor 画像兜底”。
这点在公开网站尤其重要。
七、为什么客户识别也会回退到 visitor 和历史预约
_get_customer_partner() 的逻辑很有意思。
如果标准路径没找到 partner,它会继续尝试:
- 当前 visitor 的
partner_id; - 如果 visitor 没 partner,但当前是 public user,找这个 visitor 最近一次预约事件上的
appointment_booker_id。
这说明 Odoo 想避免的不是“找不到客户”,而是:
同一个公开访客反复预约时,不要不断新建重复 partner。
也就是说,visitor 在这里不是流量统计对象,而是 前台客户连续性锚点。
八、为什么国家也会从 visitor 回填
_get_customer_country() 先走标准定位逻辑,如果没命中,再回退到 website.visitor.country_id。
这和时区 fallback 是同一个设计思想:
- 网站用户可能没有登录;
- 也可能地理库没命中;
- 但预约类型本身可能受国家限制。
因此 visitor 上已有的国家信息就变得很关键。
这保证了:
- 网站筛选 appointment type;
- 或前台默认国家值;
不会因为缺登录态就全面失效。
九、为什么 recaptcha 校验放在提交入口,而不是页面展示时
appointment_form_submit() 在真正提交时才做:
_verify_request_recaptcha_token('appointment_form_submission')
这说明官方防的是:
- 机器人伪造提交;
- 而不是单纯阻止页面访问。
所以预约页本身仍然可以公开浏览,但真正落单动作必须过验证码校验。
这是一种很标准、也很克制的公开表单防滥用边界。
十、为什么多网站下视频会议链接还要看 appointment 对应 website
测试里专门验证了:
- 不同 website/domain 下的 appointment type;
- 生成的 videocall 链接要匹配对应网站域名;
- 即使事件读取者本身未必有 appointment type 读取权限,也要能拿到正确 base url。
这说明 Website Appointment 不是“后台用哪个 base_url 就统一拼”。
而是:
前台预约对象属于哪个 website,就应该从哪个网站域名对外表达。
这在多公司、多站点环境下非常关键。
十一、实战里最容易误解的 4 个点
误区 1:appointment type 只是后台预约配置
不是。
在这里它同时是网站发布内容。
误区 2:操作员选择只是下拉框 UI 差异
也不对。
它决定了网站预约漏斗是“先选人”还是“先看片段时间”。
误区 3:公开访客没有账号,就没法做可靠识别
不对。
visitor 提供了时区、国家和客户回连兜底。
误区 4:多网站下预约链接只要系统能打开就行
不行。
面向访客的 URL 和视频会议入口必须跟 appointment 所属网站一致。
总结
把 website_appointment 串起来看,你会发现 Odoo 企业版做的根本不是“给预约类型一个网站页”。
它真正做的是一套前台预约站点机制:
- 用 website mixins 把 appointment type 升级成可发布内容;
- 用 operator/resource selection 组织前台预约漏斗;
- 用 skip_resource_selection 支持不同预约路径;
- 用 website visitor 为时区、国家和客户身份做兜底;
- 用 recaptcha 守住公开提交入口;
- 用 website/domain 对齐 保证多网站前台体验正确。
所以 Odoo 企业版 Website Appointment 的本质,不是“预约表单上网站”,而是“把预约能力真正产品化成一个公开网站流程”。
这才是这部分源码最值得学的地方。
参考源码
- enterprise/website_appointment/models/appointment_type.py
- enterprise/website_appointment/controllers/appointment.py
- enterprise/website_appointment/tests/test_appointment.py
DISCUSSION
评论区