先说结论
在 Odoo CRM 里,“系统提醒你这条 lead 可能重复”和“系统认为这几条记录现在就该合并”,根本不是一回事。
官方源码至少拆成了三层语义:
- 潜在重复提示:界面层面的风险信号
- 可操作重复搜索:拿来分配、转换、处理的更保守集合
- 真正 merge:还要再经过主记录排序、字段合并与依赖迁移
所以如果你把 duplicate banner、duplicate count、merge wizard 当成同一个东西, 你对 Odoo CRM 的 dedup 机制就一定会误判。
最典型的误判有两种:
- 看见“Potential Duplicate”就以为系统已经确认应合并
- 看见某条旧 lost 记录和当前 lead 很像,就以为 merge 一定会把它拉进来
源码并不是这么做的。
一、界面上的“潜在重复”,定位是提醒,不是裁决
_compute_potential_lead_duplicates() 的注释写得很清楚,它是为了更高 volume 场景下高效算“潜在重复线索”。
这里用到的标准有三类:
email_domain_criterionphone_sanitized- 同一个
commercial_partner
这三条标准本身就说明它不是严格法官,而是风险探测器。
为什么?
因为它故意会把“相关度较高、但未必适合立刻 merge”的对象也找出来。
比如:
- 企业域名相同,可能只是同一家公司不同联系人
- 商业实体相同,可能是母子联系人与历史商机并存
- 电话一致,可能是共享总机或前台号码
这套逻辑更像在说:
这里有重复风险,值得有人看一眼。
它没在说:
这些记录现在就该合成一条。
这是第一层边界。
二、为什么 email_domain_criterion 比你想的更“宽”
Odoo 在重复提示里并不只拿完整邮箱精确比对。
email_domain_criterion 的设计带有很强的业务偏向:
- 如果是公共邮箱提供商,保留更接近完整邮箱粒度
- 如果是企业域名,可能退化到公司域名维度
这会带来一个非常现实的结果:
john@company.comsales@company.commary@company.com
它们在界面提示层可能会被纳入同一类重复风险观察面。
这不是 bug,而是 Odoo 想提前提醒你:
- 同一家公司是不是正在被多条线索并行推进
- 这里有没有分配冲突、归因冲突、商机碎片化风险
也就是说,提示层容忍“疑似过宽”, 因为它承担的是运营清洁度提醒,而不是最终合并裁决。
三、SEARCH_RESULT_LIMIT = 21 暗示了一个重要判断:结果太多,反而不算“有用重复”
_compute_potential_lead_duplicates() 里还有个很值得注意的保护:
- 如果某个条件搜出来的结果过多,超过阈值
- 它会认为这个条件的辨识度不够高
- 返回空模型级结果,而不是把大批对象都挂进 duplicate 集合
这个设计很聪明。
因为当你碰到:
- 一个超大企业域名
- 一个客服总机电话
- 一个热门公共入口
如果系统把几十上百条线索全标成彼此潜在重复, 这个提示就失去意义了。
所以 Odoo 明确承认:
重复提示不是越多越好,结果太大时说明条件本身不够可操作。
这其实比很多“宁可全报出来”的去重逻辑更成熟。
四、真正可操作的重复搜索,条件会更保守
和提示层相比,_get_lead_duplicates() 的语义明显更收敛。
它主要基于:
email_normalizedpartner_id
然后再叠加业务过滤:
- 默认只看
won_status = pending - 默认只看
active = True
这个差异非常关键。
说明官方在“可疑重复发现”和“实际处理重复”之间故意做了分层:
- 提示层可以宽一点
- 操作层必须更保守、更偏当前可执行对象
所以你完全可能看到一种情况:
- 界面上 duplicate count 很显眼
- 但真正用来处理的重复集合并没有你想象中那么大
这不是不一致, 这是两种目标不同的集合。
五、为什么默认会排除很多历史记录
_get_lead_duplicates() 默认不把已归档 / 已失效 / 已赢单对象都拉进主要重复处理集。
逻辑上它更像在问:
当前 pipeline 里,有哪些活跃且仍待处理的重复对象需要清理?
而不是:
历史上所有长得像的记录都给我列出来。
这个区别非常业务化。
因为 dedup 真正最危险的场景,不是历史库里“有点像”的记录太多, 而是当前在跑的 pipeline 里:
- 同一个客户被多销售并行跟
- 新旧线索互相抢负责人
- 当前归因和推进状态被打碎
所以默认排除非活跃历史记录,本质上是在保护当前运营动作。
六、include_lost=True 也不是把所有历史都捞回来
有人看到 _get_lead_duplicates(include_lost=True) 就会以为:
- 那所有 lost / archived / closed 历史都会一起参与 merge 决策
实际上源码仍然保留业务边界:
- won 的对象不是重点目标
- include lost 主要是允许把某些 lost opportunity 纳进观察面
- 但“是否应该真正处理它们”仍然取决于后续具体动作
换句话说,include_lost=True 是扩大可见边界,
不是取消所有处理边界。
这对历史数据很多的 CRM 很重要。
不然 dedup 逻辑就会变成:
- 每次发现一个新 lead
- 都把历史库几十条陈年记录拉进可操作集合
那系统会非常难用。
七、自动分配里的 dedup,目标是防止“同客多派”,不是做主数据审计
在 crm_team.py 的 lead assignment 流程里,官方会在 team allocation 阶段调用 _allocate_leads_deduplicate()。
这里的 dedup 目的非常明确:
- 候选 lead 在进入 team / salesperson 分配前
- 先看看是否和当前活跃 lead 构成明显重复
- 如有必要,先 merge,再继续分配
它不是在做公司级数据治理项目, 它是在做一件更务实的事:
别让同一个客户的重复线索被分给不同销售。
这是运营边界,不是主数据边界。
如果你理解错这个目标,就容易期待太多:
- 以为分配前去重会把所有历史脏数据都自动洗干净
- 以为一切“像重复”的对象都会被自动整平
这都不是官方设计目标。
八、真正 merge 之前,还要再过一层“谁最可信”的排序
就算记录进入 merge,Odoo 也不会机械地“把几条平均揉成一条”。
_sort_by_confidence_level() 会按这些线索给主记录排序:
- 仍可推进的优先
opportunity比lead更可信- 阶段更靠后的优先
- 概率更高的优先
- 更新 / ID 更靠后的优先
这再次说明:
- 提示层在问“是不是值得关注”
- merge 层在问“如果真要合,谁最像真实主链路”
两者根本不是一个问题。
九、实战里最容易踩的 5 个坑
1. 把 duplicate count 当成 merge 建议数
它只是风险面,不是最终动作集。
2. 看到同域名就一键合并
同公司不同联系人,在 B2B 场景下非常常见。
3. 误以为潜在重复一定包含当前最关键的活跃记录
提示层和操作层集合并不完全相同。
4. 想让 assignment dedup 顺手完成历史库清洗
它主要服务当前分配秩序。
5. 把 include_lost 理解成“取消边界”
它只是扩大观察面,不是让 merge 失控。
最后一句
Odoo CRM 的 dedup 真正成熟的地方,不是它会告诉你“哪些像重复”, 而是它很清楚:
提示、处理、合并,是三件不同的事。
只要你把这三层边界分开,很多关于 duplicate banner、merge wizard 和历史记录处理的困惑都会一下子变清楚。
DISCUSSION
评论区