先说结论
Odoo Customer Portal 不是“把后台单据拿到前台展示一下”这么简单。
它实际搭起来的是一套文档浏览框架,里面至少有四层:
- 客户到底能看到哪些记录;
- 分页与筛选状态如何在 URL 里保持;
- 从列表点进详情后,上一条/下一条如何继续走;
- 登录态访问和 access token 访问如何共存。
所以 Portal 真正解决的,不是模板展示,而是:
让客户以网站体验去浏览一批受权限约束的业务文档。
一、为什么 domain 比模板更关键
以销售单为例,控制器里最重要的不是 HTML,而是 _prepare_orders_domain()。
它围绕 commercial_partner_id 去构建可见范围,核心思想是:
- 不是只看“当前联系人自己创建的单”;
- 而是看“这个商业主体下哪些联系人应该看到这批文档”。
这很典型地体现了 Odoo 的 B2B Portal 设计:
- 客户门户常常不是“个人中心”;
- 而是“公司文档入口”。
二、为什么 pager 在 Portal 里是产品能力,不只是工具函数
portal.py 里的 pager() 不只是算页码,它会同时负责:
- 当前页;
- 上一页、下一页;
- 页码序列;
- 首尾页;
- 带查询参数的 URL。
这代表分页在 Portal 里承担的是导航职责,而不是纯 offset 计算。
尤其当列表页同时带着:
- 排序;
- 日期过滤;
- 搜索条件;
如果分页不把这些状态一起维护,客户一翻页就会丢语境。
三、为什么 URL args 保持这么重要
Odoo 会把 date_begin、date_end、sortby 等参数交给 pager。
这件事表面很小,实际非常关键,因为它保证:
- 刷新页面后列表还保持同样筛选;
- 浏览器后退前进时上下文不乱;
- 链接可以复制给别人或自己稍后继续看;
- 分页不是“每次跳页都像重开一次查询”。
所以 Portal 的分页,本质上是在维护:
- 客户当前浏览的是哪一组文档结果。
四、为什么 session history 是详情页体验的关键
在 portal_my_orders() 这种列表方法里,Odoo 会把结果 id 放进 session 历史。
详情页再通过 get_records_pager() 构建:
- 上一条;
- 下一条。
这就带来一个非常自然的体验:
- 你先按某个条件筛出一批订单;
- 点进其中一张;
- 再沿着“刚才这批结果”继续前后翻。
注意这里的前后关系不是数据库全局顺序,而是:
- 你刚刚那次列表浏览结果的上下文顺序。
这正是它好用的地方。
五、为什么 access token 访问不是补丁,而是原生能力
订单详情页虽然是客户文档,但仍会允许:
- 登录用户按正常权限访问;
- 外部用户通过
access_token安全访问特定文档。
这对报价、付款、签署类场景特别重要,因为现实业务里经常需要:
- 邮件里直接给客户一个可打开的文档链接;
- 而不是先要求客户完成一整套登录流程。
所以 access token 不是“绕开权限”,而是:
- 文档级受控访问。
六、为什么 token 访问还会影响业务流程
在某些场景里,share user 带 token 打开文档,本身就会触发业务含义,例如“客户已查看报价”。
这说明 Portal 页面不是静态网页,而是业务流程的前台入口。
也正因为如此,Portal 的访问控制不能只从安全角度看,还要从:
- 已读感知;
- 客户互动;
- 跟进节奏;
这些业务后果去看。
七、最容易误解的几个点
误解 1:Portal 就是模板问题
不对。核心是 domain、pager、history、token 访问四件事。
误解 2:分页只要能翻页就行
不对。状态保持才是它真正的价值。
误解 3:上一条/下一条是锦上添花
不对。它决定详情页是不是延续同一次浏览上下文。
误解 4:access token 只是临时分享链接
不对。它也是正式业务入口的一部分。
最后一句
理解 Odoo Portal 列表页,重点不是表格长什么样,而是看懂这条主链:
partner/commercial partner domain → 带参数的 pager → session 记住结果集 → access token 或登录态进入详情 → 沿结果集继续导航。
看懂以后你就会知道,Portal 的真正价值不是“展示文档”,而是“让客户能顺畅、安全地浏览文档”。
DISCUSSION
评论区