UTM 归因处理逻辑

本文档整理系统内 utm_xxx 字段的采集、存储、多触点覆盖与下单写入全链路。
核心实现:common/services/UtmService.php;入口中间件:common/middlewares/UtmSource.php
与实现冲突时以代码为准。


一、总览

flowchart TD
    A[HTTP 请求] --> B[UtmSource 中间件]
    B --> C[UtmService::utmHandler]
    C --> D[getCurrentUtm 解析当前触点]
    C --> E[getHistoryUtm 读取历史链]
    D --> F[addUtmToUtmHistory 追加/过滤]
    F --> G[utmSave 写 Cookie 当前 UTM]
    F --> H[historyUtmSave 写历史链]
    B --> I[写入 Context utmSource/Medium/...]

    J[下单] --> K[getUtmFromHistoryByWeights 算主归因]
    K --> L[写 o_order / o_cod_order 主表 utm_xxx]
    J --> M[遍历 history 写 o_order_utm / o_cod_order_utm]
层级作用存储
当前 UTM本次访问解析出的触点Cookie:utm_source ~ utm_content
历史链(history)用户访问路径上的多触点序列(最多 20 条)游客 Cookie order_utm_history;会员 Redis
订单主归因下单时写入订单主表的单条 UTMo_order.utm_* / o_cod_order.utm_*
订单归因明细完整触点链,带 first/last/normalo_order_utm_* / o_cod_order_utm_*

二、入口与触发时机

2.1 中间件注册

模块文件说明
homeapp/home/middleware.php前台页面请求
homeapiapp/homeapi/middleware.php前台 API

中间件类:common/middlewares/UtmSource.php

每次请求执行:

  1. handlerUtm()UtmService::utmHandler(),并将结果写入 app('Context')
    • utmSourceutmMediumutmTermutmCampaignutmContent
  2. 附带处理:landing_pagekeep_alivefirst_http_refererfirst_visit_timefbcode 等(与归因辅助相关,见 § 九)。

Collect 埋点控制器会单独调用 (new UtmSource())->handlerUtm()app/home/controller/Collect.php)。

2.2 核心编排:utmHandler

// UtmService::utmHandler 简化流程
$utm = getCurrentUtm($request);          // 解析本次触点
$historyUtm = getHistoryUtm();           // 读历史
$historyUtm = addUtmToUtmHistory($utm, $historyUtm);  // 追加/过滤
utmSave($utm);                           // 写当前 Cookie
historyUtmSave($historyUtm);             // 写历史链
return $utm;

三、UTM 数据结构

UtmService::generateUtm() 统一生成:

字段说明
utm_source来源(平台名、域名、direct 等)
utm_medium媒介(network / referral / default / 自定义)
utm_campaign活动
utm_content内容
utm_term关键词
source_device当前 Context->sourceDevice
create_time创建时间戳
expire_timecreate_time + attribution_mode_cookie 天数

utm_source 为空时返回 [],表示无有效 UTM。

3.1 常量

常量含义
UTM_SAVE_DAY30默认归因 Cookie 天数
UTM_PLATFORM_SOCIALsocial社交平台 medium
UTM_PLATFORM_SEARCHcpc搜索平台 medium
UTM_PLATFORM_REFERRALreferral外链引荐
UTM_PLATFORM_NETWORKnetwork广告/网络投放(URL 无 medium 时的默认)
UTM_MEDIUM_DEFAULTdefault直接访问等
UTM_SOURCE_EMAILevent_newsletter邮件营销来源

四、当前 UTM 解析:getCurrentUtm

5 个渠道 分别解析,再按优先级合并:

渠道方法数据来源
CookiegetUtmFromCookie()utm_source ~ utm_content Cookie
URL 参数getUtmFromUrlParams()?utm_source=...
广告平台getUtmFromPlatform()Referer host、click_id 参数
外链引荐getUtmFromOther()非本站、非已知平台的 Referer host
App 内嵌getUtmFromApp()UA 为 inapp_* 的内嵌浏览器

4.1 合并优先级(多源同时存在)

仅单一渠道有值:直接返回该渠道。

全部为空:生成 direct / default(仍带上 URL 里可能存在的 campaign/content/term)。

多源同时有值时的覆盖顺序(后者优先于前者,越靠后越「赢」):

URL 参数 > Platform(含例外) > Other > Cookie > App

具体代码顺序:

  1. $urlParamsUtm 非空 → 直接返回 URL 参数(最高优先级)
  2. $platformUtm 非空:
    • 例外:当 platform 的 utm_medium == networkutm_source 与 Cookie 中相同 → 保留 Cookie(避免支付回跳等同源 network 覆盖已有归因)
    • 否则 → 返回 platform
  3. $otherUtm 非空 → 返回 other
  4. $cookieUtm 非空 → 返回 cookie
  5. $appUtm 非空 → 返回 app

4.2 URL 参数:getUtmFromUrlParams

  • 必须有 utm_source 才生效
  • 未传 utm_medium 时默认 network
  • utmDefineUtmSkipTagFilter 过滤支付域名等
  • 特殊utm_source=app_detailcouponutm_content 为空时,从 utm_campaign 解析 coupon_id 查券码填入 utm_content

4.3 广告平台:getUtmFromPlatform

按 Referer host 或 URL query 中的 click_id 识别平台,分三组映射表:

社交平台 getSocialPlatform()(节选):

标记参数utm_source
fbclidfacebook
ttclidtiktok
epikpinterest
ScCidsnapchat
click_id + adSETID(须同时存在)kwai
bbg + pixel_id(须同时存在)Bigo

搜索引擎 getSearchEnginePlatform()(节选):

标记参数utm_source
gclid / wbraid / gbraidgoogle
msclkidbing
bd_vidbaidu
nb_cidnewsbreak

AI 平台 getAiPlatform():Referer host 匹配 gemini、claude、copilot、perplexity 等。

识别到平台后,utm_medium 优先取 URL/Referer 上的值,否则默认 network;campaign/content/term 同样优先 URL 再 Referer。

4.4 外链引荐:getUtmFromOther

  • Referer host 存在,且 不属于 已知搜索/社交平台、不属于 当前请求 host
  • 排除 店铺域名:shop_nameshop_domainmain_domain
  • 排除 路径含 checkouts 且无 utm_source 的场景(支付回跳)
  • 生成:utm_source = referer hostutm_medium = referral

4.5 App 内嵌:getUtmFromApp

  • UA 经 getBrowser() 判断,须以 inapp_ 开头(如 inapp_facebook
  • utm_source = inapp_ 后的平台名,utm_medium 默认 network

五、历史链记录逻辑

5.1 读取:getHistoryUtm

用户状态存储位置说明
游客Cookie order_utm_historyJSON 数组
已登录Redis {storeId}:utmHistory:{customerId}CacheKeyHelper::orderUtmHistoryKey

登录合并:若 Redis 有历史且 Cookie 也有:

  1. recursionDel 去掉 Cookie 头部与 Redis 末条重复项(或 medium 为 default 的项)
  2. array_merge(redis, cookie) 后按 create_time 排序
  3. 删除 Cookie order_utm_history

过期清理unsetBeOverdueUtm 移除 expire_time < now 的条目。

5.2 追加:addUtmToUtmHistory

在写入历史前依次处理:

  1. utmSkipTagFilter:店铺配置 utm_skip_tag(JSON 数组,格式 source/medium)命中 → 改写为 direct/default(仍保留 campaign 等)
  2. isdDisturb() 为 true → 不追加,原样返回历史
  3. 邮件归因 utm_source == event_newsletter 且历史非空 → 返回 []historyUtmSave 因空数组直接 return,不覆盖已有历史
  4. default medium 保护:新触点 utm_medium == default 且历史非空时:
    • 若历史中不存在 default,或存在多种 medium → 不追加(返回 []
    • 仅当历史全是 default 时才允许继续追加 default
  5. 末条去重:比较当前与历史最后一条的前 5 个字段 MD5,相同则不追加
  6. 追加到数组末尾

5.3 保存:historyUtmSave

  • 相邻完全相同条目去重
  • 最多保留 20 条array_slice(..., 0, 20)
  • 游客 → Cookie,TTL = attribution_mode_cookie
  • 会员 → Redis,TTL 固定 30 天UTM_SAVE_DAY * ONE_DAY_SECONDS,与 Cookie 配置天数独立)

5.4 当前 UTM Cookie:utmSave

分别写入 utm_sourceutm_mediumutm_termutm_campaignutm_content,TTL = attribution_mode_cookie 天。


六、多归因覆盖与下单归因算法

6.1 下单主归因:getUtmFromHistoryByWeights

下单时 订单主表 只写 一条 UTM,由此方法决定:

history = getHistoryUtm()
attribution_mode_type行为
空 或 last_click首次触点current($history)(数组第一条)
last_click走权重算法,见下

last_click 权重流程(utmWeights):

  1. 将 history 按 utm_source 去重,同 source 保留最后一条array_columns
  2. 依次尝试店铺配置的三档权重列表(JSON 数组,元素为 utm_source 字符串):
    • utm_level1 → 命中则 getUtmWeightsSort 取 history 中属于该档的条目(保持时间序),取最后一条
    • 未命中再试 utm_level2utm_level3
  3. 三档均未命中 → end($history)(纯末次点击)

无 history 时:生成 direct/default(注意:fallback 分支代码中引用了未定义变量 $utmHistory,实际 campaign 等为空)。

6.2 标准订单写入

入口OrderService::saveUtm($order_id)

调用链:

  • OrderService::createOrder()saveUtm
  • CheckoutSinglePageService / CheckoutOnePageService 下单成功后

步骤

  1. getUtmFromHistoryByWeights() → 写入 o_order 主表 utm_source ~ utm_content
  2. getHistoryUtm();若空则补一条 direct/default
  3. 先删 该订单已有 o_order_utm 行,再逐条 insert 完整 history
  4. 每条明细字段:
    • 复制 history 中各 utm 字段 + source_device
    • visit_id = 当前 Context->visit_id
    • created_at / updated_at = history 项的 create_time
    • type
      • 下标 0first
      • 最后一条 → last
      • 中间 → normal

6.3 COD 订单写入

主表(下单创建时):

  • CodOrderService::orderInitUtm() → 同样调用 getUtmFromHistoryByWeights()
  • 合并进 CodOrderModel insert 数据

明细表(事务内):

  • CodOrderHandlerService::saveOrder()CodOrderUtmService::saveOrderUtm()
  • 逻辑与标准订单类似,写入 o_cod_order_utm,type 规则相同

七、过滤与「不记录」规则

7.1 isdDisturb() — 干扰请求跳过采集

以下请求 不解析 URL/Platform/Other不追加 history不 utmSave Cookie

条件说明
homeapi 且 path 不含 order大部分 API 不采集
homeisAjax()AJAX 不采集
isOptions()预检请求

homeapi 中 path 含 order 的请求仍会采集(如 COD 下单 API)。

7.2 店铺级 skip:utm_skip_tag

配置在 o_store_config,key = utm_skip_tag,JSON 数组,如 ["spam/source"]

命中 utm_source/utm_medium改写为 direct/default(campaign 等保留),仍会进入 history 逻辑。

7.3 系统级 skip:utmDefineUtmSkipTagFilter

支付跳转域名列表(PaymentHostService::getPaymentHostList(),含 paypal、airwallex 等):

  • 命中 utm_source/utm_medium 含这些子串 → 整条 UTM 置空(不记录)

用于支付回跳 Referer 污染。

7.4 邮件 event_newsletterutm_mode=skip_if_exists

营销邮件链接常带:

utm_source=event_newsletter&utm_medium={活动码}&utm_mode=skip_if_exists

业务规则(addUtmToUtmHistory):

  • 历史为空:记录邮件归因(首次触点)
  • 历史非空:不追加、不覆盖(等价 skip_if_exists 语义)

Middleware 还对 event_newsletterapp_webpushapp_sms老客户模拟:首次访问时间 Cookie 回拨,用于营销受众判定(UtmSource::handlerFirstVisitTime)。


八、店铺配置项

存储于 o_store_configStoreConfigService 默认值见下):

key默认值作用
attribution_mode_typelast_click下单主归因:末次 vs 首次
attribution_mode_cookie30Cookie 当前 UTM / 游客 history / 部分辅助 Cookie 的天数
utm_level1(空 JSON)末次归因权重第一档 utm_source 列表
utm_level2(空 JSON)第二档
utm_level3(空 JSON)第三档
utm_skip_tag(空 JSON)需改写为 direct 的 source/medium 列表

九、关联模块(读取 UTM 但非主写入链)

模块用法
CartService / CartHandlerServicegetCurrentUtm 填入 BeginCheckout 分析事件
CustomerService::getUtm注册/登录时快照当前 UTM 到顾客模型字段
AbPlanService::checkParamsAB 落地页 / 装修视角受众:按 utm_type 2/3 匹配 source 或 source/medium
ThemeViewService装修视角条件显隐,复用 AbPlanService UTM 校验
TagPixelUvFixed独立读 platform/url UTM,做 UV 固定像素补报(不经 utmHandler)
Request::getVisitIdFromSocial从社交平台 click_id 派生 visit_id
UrlService拼接链接时引用 social/search 平台映射
FirewallService各平台默认防火墙规则命名 defaultUtmSource*

十、数据表

10.1 订单主表(单条主归因)

字段
o_order{tbl_hash}utm_source, utm_medium, utm_term, utm_campaign, utm_content
o_cod_order{tbl_hash}同上

10.2 订单归因明细(完整链)

关键字段
o_order_utm{tbl_hash}同上 + source_device, visit_id, type(first/last/normal), created_at
o_cod_order_utm{tbl_hash}同上

OrderUtmModel 常量:FIRST_KEY=0FIRST='first'LAST='last'NORMAL='normal'

10.3 运行时缓存

说明TTL
Cookie utm_*当前 UTM 五字段attribution_mode_cookie
Cookie order_utm_history游客 history JSON同上
Redis {storeId}:utmHistory:{customerId}会员 history JSON30 天

十一、时序示例

11.1 典型广告 → 下单

1. 用户点击 Facebook 广告 → URL 带 fbclid
2. 首次 full page GET → utmHandler
   - getCurrentUtm: platform → facebook/network
   - history: [facebook/...]
   - Cookie + Context 更新
3. 用户浏览若干 AJAX 请求 → isdDisturb,不追加 history
4. 用户从 Google 广告再进入 → URL utm_source 优先
   - history: [facebook/..., google/...]
5. 下单 → getUtmFromHistoryByWeights
   - last_click + 无 level 配置 → 主归因 google(末条)
   - o_order_utm 两条:first=facebook, last=google

11.2 支付回跳

1. 用户已有 facebook Cookie 归因
2. 从 PayPal 跳回,Referer 为 paypal.com
   - getDefineUtmSkipTagFilter 清空 platform/other 解析
   - network 同 source 例外可能保留 Cookie
3. history 不被 PayPal 污染

11.3 游客转会员

1. 游客 Cookie 中已有 history A、B
2. 登录 → getHistoryUtm 合并 Redis 旧 history X、Y
   - recursionDel 去重 Cookie 头与 Redis 尾
   - merge + sort by create_time
3. 后续 historyUtmSave 只写 Redis

十二、运维与排查

现象排查点
下单主归因不符合预期attribution_mode_type;history 条数与顺序;utm_level1~3
history 缺触点是否 AJAX/homeapi 非 order 请求(isdDisturb);是否被 skip 过滤
邮件归因覆盖广告设计如此:有 history 时 event_newsletter 不追加
会员 history 丢失Redis key {storeId}:utmHistory:{customerId} TTL 30 天
主表与明细不一致主表走权重算法;明细是完整 history,first/last 仅标记位置
COD 与标准订单表不同但 UtmService 逻辑共用

Consolephp think order_utmapp/command/OrderUtm.php)可批量修正历史订单 o_order_utm.type 字段(first/last/normal)。


十三、关键代码索引

文件职责
common/services/UtmService.php采集、合并、history、权重、存储
common/middlewares/UtmSource.php请求入口、Context、辅助 Cookie
common/services/OrderService.phpsaveUtm 标准订单
common/services/CodOrderService.phpCOD 主表 utm
common/services/CodOrderUtmService.phpCOD 明细 utm
common/services/OrderUtmService.php明细 CRUD
common/models/OrderUtmModel.php明细表 Model + type 常量
extend/helper/CacheKeyHelper.phporderUtmHistoryKey

维护:UTM 映射表(social/search/AI)、权重算法或下单写入变更时,请同步更新本文档。