今日頭條移動APP 廣告啟用資料API對接實踐

今日頭條移動APP 廣告啟用資料API對接實踐

自從上班實習之後,就好久沒有寫部落格了,這是自畢業後的第一篇部落格,希望自己今後能養成寫部落格的一個好習慣。最近公司為了加速APP推廣,採取在外部平臺(如:今日頭條)進行廣告投放的方式,進行使用者引流。因此我們需要對廣告的啟用資料進行一個檢測,跟蹤廣告的轉化效果。以下主要列舉對接今日頭條廣告啟用資料API的流程以及介面的實現。

  1. 熟悉流程
    我們想看看今日頭條對接文件給我們提供的一個對接流程示意圖:
    這裡寫圖片描述
    由上圖可看出我們需要提供兩個介面
    介面一:當使用者點選我們投放在今日頭條的廣告時,今日頭條伺服器會向介面一下發資料,我們需要對該部分資料進行儲存操作。
    介面二:當使用者下載了我們廣告中的APP,並且使用者成功註冊後,APP呼叫介面二,介面二將對應的資料回撥到今日頭條平臺。

  2. 介面實現
    介面一流程
    這裡寫圖片描述

介面一的引數形式
ANDROID:

http://xxx.xxx.com/xxx?adid=__AID__&cid=__CID__&callback=__CALLBACK_PARAM__&imei=__IMEI__&mac=__MAC__&android_id=__ANDROIDID1__&timestamp=__TS__&ip=__IP__&os=__OS__

IOS:

http://xxx.xxx.com/xxx?adid=__AID__&cid=__CID__&idfa=__IDFA__&mac=__MAC__&timestamp=__TS__&ip=__IP__&os=__OS__&callback=__CALLBACK_PARAM__

介面一的響應方式
JSON格式

介面一響應內容
– 狀態碼200
– {status:0}
– success(在專案當中我採用的是返回success)

介面一的程式碼實現

  • 介面一的引數接收DTO
/**
* 介面一和介面二的引數DTO基類
*/
public class BaseParamsDTO{
private Integer os;             //客戶端型別,0-Android,1-IOS,2-WP,3-Others
private String idfa;            //IOS唯一標識(IOS9和IOS10當開啟了限制廣告跟蹤時,該值不能作為唯一標識)
private String imei;            //安卓唯一標識(APP需要授權才能獲取到)
private String androidId;       //安卓唯一標識(恢復出廠設定會改變)
private String ip;              
}
/**
* 監測介面引數DTO
*/
public class MonitoringParamsDTO extends BaseParamsDTO{
private String adid;            //廣告計劃id,原值
private String cid;             //廣告創意id,原值
private String mac;             //eth0網絡卡mac客戶
private String timestamp;       //時間戳
private String convertId;       //轉化跟蹤id
private String callback;        //回撥引數
}
  • 介面一儲存的實體資訊(以下只列舉主要的欄位,根據自身的業務要求進行欄位拓展)
public class UserDeviceInfo{
private String adid;            // 廣告計劃id
private String cid;             // 廣告創意id
private Integer os;             // 客戶端型別,0-Android,1-IOS,2-WP,3-Others
private String idfa;            // ios唯一標識
private String imei;            // 安卓唯一標識
private String androidid;       // 安卓唯一標識
private String mac;
private String ip;   
private String callback;        //回撥引數
private String timestamp;       //時間戳
private String convertId;       //轉化跟蹤id
//主鍵、建立時間、更新時間等欄位不一一列舉了,可根據業務需要進行拓展欄位
}
  • Service層的程式碼實現:
/**
* 儲存使用者的裝置資訊
*
* @param monitoringDto 頭條下發的監測引數
*/
public void saveUserDeviceInfo(MonitoringParamsDto monitoringDto) {
//獲取監測引數獲取使用者裝置資訊
UserDeviceInfo userDeviceInfo = userDeviceInfoService.getUserDeviceInfoByParams(monitoringDto);
//不存在使用者裝置資訊,則新建實體
if (ToolsKit.isEmpty(userDeviceInfo)) {
userDeviceInfo = new UserDeviceInfo();
}
//將頭條的下發的檢測引數轉化為使用者裝置資訊實體
BeanCopier beanCopier = BeanCopier.create(MonitoringParamsDto.class, UserDeviceInfo.class, false);
beanCopier.copy(monitoringDto, userDeviceInfo, null);
//此處,您可以再做其他業務邏輯,我在專案重要是累計了使用者的點選次數等等 
//儲存使用者裝置資訊
userDeviceInfoService.save(userDeviceInfo);
}
/**
*根據引數獲取裝置資訊實體
*匹配邏輯如下:
*1、IOS系統,idfa合法的情況下,就根據idfa查詢裝置資訊
*2、IOS系統,idfa不合法的情況下,就根據ip和idfa查詢裝置資訊
*3、Android系統,imei存在的情況下,就根據ip和imei查詢裝置資訊
*4、Android系統,imei不存在的情況下,就根據ip和AndroidId查詢裝置資訊
*5、其他情況就根據ip和ua(User-Agent)查詢裝置資訊
* @param baseParamsDto
* @return
*/
public UserDeviceInfo getUserDeviceInfoByParams(BaseParamsDto baseParamsDto) {
if (ToolsKit.isEmpty(baseParamsDto)) {
throw new ServiceException("引數為空!");
}
//查詢是否已經存在裝置資訊記錄
int os = baseParamsDto.getOs();
Map<String, Object> params = Maps.newLinkedHashMap();
params.put(UserDeviceInfo.OS_FIELD, os);
params.put(UserDeviceInfo.IP_FIELD, baseParamsDto.getIp());
UserDeviceInfo entity = null;
String idfa = baseParamsDto.getIdfa();
String imei = baseParamsDto.getImei();
//ios系統且idfa不為空
if (OSTypeEnums.IOS.getValue() == os) {
params.put(UserDeviceInfo.IDFA_FIELD, idfa);
if (TooUtil.checkIdfa(idfa)){ //判斷idfa是否合法
params.put(UserDeviceInfo.IP_FIELD,null);
}
entity = this.findEntityByParams(params);
} else if (OSTypeEnums.ANDROID.getValue() == os) {
//Android系統且imei不為空
if(ToolsKit.isNotEmpty(imei)){
params.put(UserDeviceInfo.IMEI_FIELD, imei);
}else{
params.put(UserDeviceInfo.ANDROID_ID_FIELD, baseParamsDto.getAndroidId());
}
entity = this.findEntityByParams(params);
} else {
//通過ip和ua查詢
params.put(UserDeviceInfo.UA_FIELD, baseParamsDto.getUa());
entity = this.findEntityByParams(params);
}
return entity;
}

介面二流程
這裡寫圖片描述
介面二的程式碼實現

  • 介面二回撥的url
http://ad.toutiao.com/track/activate/?callback={callback_param}&muid=
{muid}&os={os}&source={source}&conv_time={conv_time}&signature={signa
ture}
  • 介面二的引數接收DTO
/**
* 回撥啟用引數DTO
*/
public class CallBackActiveParamsDTO extends BaseParamsDTO{
//根據自己的業務需要定義回撥啟用引數,此處我主要收集使用者的id
private String userid;
}
  • Service層的程式碼實現:
    /**
* 啟用回撥
*
* @param userDeviceInfo 使用者裝置資訊
*/
public void callback(CallBackActiveParamsDTO dto) {
//獲取監測引數獲取使用者裝置資訊
UserDeviceInfo userDeviceInfo = userDeviceInfoService.getUserDeviceInfoByParams(monitoringDto);
//如果為空,則表明無該實體物件
if (ToolsKit.isEmpty(userDeviceInfo)) {
return;
}
int os = userDeviceInfo.getOs();
String muid = null; //安卓取imei進行MD5加密,ios取idfa
if (OSTypeEnums.ANDROID.getValue() == os) {
//muid賦值
String imei = userDeviceInfo.getImei();
muid = DuangKit.Secure.md5(imei);
} else if (OSTypeEnums.IOS.getValue() == os) {
muid = userDeviceInfo.getIdfa();
}
//回撥url設定,回撥引數
String url = String.format(Constant.TOU_TIAO_ACTIVATE_URL, userDeviceInfo.getCallback(), muid, os, userDeviceInfo.getTs());
//對url進行簽名
url = getSignatureUrl(url, os);
call2TouTiao(url, userDeviceInfo);
}
/**
* 啟動子執行緒回撥今日頭條介面
*
* @param url            回撥地址
* @param userDeviceInfo 使用者裝置資訊
*/
private void call2TouTiao(final String url, final UserDeviceInfo userDeviceInfo) {
ThreadPoolKit.execute(new Thread() {
public void run() {
//發起回撥,此處我使用的是公司內部對HttpClient封裝的工具類發起回撥
HttpRes httpRes = HttpKit.duang().url(url).get();
String result = httpRes.getResult();
if (ToolsKit.isNotEmpty(result)) {
Map map = ToolsKit.jsonParseObject(result, Map.class);
if (ToolsKit.isNotEmpty(map) && map.get("ret") == 0) {
//回撥成功,此處可以根據自己的業務做處理,此處我主要是儲存了回撥次數
}
}
}
});
}
/**
* 獲取簽名的url
*
* @param url 需要簽名的url
* @param os  系統型別
* @return
*/
private String getSignatureUrl(String url, int os) {
System.out.println(url);
String key = null;
if (os == OSTypeEnums.ANDROID.getValue()) {
key = Constant.TOUTIAO_ANDROID_KEY;
} else if (os == OSTypeEnums.IOS.getValue()) {
key = Constant.TOUTIAO_IOS_KEY;
} else {
throw new ServiceException("無法處理os型別,os = "   os   " : "   OSTypeEnums.getMap().get(os));
}
//使用HMAC-SHA1簽名方法對url進行簽名
String signature = TooUtil.getHmacSHA1(url, key);
//對signature進行base64加密
signature = Base64.encode(signature);
url = url   "&signature="   signature;
return url;
}
  • 涉及的主要工具類方法:
    /**
* HMAC-SHA1簽名
*
* @param message
* @param key
* @return
*/
public static String getHmacSHA1(String message, String key) {
String hmacSha1 = null;
try {
// url encode
message = URLEncoder.encode(message, "UTF-8");
// hmac-sha1加密
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec spec = new SecretKeySpec(key.getBytes(), "HmacSHA1");
mac.init(spec);
byte[] byteHMAC = mac.doFinal(message.getBytes());
// base64 encode
hmacSha1 = new BASE64Encoder().encode(byteHMAC);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
return hmacSha1;
}
private static final String IOS10_INVALID_IDFA = "00000000-0000-0000-0000-000000000000";
private static final String IOS9_INVALID_IDFA = "00000000000000000000000000000000";
/**
* 判斷idfa是否合法
*
* @param idfa
* @return
*/
public static boolean checkIdfa(String idfa) {
if (ToolsKit.isEmpty(idfa) || IOS9_INVALID_IDFA.equals(idfa) || IOS10_INVALID_IDFA.equals(idfa)) {
return false;
}
return true;
}
}

小結
基本都在貼程式碼,少部分涉及主要業務邏輯的程式碼我省略了,但是隻要跟著我這個流程走下來把今日頭條的介面接起來肯定沒問題。其實最重要的是是匹配使用者的裝置資訊,因為現在IOS9和IOS10有可能獲取到沒用的idfa(都是000000這種形式的idfa),Android在獲取imei的時候需要使用者授權。今日頭條那邊的技術人員建議在獲取不到idfa或者imei的情況下使用ip和ua匹配,但是經驗證多次介面一獲取不到ua引數,且到目前為止IOS獲取ua今日頭條還在開發階段。使用者裝置資訊的匹配的方法還需要更加完善,有更好的匹配方法也可以相互參考,謝謝。