对开发者的积分反馈接口

来自有米广告开发者WIKI
跳转至: 导航搜索

有米 iOS 积分墙积分订单服务器回调协议

说明

  • 此文档只适合 iOS 平台,Android 平台开发者请看这里:有米Android积分墙积分订单服务器回调协议
  • 接口统一使用的编码为:UTF-8
  • 适合于开发者使用自己的服务器来托管积分的情况。
  • 当我们收到广告主反馈的积分时,我们会实时反馈给开发者服务器。
  • 需要开发者提供一个接口来接收数据(web 端提供设置页面),接口接收数据的方式:GET。


流程

  1. 开发者在有米的开发者后台设置自己应用的积分反馈地址,例如:https://www.youmi.net/apps/setting
  2. 当有米的服务器接收到一个有效的效果记录时,就会按开发者提供的反馈地址,加上相关的参数(具体参数见下面的表格),一起反馈给开发者的服务器(GET的方式)。


参数

参数全称 参数名 类型 说明
Feedback Param _fb 字符串 该值是在请求过程中传入的预留参数fb,在此处回调。注意:若请求过程中没有传如fb参数,或fb参数为空,则回调时将不会拼上`_fb`参数,目前SDK不支持`_fb`参数传入
Order ID order 字符串 订单ID:该值是唯一的,如果开发者接收到相同的订单号,那说明该订单已经存在。
App ID app 字符串 开发者应用ID
Ad Name ad 广告名 广告名,如果是应用类型的广告则是应用名
Ad Identify adid 广告编号(整形) 广告的编号ID
User ID user 字符串 用户ID:开发者可以设置自己的用户ID,或者将该字段作为回调预留字段使用
Device ID device 字符串 设备ID:iOS是MAC地址,iOS 7 没有MAC地址则传Advertising Identifier (IDFA) "https://developer.apple.com/library/ios/documentation/AdSupport/Reference/ASIdentifierManager_Ref/ASIdentifierManager.html#//apple_ref/occ/instp/ASIdentifierManager/advertisingIdentifier“
Channel chn 整型 渠道号,对于IOS来说该值为0
Price price 浮点型 开发者获得的收入
Points points 整型 用户可以赚取的积分
Order Create Time time 整型 有米创建订单的时间
Store Id storeid 整型 应用商店的 Id,该值在某些任务类型可能为空
Sig sig 字符串 保留字段(无意义)
Sign sign 字符串 签名

注意:

  • 本协议的所有参数均采用 UTF-8。
  • 部分字段可能包含有特殊字符,在接收到请求时请自行使用 urldecode。
  • $dev_server_secret:开发者服务器端密钥,由有米单独提供给开发者。请登录后进入“https://www.youmi.net/apps/setting” 进行服务器地址设置,设置成功后就会分配一个密钥给您的应用,切记,服务器秘钥,并非应用秘钥,请做好区分。

Wall Settings.png


签名算法

将【参数】列表中除“sign”外的所有参数作为 key 求 MD5 值。
假设参与参数签名计算的请求参数分别是“k1”、“k2”、“k3”,它们的值分别是“v1”、“v2”、“v3”,则参数签名计算方法如下:

  • 将请求的非binary类型的参数格式化为“key=value”格式,即“k1=v1”、“k2=v2”、“k3=v3”;
  • 将格式化好的参数键值对以字典序升序排列后,拼接在一起,即“k1=v1k2=v2k3=v3”;
  • 在拼接好的字符串末尾追加上 $dev_server_secret;
  • 上述字符串的 MD5 值即为签名的值。

注意:

  • 计算签名时的请求参数中不要包含 sign(签名)参数,因为 sign 参数的值此时还不知道,有待计算。
  • 签名过程中的各个参数并未经过 urlencode 处理
  • 为保证回调参数变更时,签名不会出现异常,请确保务必以我们提供的签名函数作为验证手段(或以相同算法实现),以避免因为参数变更而导致签名验证失效,影响数据的获取。

示例:

原URL:http://api.youmi.com/callback/youmiios?order=YM140927--uPMAL-c7&app=9076333dcfc7f490&ad=去哪儿攻略&adid=4188&user=1067748&chn=0&points=979&price=1.96&time=1411751092&device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153&storeid=555610791&sig=8ef41e70

dev_server_secret: 21bd64dc2eaf91f7


经计算后

sign=095551d3f009c654baf3fda7dd0df764
最终URL:http://api.youmi.com/callback/youmiios?order=YM140927--uPMAL-c7&app=9076333dcfc7f490&ad=去哪儿攻略&adid=4188&user=1067748&chn=0&points=979&price=1.96&time=1411751092&device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153&storeid=555610791&sig=8ef41e70&sign=095551d3f009c654baf3fda7dd0df764

PHP实现:

 <?php
 $url = 'http://api.youmi.com/callback/youmiios?order=YM140927--uPMAL-c7&app=9076333dcfc7f490&ad=去哪儿攻略&adid=4188&user=1067748&chn=0&points=979&price=1.96&time=1411751092&device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153&storeid=555610791&sig=8ef41e70';
 $dev_server_secret = '1234567890';
 $url .= '&sign=' . getUrlSignature($url, $dev_server_secret);
 echo $url, "\n"; 
 
 function getUrlSignature($url, $secret){
   $params = array();
   $url_parse = parse_url($url);
   if (isset($url_parse['query'])){
     $query_arr = explode('&', $url_parse['query']);
     if (!empty($query_arr)){
       foreach($query_arr as $p){
         if (strpos($p, '=') !== false){
           list($k, $v) = explode('=', $p);
           $params[$k] = urldecode($v);
         }
       }
     }
   }
   return getSignature($params, $secret);
 }
 
 function getSignature($params, $secret){
   $str = '';
   ksort($params);
   foreach ($params as $k => $v) {
     $str .= "{$k}={$v}";
   }
   $str .= $secret;
   return md5($str);
 }


java实现:

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.TreeMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.security.MessageDigest; 
import java.security.GeneralSecurityException; 
 
public class YoumiSign {
    /**
     * 签名生成算法
     *
     * @param HashMap<String,String> params 请求参数集,所有参数必须已转换为字符串类型
     * @param String                 dev_server_secret 开发者在有米后台设置的密钥
     * @return String
     * @throws IOException
     */
    public static String getSignature(HashMap<String, String> params, String dev_server_secret) throws IOException {
        // 先将参数以其参数名的字典序升序进行排序
        Map<String, String> sortedParams = new TreeMap<String, String>(params);
 
        Set<Map.Entry<String, String>> entrys = sortedParams.entrySet();
        // 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
        StringBuilder basestring = new StringBuilder();
        for (Map.Entry<String, String> param : entrys) {
            basestring.append(param.getKey()).append("=").append(param.getValue());
        }
        basestring.append(dev_server_secret);
        //System.out.println(basestring.toString());
        // 使用MD5对待签名串求签
        byte[] bytes = null;
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            bytes = md5.digest(basestring.toString().getBytes("UTF-8"));
        } catch (GeneralSecurityException ex) {
            throw new IOException(ex);
        }
        // 将MD5输出的二进制结果转换为小写的十六进制
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex);
        }
        return sign.toString();
    }
 
    /**
     * 对一条完整的未签名的URL做签名,并将签名结果添加到URL的末尾
     * 
     * @param String url 未做签名的完整URL
     * @param String dev_server_secret 签名秘钥
     * @return String
     * @throws IOException, MalformedURLException
     */
    public static String getUrlSignature(String url, String dev_server_secret) throws IOException, MalformedURLException {
        try {
            URL urlObj = new URL(url);
            String query = urlObj.getQuery();
            String[] params = query.split("&");
            Map<String, String> map = new HashMap<String, String>();
            for (String each : params) {
                String name = each.split("=")[0];
                String value;
                try {
                    value = URLDecoder.decode(each.split("=")[1], "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    value = "";
                }
                map.put(name, value);
            }
            String signature = getSignature((HashMap<String, String>) map, dev_server_secret);
            return url + "&sign=" + signature;
        } catch (MalformedURLException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        }
    }
 
    /**
     * 检查一条完整的包含签名参数的URL,其签名是否正确
     * 
     * @param String url 已经签名的完整URL
     * @param String dev_server_secret 签名秘钥
     * @return boolean
     */
    public static boolean checkUrlSignature(String signedUrl, String dev_server_secret) {
        String urlSign = "";
        try {
            URL urlObj = new URL(signedUrl);
            String query = urlObj.getQuery();
            String[] params = query.split("&");
            Map<String, String> map = new HashMap<String, String>();
            for (String each : params) {
                String name = each.split("=")[0];
                String value;
                try {
                    value = URLDecoder.decode(each.split("=")[1], "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    value = "";
                }
                if ("sign".equals(name)) {
                    urlSign = value;
                } else {
                    map.put(name, value);
                }
            }
            if ("".equals(urlSign)) {
                return false;
            } else {
                String signature = getSignature((HashMap<String, String>) map, dev_server_secret);
                return urlSign.equals(signature);
            }
        } catch (MalformedURLException e) {
            return false;
        } catch (IOException e) {
            return false;
        }
    }
}


Objective-C实现

//
//  AppDelegate.m
//  sign
//
//  Created by Enzo Yang on 11/7/14.
//  Copyright (c) 2014 YouMi. All rights reserved.
//
 
#import "AppDelegate.h"
#import <CommonCrypto/CommonHMAC.h>
 
 
@interface URLSigner : NSObject
 
// 根据查询参数得到URL签名
- (NSString *)generateSignWithDict:(NSDictionary *)params serverSecret:(NSString *)serverSecret;
 
@end
 
@implementation URLSigner
 
- (NSString *)generateSignWithDict:(NSDictionary *)params serverSecret:(NSString *)serverSecret {
    NSArray *keys = [params allKeys];
 
    // key 按字母顺序排序
    NSArray *sortedKeys = [keys sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        return [obj1 compare:obj2 options:NSLiteralSearch];
    }];
 
    // 把查询字符串按顺序按 k1=v1k2=v2 的格式拼起来
    NSMutableString *str = [[NSMutableString alloc]init];
    for (NSString *key in sortedKeys) {
        [str appendString:[NSString stringWithFormat:@"%@=%@", key, [params objectForKey:key]]];
    }
 
    // 拼上密钥
    [str appendString:serverSecret];
 
    // MD5 后得到签名
    return [self md5HexDigest:str];
}
 
- (NSString*)md5HexDigest:(NSString*)input {
    const char* str = [input UTF8String];
    unsigned char result[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (uint32_t)(strlen(str)), result);
 
    // 32位小写的MD5
    NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH*2];
    for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [ret appendFormat:@"%02x",result[i]];
    }
 
    return ret;
}
 
 
@end


签名参数的校验

说明:这里的校验是指开发者在嵌入广告的初期,开发者如果发现校验不通过,才需要使用该接口进行校验,当然也可以直接咨询我们的客服。正常校验逻辑由开发者自己实现。

假设开发者最终收到我们的反馈请求为: http://api.youmi.com/callback/youmiios?order=YM140927--uPMAL-c7&app=9076333dcfc7f490&ad=去哪儿攻略&adid=4188&user=1067748&chn=0&points=979&price=1.96&time=1411751092&device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153&storeid=555610791&sig=8ef41e70&sign=095551d3f009c654baf3fda7dd0df764

这时如果计算得到的签名和接受到的不同,这时可以请求这样的一个链接来测试: http://ios.wall.youmi.net/v2/check_fb_sig?order=YM140927--uPMAL-c7&app=9076333dcfc7f490&ad=去哪儿攻略&adid=4188&user=1067748&chn=0&points=979&price=1.96&time=1411751092&device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153&storeid=555610791&sig=8ef41e70&sign=095551d3f009c654baf3fda7dd0df764

就是把 http://api.kaifazhe.com/youmi.php 替换成 http://ios.wall.youmi.net/v2/check_fb_sig 即可。
该测试链接会输出相应的结果。


md5原文顺序为:ad=去哪儿攻略adid=4188app=9076333dcfc7f490chn=0device=0AD80C3C-D320-AC2B-5FD3-994E2FA7A153order=YM140927--uPMAL-c7points=979price=1.96sig=8ef41e70storeid=555610791time=1411751092user=1067748**your_secret**(如果有新增参数,会继续增加到签名里面,所以请签名函数不要写死这几个参数。)

返回值

  1. 有米会根据开发者服务器返回的 http 状态码(http_code)来判断该进行什么样的操作,正常情况收到的 http 状态码应该是200或者403。
  2. 如果是200,表示开发者正常接收到信息并且正常处理了。
  3. 如果是403,表示开发者拒绝了该次请求,此时中间层服务器不会再重复请求开发者服务器。
  4. 如果超时、或收到的不是2xx、301、302、303、307、400、403,有米的服务器就会放在下一次循环请求开发者服务器。
  5. 下一次循环请求开发者服务器会有一定的延迟,延迟分别为:5s, 10s, 60s, 300s, 600s, 3600s(距离上一次发送)。即最差环境下,有米最多将发送7次请求,若7次请求全部失败,则该链接将被丢弃。
  6. 因为网络等问题,开发者服务器可能会接收到订单号完全相同的多条记录,这时开发者服务器需要把重复的丢失,并以 http 状态码(http_code)403输出。也就是说:开发者服务器需要对接收到的信息进行去重