先给一句结论
Odoo Portal 的列表页,从来不是:
- search 一把
- render 一个模板
- 翻页继续 search
它真正做的是:
先定义“谁能看到哪些对象”,再定义“这批对象如何分页、排序和跳详情页”。
如果你把这两层混在一起,自定义 Portal 时很容易:
- 列表页漏数据
- 详情页越权
- 分页跳页后条件丢失
- access token 和登录态边界搞乱
第一层:Portal 列表的安全边界首先在 domain
看 addons/sale/controllers/portal.py,销售订单门户列表先准备 domain:
('partner_id', 'child_of', [partner.commercial_partner_id.id])
这句话的意义非常大。
它不是简单“只看当前联系人自己的单”,而是允许同一商业主体下的子联系人一起看。
也就是说,Portal 列表的核心不是模型 search 权限本身,而是:
- 当前门户用户映射到哪个 partner
- domain 把业务上允许看到的文档边界画在哪里
如果你自定义时只想“照抄一个 controller”,最容易漏掉的就是这层业务 domain。
第二层:分页不是装饰,它会固化你的查询条件
portal.pager() 做的不只是算页码。
它还会:
- 根据
total / step计算 page_count - 纠正非法 page 值
- 把
url_args拼回每个分页链接
这意味着如果你有:
- 日期范围
- sortby
- filterby
- search 关键字
却没把它们放进 url_args,那翻到第 2 页时条件就丢了。
于是你会看到一种非常常见的 bug:
- 第 1 页是筛选后的结果
- 第 2 页突然变成全量列表
问题不在 pager 算错,而在你没有把查询上下文带过去。
第三层:列表页和详情页的放权机制不是一套
列表页通常走:
auth='user'- 当前登录用户的 partner domain
详情页则常常还要处理:
auth='public'access_token
看 portal_order_page(),真正的入口不是你手写 search,而是 _document_check_access()。
这一步的心智模型是:
- 登录态能不能看
- 没登录但带 token 能不能看
- 没权限就 redirect / raise
所以很多人把列表页做得很严,结果详情页自己写了个 browse(order_id),边界立刻破掉。
第四层:session history 不是安全机制,是导航体验机制
销售 portal 列表会把当前页看到的 ids 放进 session:
my_orders_historymy_quotations_history
它的用途主要是:
- 在详情页里做上一条 / 下一条导航
这不是授权依据。
也就是说:
- session 里有 id,不代表用户就能看
- 真正访问详情页时,还得再过 document access 检查
这点非常重要。
很多自定义误把“我把 id 存 session 了”当成“这条记录已经授权过”,这是不成立的。
第五层:为什么 token 化详情页常和列表页看起来“不一致”
因为两者本来就不是一个入口模型。
列表页
- 面向当前登录用户
- 通过 partner domain 批量约束
token 详情页
- 面向单文档分享
- 通过 access token 放开一条具体记录
所以你可能会看到:
- 某人不在 portal 列表里看到订单
- 但拿着分享链接能打开这张订单
这不一定是 bug,而可能是产品设计。
关键在于你自己要清楚:
你做的是“用户工作台列表”,还是“单对象受控分享链接”。
这两者不要混成一套授权逻辑。
自定义 Portal 时最常见的 4 个坑
坑 1:只改 search,不改 count
分页总数通常来自 search_count(domain)。
如果列表数据用了新 domain,但 count 还沿用旧 domain,页码一定错。
坑 2:只改列表 domain,不改详情 access check
结果列表收紧了,详情页却还能猜 id 打开。
坑 3:忘了在 url_args 里保留筛选条件
翻页、排序、日期过滤经常因此失效。
坑 4:把 session history 当授权缓存
它只是导航辅助,不是权限证明。
一个稳妥的开发顺序
做 Portal 列表自定义时,建议按这个顺序:
- 先定义“谁能看哪些记录”的业务 domain
- 再定义 sort / filter / date range 如何影响 domain
search_count和search保持同一套 domain- 所有筛选参数都进
url_args - 详情页统一走
_document_check_access()或等价机制
这样做最不容易把安全边界和导航体验缠在一起。
结论
Portal 列表真正难的,不是模板或分页按钮,而是三件事必须始终一致:
- domain 负责谁能看
- pager 负责带着条件翻页
- 详情 access check 负责单对象最终放权
把这三层分清楚,Portal 自定义就稳很多;混在一起,越做越像“偶尔能用、偶尔越权”的玄学系统。
DISCUSSION
评论区