很多人第一次看到 Odoo 企业版 Frontdesk,脑子里的理解都很朴素:
- 前台放一个二维码;
- 访客填姓名、电话、拜访谁;
- 系统给员工发个提醒;
- 完事。
这个理解不能说错,但太低估官方了。
如果你把 /home/ubuntu/odoo-temp/enterprise/frontdesk 里的 models/frontdesk_frontdesk.py、models/frontdesk_visitor.py 和 controllers/main.py 连起来看,会发现它真正要解决的不是“怎么做个签到页面”,而是下面这些更现实的问题:
- 公开 kiosk 怎么放出来,又不至于任何人随便伪造入口?
- 大屏二维码和访客手机自助签到,为什么不能共用一个永不过期的链接?
- 预约访客为什么不是简单 list,而是只暴露一个时间窗口内的 planned visitors?
- 通知为什么要拆成 frontdesk 负责人、host、饮品责任人三层,而不是统一发一条消息?
- host 没有系统账号、人在休假、或者访客还要点饮品时,链路怎么兜底?
所以 Frontdesk 的本质不是“访客表单”,而是“把公开签到入口、接待协作和内部通知责任拆清楚的前台编排层”。
这也是它比很多“扫码登记小工具”更像企业系统的原因。
一、先说结论:Frontdesk 不是单个页面,而是三层对象协作
从模型看,核心至少有三层:
frontdesk.frontdesk:前台站点/签到台配置;frontdesk.visitor:一次具体来访记录;frontdesk.drink:可选饮品与饮品责任人。
再加上控制器里的公开路由,整个系统其实分成两条世界:
外层:公开 kiosk 世界
这里解决的是:
- 访客能不能进入签到页;
- 能看到哪些 host;
- 能不能用手机接管签到;
- 预约访客能不能快速点选自己。
内层:员工协作世界
这里解决的是:
- 谁该被通知;
- 通过 discuss、邮件还是短信通知;
- host 不在线时是否有替代通道;
- 饮品等后勤事项怎么分发。
中间:visitor 记录作为衔接对象
Odoo 没把签到理解成“提交一个网页表单”。
它真正落地的是一条 visitor 业务对象生命周期:
plannedchecked_inchecked_outcanceled
也就是说,页面只是入口,visitor 记录才是真正的业务事实。
二、公开 kiosk 不是裸开放,而是 access token + 临时 token 双层校验
frontdesk.frontdesk 上有一个 access_token,默认就是 UUID。
控制器里 /kiosk/<frontdesk_id>/<token> 和 /kiosk/<frontdesk_id>/mobile/<token> 这两条公开路由,都会先走 _verify_token()。
它并不是只接受一个固定 token,而是支持两类令牌:
- 固定 kiosk token:来自
frontdesk.access_token; - 临时 mobile token:由
_get_tmp_code()生成的 HMAC + 当前时间拼接值。
这就很有意思。
为什么要双 token?
因为大屏 kiosk 和手机自助签到不是同一个安全场景。
固定 kiosk token
适合:
- 前台固定平板;
- 墙上长期二维码;
- 内网接待机。
它强调的是“站点身份稳定”。
临时 mobile token
适合:
- 访客从大屏扫二维码切到自己手机;
- 二维码短时间有效;
- 避免截图长期传播后仍能反复打开。
控制器对临时 token 的处理是:
- 验证 HMAC 是否匹配;
- 验证时间戳距当前是否不超过 1 小时。
所以官方不是简单把 kiosk 链接复制到手机,而是专门加了一层“可过期的移动入口”。
这说明它把移动签到看成“公开入口的二次授权”,不是同一个安全平面。
三、为什么 request.session.logout(keep_db=True) 很关键
_get_additional_info() 里第一句就是:
request.session.logout(keep_db=True)
这行很容易被忽略,但它很企业级。
因为 kiosk 是公开入口:
- 可能跑在共享平板上;
- 上一个人可能刚登录过后台;
- 浏览器可能残留内部会话。
如果不先 logout,公开签到页就可能带着内部身份上下文继续渲染,严重时会把权限和数据边界搞混。
所以 Odoo 先做的不是“渲染页面”,而是:
把当前 HTTP 会话先降回公共态,再允许进入前台界面。
这类细节,往往才是真正区分“企业模块”和“演示功能”的地方。
四、为什么预约访客只暴露前后 45 分钟窗口,而不是全量 planned
frontdesk_frontdesk.py 里定义了:
PLANNED_VISITOR_TIME = 45
_get_planned_visitors() 会只读取:
- 当前时间前 45 分钟;
- 当前时间后 45 分钟;
- 状态是
planned; - 且属于当前 station。
这不是一个随手写的常量,而是很明确的业务判断。
如果把所有 planned visitor 全放出来,会怎样?
会立刻出现三个问题:
- 隐私暴露过多:路过 kiosk 的人能看到更晚的来访计划;
- 误签到风险变高:访客可能误点到当天稍后甚至更晚的预约;
- 前台界面噪音太大:大量历史/未来预约会冲掉真正临近的来访对象。
所以官方的思路不是“让预约数据尽量全”,而是:
只把“当前前台真正可能发生”的那一小段预约窗口暴露给公开端。
这其实是把 kiosk 当成“现场接待界面”,而不是“访客数据库查询器”。
五、host 选择为什么受公司、联系方式和白名单三重约束
公开端取 host 时,不是直接把 hr.employee 全表扔给访客。
hosts_infos() 和 get_departments() 都做了明显限制:
- 只能看当前 frontdesk 所属
company_id; - host 必须至少有
work_email或work_phone; - 如果 frontdesk 配了
host_ids白名单,就只能从白名单里挑; - 搜索结果还会分页返回,并只给前端必要字段。
这说明官方在这里想回答的不是“访客能不能搜员工”,而是:
前台界面里哪些人应该被当作可接待对象公开出来。
为什么没联系方式的员工要被过滤掉?
因为访客选完 host 以后,系统马上要进入通知链路。
如果一个 host:
- 既没有 user;
- 也没有邮箱;
- 也没有工作电话;
那他在 Frontdesk 里就几乎不可达。
把这种人展示给访客,只会制造“能选但通知不到”的伪能力。
所以 Odoo 干脆在入口层就收窄候选集合。
六、visitor 创建不是纯建单,而是默认就把状态推进到 checked_in
控制器 prepare_visitor_data() 有两条路径:
路径 1:已有预约 visitor
如果传了 visitor_id,系统会:
- 直接把该 visitor 写成
checked_in; - 如果传了
drink_ids,先补饮品,再通知饮品责任人。
路径 2:现场新建 visitor
如果没有 visitor_id,系统会:
- 新建
frontdesk.visitor; - 带上 station、name、phone、email、company、host_ids;
- 直接写
state='checked_in'; - 同时把
check_in设为当前时间。
这很说明问题:
公开端不是“申请来访”,而是“确认已到场”。
所以 visitor 在 kiosk 提交之后,业务状态不是 planned,而是立刻进入现场态。
这也解释了为什么通知在这里立刻发生,而不是等人工审批。
七、通知为什么要拆成三类角色,而不是群发
frontdesk.visitor._notify() 和 _notify_to_people() 把接收方拆得很细:
- frontdesk 负责人
responsible_ids - 来访 host
host_ids - 饮品责任人
drink.notify_user_ids
这个拆分很重要。
1)frontdesk 负责人
他们收到的消息语义是:
- 某站点有访客到了;
- 访客是谁;
- 要见谁。
这是“前台运营视角”的通知。
2)host
他们收到的是“你的访客到了”。
这是“被拜访人视角”的通知。
3)饮品责任人
只有访客选了 drink 时才会触发。
这是“后勤服务视角”的通知。
所以 Frontdesk 不是一条签到消息广播给所有人,而是把一次来访拆成接待、会见、后勤三类责任。
这比“前台群里发一句某某到了”成熟得多。
八、为什么 discuss / 邮件 / 短信不是互斥,而是带兜底逻辑的组合
这块是源码里最值得读的地方之一。
很多产品会把通知通道做成一个 radio:
- 选 discuss;
- 或选 email;
- 或选 SMS。
但 Frontdesk 不是这么干的。
对有系统账号的 host
如果 host 对应 user_id,并且开启 notify_discuss,会优先走 discuss 私聊。
对没有系统账号的 host
如果 host 没有 user:
- 没法私聊;
- 但仍可能有
work_email; - 也可能有
work_phone。
于是源码就做了一个很细的 fallback:
notify_discuss=True但 host 没 user 时,- 如果没开 email/sms,也会根据 host 的邮箱或手机号兜底通知。
测试里也明确覆盖了这种情况。
这背后的思路非常务实:
系统配置表达的是“优先通道”,不是“如果这个通道不通就彻底不通知”。
企业协作里最怕的不是“通道不统一”,而是“人已经到了,结果一个都没通知到”。
Odoo 这里明显优先保证可达性。
九、host 休假检测说明 Frontdesk 不只是通知系统,还在做接待风险预判
create() 完成后会触发 _check_and_notify_visitor()。
这段逻辑会:
- 取 host 对应的
resource_id; - 在当前公司
resource_calendar_id上检查该时刻是否命中 leave interval; - 如果 host 在休假,就给 visitor 发邮件或短信提醒。
这个设计很妙。
因为它不是等 host 没响应后再补救,而是 visitor 建单时就先检查:
- 你要找的人是不是当前不可用;
- 如果不可用,要不要提前告诉来访者。
邮件正文里甚至会补上:
- 哪位 host 不可用;
- 其 manager 是谁;
- 可以联系 manager 或 frontdesk responsible。
这说明 Frontdesk 真正想做的是“现场接待降摩擦”,而不是“把签到动作数字化”。
否则它没必要管 host 是否正在 leave。
十、为什么 checked_out 时顺手把 served=True
write() 里对 state='checked_out' 做了两件事:
- 写
check_out当前时间; - 同时把
served=True。
这看起来有点奇怪:签出为什么会影响饮品服务状态?
本质上这是一个务实假设:
- 访客已经离场;
- 未完成的饮品服务也没必要继续挂着待办;
- 对 frontdesk dashboard 来说,应把“待送饮品”从现场队列里清掉。
也就是说,served 在这里不只是“真的已送达”,还带一点“现场服务任务已结束”的语义。
如果你后续二开饮品履约逻辑,这个隐含约定要注意。
十一、dashboard 统计为什么只看 planned / checked_in
_compute_dashboard_data() 聚合的是:
- 当前在场人数
checked_in; - 待到访人数
planned; - 未送饮品且未取消的数量;
- 最近签到时间。
它没有把 checked_out、canceled 混进“当前运营面板”。
这再次说明 Frontdesk dashboard 关心的是:
- 现场还有多少人;
- 近期还有谁要来;
- 后勤还有多少事没处理;
- 刚刚是不是有新签到。
也就是一个实时接待面板,而不是全历史报表。
十二、跨公司 public user 公司权限这处细节,说明官方很清楚 kiosk 是 public route
_onchange_company_id() 里有个容易被略过的处理:
- 如果 station 绑定了非当前环境公司;
- 且
base.public_user还没被授予该公司; - 就把该公司加进 public user 的
company_ids。
这段非常关键。
因为 kiosk 路由是 auth='public'。
也就是说,公开端读取 frontdesk 数据时,底层仍然要有一个“公共身份”去访问对应公司数据。
如果 public user 没有该公司的上下文,跨公司站点就可能打不开或读不到正确字段。
所以 Frontdesk 连“public user 的 allowed companies”这种底层细节都补上了。
这再次证明它不是临时网页,而是一个认真落地在 Odoo 多公司语义里的模块。
十三、实战里最容易误解的 4 个点
误区 1:Frontdesk 就是一个公开表单
不是。
它背后有:
- token 校验;
- session 降权;
- host 白名单;
- 预约窗口;
- 多通道通知;
- leave 预警。
误区 2:通知开 discuss 就够了
不一定。
如果 host 没 user 账号,真正能兜底的是 email/sms 可达性。
误区 3:planned visitor 就应该全部展示
错。
公开 kiosk 只暴露临近时间窗口,核心是减少隐私泄露和误操作。
误区 4:饮品只是一个 UI 附加项
也不是。
它对应独立责任人和单独通知链,已经属于接待后勤协作的一部分。
十四、对二开最有价值的启发
如果你准备扩展 Frontdesk,最值得沿用的不是页面样式,而是下面这些边界:
1)公开入口和内部对象分离
不要让 kiosk 页面直接拥有后台权限;永远让 token + public route 去承接入口。
2)把“谁该被通知”拆成角色而不是部门大群
frontdesk 负责人、host、后勤责任人,语义完全不同。
3)把“可见的人”限制成“可达的人”
如果一个 host 根本收不到通知,就别在公开端让访客选到他。
4)现场系统优先考虑异常兜底
host 请假、没 user、手机签到二维码过期,这些才是真实企业现场最常见的问题。
总结
把 frontdesk 源码串起来看,你会发现 Odoo 企业版并不是想做一个“扫码填表”的小功能。
它真正做的是一条完整的前台协作链:
- 用 固定 token + 临时 mobile token 管公开入口;
- 用 预约时间窗口 控制公开可见范围;
- 用 visitor 状态机 承接现场事实;
- 用 负责人 / host / 饮品责任人 三层通知拆责任;
- 用 discuss / email / sms fallback 保证可达;
- 用 leave 检测 提前暴露接待风险;
- 用 public user company 权限处理 保证多公司可用。
所以 Odoo 企业版 Frontdesk 的本质,不是“访客签到页”,而是“把公开接待入口、安全边界和内部协作责任编排到一起的现场运营模块”。
这才是这套源码真正值得学的地方。
参考源码
- enterprise/frontdesk/models/frontdesk_frontdesk.py
- enterprise/frontdesk/models/frontdesk_visitor.py
- enterprise/frontdesk/controllers/main.py
- enterprise/frontdesk/tests/test_frontdesk.py
DISCUSSION
评论区