先说结论
Odoo 的商品对比功能,不是把几个商品卡片横着摆一排这么简单。
website_sale_comparison 真正在解决的,是“如何把不同商品的可比信息组织成一个可读、可决策的页面”。
它至少处理了这些问题:
- 对比页怎么取商品;
- 最多允许同时比较多少个;
- 变体商品到底展示哪一层属性值;
- 属性应该如何按类别分组,而不是乱成一张大表;
- 价格、划线价、图片 URL 用什么口径回给前端。
所以这个模块更像:
一套把商品复杂配置翻译成“用户可比较信息结构”的展示引擎。
一、为什么对比页本质上是一个“受约束的选择集合”
/shop/compare 会从 URL 参数里读取 products,再用 product.product 去搜索有效记录。
这里至少传达了两个信号:
- 对比页对的是 具体变体级商品,而不只是模板;
- 最终能否展示,还要重新经过搜索与读取权限校验。
换句话说,对比页不是前端自己缓存几条卡片就算完成,而是后端重新确认:
- 这些商品 ID 是合法的吗;
- 当前访客能不能读到;
- 页面应该基于哪一批真实商品对象渲染。
这能避免前端状态漂移太远,也让分享型 compare URL 更可靠。
二、为什么系统要强行限制最多 4 个商品
前端交互里明确写了:
You can compare up to 4 products at a time.
很多人第一反应会觉得这是 UI 保守,但从产品角度看,这其实很合理。
因为商品对比不是越多越好。
当对比项一多,页面会迅速出现三个问题:
- 横向信息密度爆炸;
- 属性行数过多导致用户根本看不完;
- 购买决策反而被拖慢。
Odoo 把上限收在 4,本质上是在承认一个事实:
对比页的目标不是做数据库导出,而是帮助用户尽快做出购买选择。
这和“功能越多越强”是两种完全不同的产品思路。
三、为什么属性必须先按 category 分组
_prepare_categories_for_display() 在 product.template.attribute.line 和 product.product 两层都做了相似动作:
- 先拿到属性;
- 按
product.attribute.category排序和分组; - 没有 category 的属性也不会丢,而是放进一个空类别槽位;
- 最终返回有序的
OrderedDict。
这背后的好处非常直接:
如果没有 category,对比页会变成一串无序属性:颜色、尺寸、材质、续航、接口、重量全部混在一起。
而有了 category 之后,页面才能呈现更接近人类决策方式的结构,比如:
- 基础特性
- 设计与尺寸
- 选项与配置
- 续航与性能
这一步看似是显示细节,实际上决定了用户能不能“扫读”对比结果。
所以 Odoo 不是简单地把属性表扔到模板里,而是在源码层先做信息架构整理。
四、为什么变体值展示不是只拿当前值那么简单
product.product._prepare_categories_for_display() 有一段非常关键:
对每个属性,系统会优先拿当前 product 的 product_template_attribute_value_ids;
如果某些属性属于 no_variant 情况,没有具体变体值,它就退回到 attribute line 上的全部可能值。
这个设计很重要。
因为现实里并不是每个可展示属性都会映射成一个真正切换 SKU 的变体。
如果系统只展示“当前变体值”,你可能会出现:
- 某些属性在比较表里莫名空白;
- 用户误以为商品没有该属性;
- 模板层配置和变体层表现严重脱节。
Odoo 的处理更稳妥:
对比页要展示的是“用户认知上属于这个商品的信息”,而不只是严格的 SKU 差异字段。
这让对比更接近商品说明,而不是数据库结构裸露。
五、为什么价格返回的是组合信息而不是裸字段
/shop/compare/get_product_data 不直接读简单价格字段,而是调用 _get_combination_info_variant()。
然后再从组合信息里取:
display_namewebsite_urlimage_urlpriceprevent_zero_price_salecurrencylist_price或compare_list_price对应的划线价
这说明对比页价格不是静态 catalog 数据,而是:
- 已经考虑变体上下文;
- 已经考虑是否显示划线价;
- 已经考虑零价商品的销售限制;
- 已经返回可直接给前端渲染的口径。
对前台体验来说,这很关键。
因为用户在 compare 页看到的价格,必须尽量和商品页、变体选择后的价格保持一致,否则信任立刻下降。
六、为什么图片 URL 也要单独封装
_get_image_1024_url() 专门通过 website.image_url() 返回本地图片地址。
这看起来像小事,但它体现出 Odoo 的一致性思路:
- 对比页不应该自己猜图片路径;
- 图片输出要走 website 层统一口径;
- 前端拿到的是可直接消费的 URL,而不是还要再拼装的字段片段。
这能减少前端模板和不同页面之间的实现偏差。
七、为什么商品对比其实在倒逼商品数据建模
很多团队上线 compare 功能后才会发现:
- 属性没有分类;
- 属性命名不统一;
- 有些信息写在销售描述里,没有进属性体系;
- 变体和 no_variant 属性边界混乱。
一旦这样,对比页就会立刻暴露数据治理问题。
因为 compare 页是最不讲情面的:
它会把你的商品结构原封不动地并排摆出来。
所以 website_sale_comparison 的价值不只在前端功能,还在于它反过来督促你把商品属性建模做干净。
八、实施时最容易踩的坑
1. 只开功能,不整理 attribute category
结果就是表格很长,但用户完全抓不到重点。
2. 误把模板描述当可比较数据
说明文案可以很长,但对比页真正需要的是可结构化字段。
3. 忽视 no_variant 属性
如果你的数据设计里这类属性很多,却没理解回退逻辑,就容易误判页面为何出现“多值展示”。
4. 让 compare 页承载过多商品
即便你能技术上放开限制,用户认知负担也未必能承受。
最后总结
website_sale_comparison 真正做的不是“商品并排显示”,而是:
- 控制比较集合的大小;
- 以变体为核心组织对比对象;
- 把属性按 category 重构成可读结构;
- 在变体值和模板值之间找到合理展示边界;
- 保证价格、图片和展示名称与前台口径一致。
所以它本质上不是一个表格插件,而是:
把 Odoo 商品配置体系翻译成购买决策页面的一层解释器。
如果你的商品数据建模清楚,它会非常有帮助; 如果你的属性体系很乱,它也会毫不客气地把问题摊在用户面前。
DISCUSSION
评论区