app
都回传给渠道商,渠道商自己归因app
, 然后首次安装启动时能从本地存储获取到归因数据100%
。100%
。理由是第一个触点给用户建立了认知,与用户形成了连接。40%
功劳,其余平分剩余的20%
功劳。兼顾最初的线索和最终的决策。OAID
: OAID
全称是Open Anonymous Device Identifier
,中文名是匿名设备标识符。 OAID
是一种非永久性设备标识符,最长64
位,在系统首次启动的时候生成AndroidID
: ANDROID_ID
是设备首次启动时由系统随机生成的一串64位的十六进制数字IMEI
: 国际移动设备识别码(International Mobile Equipment Identity,IMEI
),即通常所说的手机序列号、手机“串号”,用于在移动电话网络中识别每一部独立的手机等移动通信设备,相当于移动电话的身份证。Mac
: 手机的网卡地址IP
: 分配给用户上网使用的网际协议(全称Internet Protocol
, 简称IP
)的设备的数字标签User_Agent
: 一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU
类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等根据上图, 我们先给一个最基础的表结构,大家可以根据具体业务增减字段
# 应用表(apps)
id, appid(客户端使用), name, os, attribute_cycle_days(归因周期), attribute_white_list(JSON 白名单列表)
# 渠道表(channels)
id, name, template_query(此字段预先组装好格式)
# 监测链接表(links)
id, app_id, channel_id, channel_name(自定义渠道名), events(JSON 需要回传的事件, 方便后续动态增加回传事件), exp(有效期)
# 广告点击表:按天分表(click_logs)
id, appid, ad_name, [oaid, imei, android_id, mac, ip, ua](这些匹配方式看各自需要存储), exp, callback, data(JSON冗余字段), attributed_at(归因成功时间',')
# 归因成功日志表(这个表按各自日志需要设计)
# 回调日志表(这个表按各自日志需要设计)
根据时序图, 来说明实际场景(以下为伪代码, 所有数据库查询自行做好缓存处理)
渠道表
加一个template_query
字段的原因oppo商店
的格式是这样ad_id=__ADID__&android_id=__ANDROIDID__&imei_md5=__IMEI__&oaid=__OAID__
头条
的格式是这样ad_name=__AID_NAME__&android_md5=__ANDROIDID__&callback=__CALLBACK_PARAM__&idfa=__IDFA__&imei_md5=__IMEI__&ip=__IP__&mac_md5=__MAC1__&oaid=__OAID__&site=__CSITE__&ua=__UA__
https://api.domain.com/api/v1/links/{id}/click_logs?ad_id=__ADID__&android_id=__ANDROIDID__&imei_md5=__IMEI__&oaid=__OAID__
https://api.domain.com/api/v1/links/{id}/click_logs?ad_id=__ADID__&android_id=__ANDROIDID__&imei_md5=__IMEI__&oaid=__OAID__
接口时https://api.domain.com/api/v1/links/{id}/click_logs?ad_id=123456789&android_id=123456789&imei_md5=123456789&oaid=123456789
// 统一的请求结构
class AdClickRequest {
public $oaid;
public $imei;
public $imei_md5;
public $andoird_md5;
public $ad_name;
public $callback;
// xxx 更多字段
}
const FIELDS = ['oaid', 'imei', 'imei_md5', 'xxx'];
function clickLogs($id)
{
// 1. 根据不同框架, 把数据解析到统一请求上
$req = new AdClickRequest();
// 2. 查询监测链接表
$link = "select * from links where id = {$id}"; // $id
if (is_null($link)) {
return 'FAIL';
}
// 3. 写入点击日志表, 点击量大走队列插入
$logModel = "insert into click_logs(`oaid`, `imei`, `events`, `exp`, 'xxx更多字段') values({$req->oaid}, {$req->imei}, {$link->events}, {$link->exp}, 'xxx更多字段')";
// 4. 写入 redis
$pipe = \Redis::pipeline();
$value = $logModel . '.' . $logModel->id;
foreach (FIELDS as $key) {
$redisKey = sprintf('attributes:%d_%s', $link->app_id, $req->{$key});
$pipe->set($redisKey, $value, $logModel->exp*60*60*24);
}
$pipe->exec();
return 'OK';
}
class AppReportRequest {
public $deviceKey;
public $oaid;
public $imei;
public $mac;
// xxx 更多字段
}
const FIELDS = ['oaid', 'imei', 'imei_md5', 'xxx'];
function appReport($appId)
{
// 0. 如果是 deepLink 拉起, 最好加一个延迟 10s 的队列归因, 防止`app`请求先于渠道商监测链接请求
// 1. 根据不同框架, 把数据解析到统一请求上
$req = new AppReportRequest();
// 2. 查询 app
$app = "select * apps where id={$appId}";
// 3. 查询 redis
$pipe = \Redis::pipeline();
$keys = [];
foreach (FIELDS as $key) {
$redisKey = sprintf('attributes:%d_%s', $app->app_id, $req->{$key});
$keys[] = $redisKey;
$pipe->get($redisKey);
}
// result 为一个数组, 如果匹配到了里面就是日志表的表名和主键
$result = $pipe->exec();
$value = collect($result)->filter()->first();
if (is_null($value)) {
return '归因失败';
}
$logModel = "select * from click_logs{$value->table} where id = {$value->id}";
// 接下来可以用队列事件解耦之后的流程
\Redis::set("attribute_devices:{$appId}_{$req->deviceKey}", $logModel, 60*60*24*7);
// 存储归因成功日志表
// 修改点击日志状态等等
// 删除所有归因的 $keys, 防止重复归因
// 根据 $app->attribute_cycle_days 设置归因周期
return 'SUCCESS';
}
function appCallback($appId, $deviceKey)
{
$logModel = \Redis::get("attribute_devices:{$appId}_{$deviceKey}");
if (is_null($logModel)) {
return 'FAIL';
}
// 通过 $logModel->attributed_at 判断次留是否有效, 判断是否七日内付费
// 处理回传的逻辑
return 'SUCCESS';
}
// 假设这个是客户端的方法, 在需要打点的地方每次都调用这个方法
function eventReport(event) {
// 从本地存储获取数据, 一定要存成 json 格式, 继续反序列化
var data = storage.get('attribute_events');
// 如果已经上报过了, 不要上报
if (data[event]) {
return;
}
// 上报接口
var res = api.post('/api/v1/event_callback', '参数');
if (res.code !== 200 || ! res.data.status) {
return;
}
data[event] = true;
storage.set('attribute_events', data);
}
// 归因上报的接口,
function attributeReport() {
// 请求接口
var res = api.post('/api/v1/attributes', '参数')
if (res.code !== 200 || ! res.data.status) {
return;
}
// 把整个事件缓存删除掉,这样子才能继续上报
storage.delete('attribute_events');
}
oaid, mac, imei
等API
相同的匹配流程查询到日志ID
oaid
), 等等其它信息JSON
存储, 存成{"event1": "status1", "event2": "status2"}
这样