先说结论
Website Reveal 不是“访客一打开页面,系统立刻查 IP 并建商机”。
Odoo 这里做的是一条延迟批处理链路:
- 先根据规则把命中的访问记到
crm.reveal.view; - cron 再按 IP 成批取待处理记录;
- 批量调用 reveal 服务;
- 成功、未命中、额度不足分别走不同回写分支。
所以 Reveal 的核心不是“识别某个访客”,而是把潜客识别做成一条可控、可停批、可去重的后台任务。
一、为什么规则要先缓存,而不是每次页面访问都现查数据库
_get_active_rules() 用了 @tools.ormcache(),这说明 Odoo 一开始就把 Reveal 规则当成了高频读取、低频变更的数据。
它会把规则整理成两层结构:
country_rules:按国家 code 先缩小可匹配规则范围;rules:保存 regex、website_id、state_codes 等细节。
这样做的收益很直接:
- 页面访问发生时,不用每次都全表扫规则;
- 先按国家初筛,再做 URL / website / state 匹配;
- 规则变更时再通过
create/write/unlink清缓存。
也就是说,Odoo 并不想把 Reveal 做成“每次 PV 都重新算一遍完整规则集”的实时系统,而是更偏向读缓存、改规则时失效的方案。
这非常像后台营销/潜客识别场景该有的风格:
优先控制吞吐和稳定性,而不是追求每次浏览都做最重的实时判断。
二、为什么 Reveal 不直接按页面访问建 lead,而要先落 crm.reveal.view
这是很多人第一次看源码时最容易误读的地方。
如果直接在页面访问当下就调 IAP:
- 请求会跟着前台访问量波动;
- 同一 IP 的多次访问很容易重复消耗;
- 外部服务慢时会把前台链路拖住;
- 没额度时也不好优雅停下来。
所以 Odoo 选择先落中间层 crm.reveal.view,再由 _process_lead_generation() 统一拉取待处理数据。
这带来三个实际好处:
- 前后台解耦:前台只记“值得处理的访问”;
- 批量优化:后台按批调用 IAP,成本更可控;
- 状态可追踪:待处理、未命中、已消费都能落状态。
因此 Reveal 更接近消息队列思路,而不是同步 RPC 思路。
三、按 IP 聚合处理,说明 Odoo 真正在防“重复识别同一家公司”
_get_reveal_views_to_process() 直接写 SQL,把 crm_reveal_view 按 reveal_ip 聚合,并按规则 sequence 收拢成数组。
这一步很关键。
它说明 Odoo 关心的不是“多少条 page view”,而是:
- 哪些 IP 还没处理;
- 同一个 IP 命中了哪些规则;
- 这组规则按什么优先级交给服务侧判断。
换句话说,PV 是原料,IP 才是 Reveal 的处理单位。
这能显著降低重复识别成本,因为一个公司访客在同一时段内可能刷多个页面,但从潜客识别角度,你通常并不想为这些页面各建一条线索。
四、credit_error 为什么不是普通失败,而是“停批信号”
_perform_reveal_service() 里最值得注意的分支,不是 success,也不是 not_found,而是 credit_error。
一旦服务回了额度不足:
- Odoo 会通知无更多 credits;
- 返回
False,让外层循环停止继续拉下一批; - 不再把这个错误当作普通单条失败去吞掉。
这说明在 Reveal 设计里,额度不足不是某个 IP 的个别异常,而是整条后台任务的资源边界。
这和很多人想的不一样:
- 不是“这一条建不了,下一条继续试”;
- 而是“预算边界已经撞到,整批都该停下”。
这是非常合理的,因为 Reveal credits 是共享预算,不是每个 IP 各自独立的钱包。
五、为什么 not_found 和“没有返回结果”要单独回写状态
对于返回 not_found 的 IP,源码会把对应 views 标成 reveal_state = 'not_found'。
而不是简单放着不管,等下一轮 cron 再扫。
这背后的逻辑很重要:
- 如果明确没识别到公司,就不该无限重试;
- 无限重试会浪费批次资源,还会干扰真实待处理记录;
- 明确写回 not_found,才能让“没命中”和“还没处理”分得开。
同时,源码还处理了另一个更隐蔽的问题:
- 如果 IAP 返回结果缺项,导致某些 IP 没进入
done_ips; - Odoo 会把这类残留 IP 也强制回写为
not_found; - 目的就是避免因为外部回包异常而进入死循环。
这非常老练:它不是假设服务永远完美,而是默认“返回异常时也要兜底终止”。
六、6 个月去重窗口说明 Reveal 不是“看到同一家公司就反复建单”
_unlink_unrelevant_reveal_view() 会先找过去一段时间内已有 reveal_ip 的线索,再把对应 IP 的 reveal view 删掉。
默认窗口是 6 个月,可由参数控制。
这说明 Odoo 的产品定义不是“只要网站活跃,就不断给销售塞重复潜客”,而是:
同一家公司在一个有效期窗口内,通常只值得被捕获一次。
这点非常符合销售现实。
因为很多 B2B 网站访客不是一次性访问,而是会在几周甚至几个月内多次回来。如果每次回访都重新建线索,CRM 很快就会被相同公司刷屏。
七、为什么规则 payload 不只包含规则 id,还包含 user_country、size、roles
_get_rules_payload() 发给服务侧的数据并不只是“哪条规则命中”。
它还会补:
- 公司规模上下限;
- 行业标签 reveal ids;
lead_for是 companies 还是 people;- role / seniority / extra_contacts;
- 当前公司所在国家
user_country。
这说明 Reveal 规则本质上不是路由规则那么简单,它同时也是潜客筛选策略。
也因此,实施里一旦出现“明明命中页面却没有产出 lead”,你不能只查 URL regex,还要查:
- 规则是不是 people 模式;
- 角色/职级过滤是不是过窄;
- 规模范围是不是把目标公司排掉了。
八、最容易误判的地方
1. 以为 Reveal 是实时建单
其实它是前台采样、后台批处理。
2. 以为 page view 数量决定 lead 数量
真正的处理单位更接近 IP,而不是单条浏览记录。
3. 以为额度不足只是个别请求失败
实际上 credit_error 会让整轮处理停下来。
4. 以为 not_found 还会自动无限重试
不会,源码会明确回写状态,避免重复占用批次。
九、排错顺序建议
Reveal 没产出或产出异常时,我建议按这个顺序查:
_get_active_rules()缓存是否因规则变更失效;- URL / website / country / state 是否真命中规则;
crm.reveal.view是否被正确写成to_process;- SQL 聚合后该 IP 是否进入本轮批次;
- 服务返回的是成功、not_found 还是 credit_error;
- 过去 6 个月是否已存在同 IP 线索导致视图被提前清掉。
一句话记忆
Odoo CRM Reveal 的核心不是“识别一个访客”,而是“把潜客识别做成带缓存、带批处理、带预算刹车的后台流水线”。
DISCUSSION
评论区