自从上班实习之后,就好久没有写博客了,这是自毕业后的第一篇博客,希望自己今后能养成写博客的一个好习惯。最近公司为了加速APP推广,采取在外部平台(如:今日头条)进行广告投放的方式,进行用户引流。因此我们需要对广告的激活数据进行一个检测,跟踪广告的转化效果。以下主要列举对接今日头条广告激活数据API的流程以及接口的实现。
-
熟悉流程
我们想看看今日头条对接文档给我们提供的一个对接流程示意图:
由上图可看出我们需要提供两个接口:
接口一:当用户点击我们投放在今日头条的广告时,今日头条服务器会向接口一下发数据,我们需要对该部分数据进行保存操作。
接口二:当用户下载了我们广告中的APP,并且用户成功注册后,APP调用接口二,接口二将对应的数据回调到今日头条平台。 -
接口实现
接口一流程:
接口一的参数形式:
ANDROID:
http://xxx.xxx.com/xxx?adid=__AID__&cid=__CID__&callback=__CALLBACK_PARAM__&imei=__IMEI__&mac=__MAC__&android_id=__ANDROIDID1__×tamp=__TS__&ip=__IP__&os=__OS__
IOS:
http://xxx.xxx.com/xxx?adid=__AID__&cid=__CID__&idfa=__IDFA__&mac=__MAC__×tamp=__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
//TODO 主键、创建时间、更新时间等字段不一一列举了,可根据业务需要进行拓展字段
}
- Controller层实现
//PS:最近有评论让我贴出controller代码,现在我已经很久没接触这个代码了,现在controller代码是现写的,仅供参考,controller只要接收头条返回的参数,业务中再做一些参数校验即可
@RequestMapping("/monitoring")
@ResponseBody
public String saveMonitoringParams(MonitoringParamsDTO monitoringParamsDTO){
//TODO 业务中可以做一些参数校验
// 调用sevice方法保存监控参数(即下面的saveUserDeviceInfo方法)
// 处理成功返回success,否则返回其他
return "success";
}
- 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);
//TODO 此处,您可以再做其他业务逻辑,我在项目重要是累计了用户的点击次数等等
//保存用户设备信息
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;
}
- Controller层实现
//PS:最近有评论让我贴出controller代码,现在我已经很久没接触这个代码了,
//现在controller代码是现写的,仅供参考,controller只要接收我们自己的app客户端传过来的参数,做下参数校验,
//然后再调用service方法即可
@RequestMapping("/callback")
@ResponseBody
public String callback(CallBackActiveParamsDTO callBackActiveParamsDTO ,Sting source){
//TODO 业务中可以做一些参数校验
// 调用sevice方法中的激活回调方法(即下面的callback方法)
// TODO 处理成功返回success,具体返回什么看自己的业务
// 这个接口时给我们自己的app客户端调用的,所以返回参数可以自定
return "success";
}
- Service层的代码实现:
/**
* 激活回调
*
* @param dto 回调激活参数信息
* @param source 激活来源,如:login
*/
public void callback(CallBackActiveParamsDTO dto,String source) {
//获取监测参数获取用户设备信息
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设置,回调参数, Constant.TOU_TIAO_ACTIVATE_URL就是头条接口二的url
// Constant.TOU_TIAO_ACTIVATE_URL = http://ad.toutiao.com/track/activate/?callback=%s&muid=%s&os=%s&source=%s&conv_time=%s
String url = String.format(Constant.TOU_TIAO_ACTIVATE_URL, userDeviceInfo.getCallback(), muid, os,source, 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) {
//TODO 回调成功,此处可以根据自己的业务做处理,此处我主要是保存了回调次数
}
}
}
});
}
/**
* 获取签名的url
*
* @param url 需要签名的url
* @param os 系统类型
* @return
*/
private String getSignatureUrl(String url, int os) {
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今日头条还在开发阶段。用户设备信息的匹配的方法还需要更加完善,有更好的匹配方法也可以相互参考,谢谢。