先说结论
在 Odoo 里,网站访客不是一个“记一条 pageview 就完事”的统计对象。
从 /home/ubuntu/odoo-temp/addons/website/models/website_visitor.py 与 tests/test_website_visitor.py 看,website.visitor 实际承担的是:
- 给匿名或已登录访问者生成稳定身份;
- 记录页面访问轨迹;
- 把短时间连续浏览和跨 8 小时再访问区分开;
- 登录后把匿名轨迹归并到 partner 身份;
- 对长期无价值的匿名访客做定期清理。
所以它更像:
一个网站前台身份聚合层,而不是单纯流量报表表。
这也是为什么它既跟 request、session、partner 有关系,又跟 page tracking、清理 cron 和消息联系能力绑在一起。
一、访客身份为什么不是随机 UUID,而是跟登录状态强绑定
_get_access_token() 是整条链路的起点。
它有两种模式:
1. 已登录用户
如果当前用户不是 public user,access token 直接用:
request.env.user.partner_id.id
这说明登录态访客的目标非常明确:
- 不是再造一层匿名身份;
- 而是直接把网站行为和真实业务联系人对齐。
2. 匿名用户
如果还是 public user,token 会根据:
- IP
- User-Agent
- session id
拼接后做 hash,截成固定长度。
也就是说匿名访客也不是完全一次性的,它在“同一会话 + 同类访问环境”下尽量保持稳定。
这套设计透露出一个很实际的目标:
网站先尽量聚合同一个人,再考虑后续是否能把他认成业务联系人。
二、为什么 visit_count 不是每次点开页面都 +1
源码里 visit_count 的定义很有意思:
- 如果上次连接时间距离现在超过 8 小时,才算一次新 visit;
- 否则只是当前 visit 内的继续活动。
这比“每个 pageview 都算一次访问”更接近运营语义。
因为真实世界里:
- 用户连续看 10 个页面,不代表 10 次访问;
- 早上看一次、晚上再回来一次,才更像两次独立 visit。
对应地,website_track 仍会继续记 page-level 轨迹,二者职责分开:
visit_count看会话级访问次数;website_track_ids看页面级轨迹。
三、为什么 tracked page 与 untracked page 会被区别对待
测试文件里很清楚地构造了:
track = False的 view;track = True的 view。
只有 tracked 页面会进入 _handle_webpage_dispatch(),继而触发:
- 获取或创建 visitor;
- 新增
website.track记录。
这说明 Odoo 的设计并不是“所有页面一律统计”,而是:
哪些页面值得作为行为证据追踪,要由页面层显式决定。
这很合理。
因为有些页面只是技术跳板、空白容器或不重要的中间视图,没有必要每次都计进访客行为证据里。
四、为什么 visitor 里同时存 country、lang、timezone 和 partner 映射
website.visitor 不是只记轨迹,它还记:
country_idlang_idtimezonepartner_idemailmobile
但这些字段背后来源并不一样:
1. 国家与语言
创建 visitor 时会从当前 request 环境里取:
- geoip country code
- request.lang
2. 时区
优先从 cookie tz 里拿;
- 如果没有,再看已登录用户自己的时区。
3. 联系方式
不是直接存一份独立真值,而是从 partner_id 关联读取。
这意味着 Odoo 想要的是:
- 前台行为身份有自己的访问上下文;
- 一旦能和业务联系人对上,就尽量复用正式联系人数据,而不是再维护一套平行资料。
五、为什么登录后会发生访客归并,而不是简单新建一条登录访客
_merge_visitor() 很能体现这套模型的目标。
当匿名访客后来登录后,系统不会简单保留匿名 visitor 再新开一个已登录 visitor,然后任由数据分裂。
它更倾向于:
- 把匿名 visitor 的 tracking 数据迁到目标 partner visitor;
- 再删除匿名 visitor。
官方测试也专门验证了:
- 登录前访问过的 tracked page;
- 登录后继续访问;
- 最后应该仍尽量归在同一个更有业务价值的身份上。
这背后的想法非常清楚:
访客系统要尽量回答“这个人后来是谁”,而不是永远停留在匿名碎片。
这对后续 CRM、营销自动化、客服联系都更有价值。
六、为什么 _upsert_visitor() 要用原生 SQL UPSERT,而不是普通 ORM create/write
这段实现很值得看。
_upsert_visitor() 直接写 SQL:
INSERT ... ON CONFLICT (access_token) DO UPDATE
而不是先 search,再 create 或 write。
原因至少有三个:
1. 并发更稳
高并发访问下,同一 token 的 visitor 很容易被同时命中。
2. 可以把“更新访客 + 记录 track”尽量收敛到一条更紧凑的数据库操作里
源码甚至支持在同一个 SQL 里顺手插入 website_track。
3. visit_count 的 8 小时逻辑更容易在数据库层原子化处理
这类“根据旧值判断是否递增”的逻辑,放 SQL 更不容易出竞态问题。
这不是为了炫技,而是网站访问模型天然就比很多后台业务更高频、更并发。
七、为什么 _add_tracking() 不会每次刷新都无限加新轨迹
源码里还有个 30 分钟边界:
- 如果同一个 visitor 的最近一次匹配页面访问还在 30 分钟内,
_add_tracking()不会无脑再建一条同类 track。
这说明 Odoo 不是要把每次页面刷新都堆成原始日志海洋,而是想在行为可用性与数据膨胀之间找平衡。
对实施来说,这个思路很重要:
- 如果你期待 visitor track 是完美无损的点击流日志,它不是;
- 它更偏“足够支撑网站运营与业务判断”的行为摘要层。
八、为什么旧匿名访客会被 cron 清理,而且默认只清没绑 partner 的人
_cron_unlink_old_visitors() 与 _inactive_visitors_domain() 定义了清理策略:
- 默认保留最近
website.visitor.live.days(默认 60 天)内活跃的访客; - 主要清理的是没绑定 partner 且长期不活跃的匿名 visitor。
这特别说明了 Odoo 对 visitor 数据价值的判断:
- 已经能对上业务联系人的,价值更高,应尽量保留;
- 长期无人认领、也没新活动的匿名轨迹,保留价值有限,反而会膨胀数据库。
所以它没有继续走“archive 一堆无意义历史访客”的路线,而是直接删除。
九、最容易误解的,是把 visitor 当分析工具而不是身份桥梁
误解一:visitor 就是 GA 风格流量记录
不完全是。
它更强调“这个访问者后面是否能和 Odoo 业务对象接起来”。
误解二:登录后新建一条更干净
这会让匿名行为和真实业务联系人割裂。
误解三:轨迹越细越好
未必。网站业务系统关注的是“够不够支撑运营动作”,不是一定要成为全量日志仓。
误解四:匿名访客都应该永久保留
长期看往往只会把噪音和存储成本一起堆高。
十、实战里怎么利用这套 visitor 机制
1. 把 visitor 看成前台身份缓冲层
它是匿名世界和业务联系人世界之间的桥。
2. 认真区分哪些页面值得 track
不要默认所有页面都追。
3. 如果要做 CRM 或营销联动,优先利用登录后归并能力
别自己再造一套“匿名线索池”。
4. 清理策略不要随便关掉
匿名 visitor 的价值会快速衰减。
5. 对 page-level 统计要理解其“摘要化”属性
它不是原始日志系统。
结语
website.visitor 真正有意思的地方,在于它把网站前台访问行为做成了一条可逐步升格的身份链路:
- 先是匿名 token;
- 再是带轨迹的访客;
- 后来可能和 partner 对齐;
- 最终成为 CRM、客服或营销动作可利用的证据。
所以它不是一条轻量统计记录,而是:
Odoo 网站把“这个浏览器访问过什么”慢慢转成“这个业务联系人做过什么”的过渡层。
DISCUSSION
评论区