很多人第一次看 Odoo POS Self Order,会把它理解成:
一个公开菜单页,顾客扫码后自己下单。
这描述太轻了。 如果它真的只是“公开页面 + 下单接口”,那你很快就会遇到一连串问题:
- 任何人拿到链接是不是都能替这家门店下单;
- 同一个链接到底代表整店、某张桌还是某个 kiosk;
- 顾客请求是以谁的身份在 Odoo 里执行;
- 订单能不能随便改桌、删单、改价格。
结论先说:Odoo POS Self Order 不是彻底匿名的开放 API,而是一套“入口校验 + 模式分流 + 桌台上下文 + 默认用户降权执行”的受控自助下单机制。 它看起来是公开网页,实际上后台一直在小心收紧边界。
入口校验的第一层:不是知道 config_id 就算有权限
在 controllers/self_entry.py 的 _verify_entry_access() 里,系统先检查:
config_id是否存在且为数字;- 对应
pos.config是否存在; self_ordering_mode是否不是nothing;- 如果带
access_token,token 是否和配置匹配。
这说明自助点餐入口不是“只要猜到 config_id 就能进”。
更关键的是,源码把 config token 分成两种状态:
- 带 token 访问;
- 不带 token 访问。
而最终是否真正保留 token 能力,还要看配置模式与会话状态。也就是说:
token 不是装饰参数,而是入口能力的一部分。
为什么请求不是用 public 用户直接到处跑
虽然 route 是 auth="public",但 _verify_entry_access() 并没有让后续逻辑一直以匿名用户执行。
它会取:
company = pos_config_sudo.company_iduser = pos_config_sudo.self_ordering_default_user_id
然后构造:
with_company(company).with_user(user).with_context(allowed_company_ids=company.ids, ...)
这层非常关键。它表示 Odoo 的想法不是“匿名顾客直接操作业务对象”,而是:
顾客通过公开入口触发流程,但真正业务执行要落到一个受控的默认用户上下文里。
好处很明显:
- 权限边界更可控;
- 公司上下文更稳定;
- 多公司下不容易串库;
- 数据读取与写入范围不必完全暴露给 public 用户。
mobile 和 kiosk 为什么不是同一个入口换皮
很多实施会把 mobile 和 kiosk 理解成“一个手机版、一个大屏版”。其实源码里的差别更深。
在 _verify_entry_access() 和 process_order() 里,至少有四个关键区别:
- mobile 模式要求 active session 才真正可用;
- mobile 模式会解析
table_identifier,并尝试绑定到restaurant.table; - kiosk 模式不走桌台绑定,而更像公共自助终端;
- tracking number 前缀不同:kiosk 用
K{config.id}-,mobile 用S。
这说明 Odoo 并不是只在前端 UI 上区分设备,而是在后端订单语义上就区分:
- 这是桌边扫码单;
- 还是门店公共终端单。
为什么 table_identifier 不是可有可无的前端参数
对于 mobile 自助点餐,table_identifier 不是“顺便传个桌号文本”,而是会参与:
- 桌台对象查找;
- parent table 回退;
- 订单
self_ordering_table_id绑定; - 后续 draft 单聚合和用户取单逻辑。
在 process_order() 里,订单会写:
self_ordering_table_id = table.idsource = 'mobile'或'kiosk'floating_order_nametracking_number
这意味着桌台上下文一旦绑定,不只是前端显示“桌 12”,而是会变成订单结构的一部分。
所以如果你在现场看到“顾客扫了 A 桌二维码,订单却跑到 B 桌”这种问题,优先别怀疑 UI 文案,而要去查:
table_identifier是否正确;- 该 table 是否 active;
- 是否被 parent table 重写;
- 订单写入时
self_ordering_table_id实际落成了谁。
为什么系统还要单独做删单 token 校验
在 remove_order() 里,除了入口 access_token 外,删单还要额外校验 order_access_token,而且只有 draft 单能删。
这很值得注意,因为它说明:
- 配置入口 token 只证明“你能进入这个自助点餐环境”;
- 订单 access token 才证明“你能操作这张具体订单”。
这是一种典型的双层边界:
- 店级入口权限;
- 单级对象权限。
如果你忽略这一层,就会误以为拿到自助点餐入口就能删除别人订单。Odoo 显然不想这么干。
为什么 sync_from_ui() 之后还要回头验价
process_order() 先把订单丢给 pos.order.sync_from_ui(),随后又执行 _verify_line_price() 与 _compute_combo_price()。
这正说明一件事:
自助点餐前端可以描述点了什么,但不能完全定义最终价格。
尤其是 combo、属性加价、fiscal position、pricelist 等场景,后端会重新校验和计算。否则一个公开页面就等于把定价真相交给浏览器,这在商业上太危险。
最容易误解的四件事
误区一:Self Order 就是匿名商城接口
不对。 它虽然以 public route 暴露,但后端真正执行依赖默认用户、公司上下文和 token 校验。
误区二:知道 config_id 就能下单
不对。 还要看配置模式、token、active session,以及具体模式是否允许。
误区三:mobile 和 kiosk 只是页面长得不一样
不对。 两者在桌台绑定、tracking number、订单来源语义上都不同。
误区四:二维码里的桌号只是前端展示信息
不对。
table_identifier 直接决定订单会不会绑定到正确桌台。
实战排错顺序
如果你遇到“扫码打不开 / 能看菜单不能下单 / 下单跑错桌 / 删单失败 / kiosk 与 mobile 行为不一致”,建议按这个顺序查:
pos.config的self_ordering_mode是什么;- 入口 URL 的
config_id和access_token是否正确; - 当前配置是否有 active session;
self_ordering_default_user_id是否存在且权限足够;- mobile 模式下
table_identifier是否命中 active 桌台; - 是否被 parent table 回退改写;
- 订单写入后
source、tracking_number、self_ordering_table_id是否正确; - 删单时
order_access_token是否匹配且订单是否仍为draft。
最后的结论
Odoo POS Self Order 看起来像一个“谁都能打开的点餐页”,但它真正的设计重点不是开放,而是可控开放:
- 入口有配置与 token 校验;
- 执行有默认用户与公司上下文;
- mobile 与 kiosk 有不同语义;
- 订单与删单都有对象级边界。
所以更准确的理解应该是:
自助点餐不是把 POS 暴露到公网,而是在公网入口外面再套了一层受控业务壳。
DISCUSSION
评论区