直投2.0介绍更新时间: 2025-04-08 18:49:00

本文档介绍了直投相关内容,您可在阅读文档内容后,了解直投概况。

一、什么是直投

小米手机上非应用商店的分发行为都需介入安装器完成安装激活,需用户多次操作,转化链路较长。而直投恰好能解决这个问题,在媒体侧直接调起小米应用商店官方详情页完成下载安装,将缩短从下载到激活的转化链路,提高转化效率。

直投使用场景:

三方投放、应用内自更新、应用分发等;

二、直投2.0的核心优势

三方渠道分发应用,未经过小米安全检测,用户下载链路长、效率低;直投直接调起商店详情页,缩短下载链路,提升下载效率。

  • 三方渠道分发链路:用户点击下载安装-->小米安全检测,允许三方安装-->用户确认下载途径-->用户验证身份安装--安装完成

  • 直投2.0分发链路:用户点击下载-->应用商店详情页点击安装-->安装完成

使用直投分发链路实例:

(1)示例1 :戏鲸

  • 未使用直投分发链路实例:

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/FddStfzzPFr%24uVm31GQvyw%40%40

密码:Kcy2

  • 使用直投分发链路实例

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/gllHybZZ6GP%24uVm31GQvyw%40%40

密码:nl25

(2)示例2:番茄免费小说

  • 未使用直投分发链路实例

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/54BHEK%24vgb3%24uVm31GQvyw%40%40

密码:i3bF

  • 使用直投分发链路实例

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/dBxEpEr3zfv%24uVm31GQvyw%40%40

密码:c930

(3)示例3:书旗小说

  • 未使用直投分发链路实例

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/5znL219N7Vf%24uVm31GQvyw%40%40

密码:I6DZ

  • 使用直投分发链路实例:

示例视频地址:https://kpan.mioffice.cn/webfolder/ext/%23B5IYYcqWTL%24uVm31GQvyw%40%40

密码:Y8b3

三、直投流程

为了保障安全性,直投2.0针对开放能力做了校验。直投2.0流程如下:

四、直投2.0能力介绍

1、直投2.0基础版

(1)基础版详情页样式:免费提供全屏详情页样式,满足开发者内容展示需求
全屏详情页具有丰富的应用决策信息,支持富媒体素材。

(2)安全性升级:增加双重验证,解决恶意劫持、恶意刷量问题,保障分发安全

  • 双向加密:应用详情页增加风控验证码、调用签名也做了二次校验。

  • 设备绑定:单个生成deeplink链接,A设备使用后,无法在B设备进行二次使用(oaid、imei等标识顺序实别)。
  • 频率限制:单个生成deeplink链接,可对单个用户使用的次数限制。
  • 时效校验:单个生成 deeplink链接,限制生成后的使用周期。

(3)状态回传能力:小米回传下载链路的更多关键数据

广告投放过程中,市场上提供的投放数据大多是曝光以及激活后链路效果,缺少商店内相关数据,无法分析全投放链路的转化效果。直投详情页支持提供商店内实时转化数据,打通完整的数据链路,助力应用投放效果跟踪,优化投放策略。

(4)虚拟分包能力:实现虚拟归因能力,追踪多渠道投放效果

应用在不同渠道分发时,需制作多个物理包追踪投放效果,维护成本较高。直投详情页提供的虚拟分包能力帮助解决该问题,无需集成SDK,相关分包参数信息由合作方定义且仅有广告主可以获取,提高安全性,节约制作成本。

2、直投2.0定制版(付费开放)

(1)样式升级:多样化的详情页,满足不同投放场景的搭配需求
目前直投详情页提供半屏、弹出式、minicard、H5定制详情页等多种展示样式,有更沉浸的应用下载交互、更丰富的应用决策信息、更灵活的应用介绍样式供选择,支持富媒体素材,可根据自身使用场景以及应用类型选择搭配。

(2)H5个性化定制能力:H5个性化配置,提升内容匹配度
在提供头尾安全信息和下载button条件下,支持配置H5个性化内容,展示内容与广告创意匹配度更高,充分突出应用的特点和优势,提高转化效率。

(3)促激活提醒能力:利用系统激活提醒,实现后端转化再提升
用户在安装完成到激活链路流失较多,直投详情页支持在下载安装完成后,主动对用户进行激活提醒。停留在媒体内的用户会收到弹框提醒,媒体外的用户会收到push推送,提升安装后激活转化。

四、直投2.0定制版

1、固定模板详情页

  • 直投详情页可以按媒体定制化需求,提供弹出式、半屏等多种固定模板展示样式,有更沉浸的应用下载交互、更丰富的应用决策信息、更灵活的应用介绍样式供选择,可根据自身使用场景以及应用类型选择搭配。

上传文件

2、H5个性化详情页

  • 除固定模板详情页外,小米应用商店还提供H5个性化详情页,在提供头尾安全信息和下载button条件下,支持配置H5个性化内容,展示内容与广告创意匹配度更高,充分突出应用的特点和优势,提高转化效率。

上传文件

3、促用户激活

  • 用户在安装完成到激活链路流失较多,直投详情页支持在下载安装完成后,主动对用户进行激活提醒。停留在媒体内的用户会收到弹框提醒,媒体外的用户会收到push推送,提升安装后激活转化。
上传文件

五、能力申请

直投2.0基础版:免费开放。
直投2.0定制版:计费开放,可申请免费内测资格。
如有合作意向,请您按以下方式快速申请:

邮件主题:直投2.0能力申请-【媒体名】

邮件发送至:appstore-zhitou@xiaomi.com

邮件内容包含:

公钥证书。请参考《直投2.0接入技术文档》提前生成公钥证书,并打包成zip格式随邮件发送。

【正文信息】

字段 说明
申请背景 介绍申请直投能力的需求背景或用途
使用场景 附上:使用直投的实际业务场景or交互流程示意图
合作形式 广告主/广告媒体/广告平台
*使用直投的媒体和包名 如果有明确使用直投能力的媒体,需要提供所有媒体的应用名称和包名
(eg. 小米浏览器调起商店下载抖音,小米浏览器为使用媒体,抖音为被分发应用
*申请能力 1、全屏详情页样式
2、虚拟归因能力
3、下载链路数据回传
按实际需求,选择申请开通的能力
申请使用时长 填写使用有效期范围,如果为长期使用,请填写“长期”

邮件答复

自申请之日起,1-3个工作日内完成密钥配置并回复邮件。

注意事项

  1. 接入完成后,小米应用商店为您分配的业务编码(appClientId)和密钥请注意妥善保存。
  2. 本文使用的deeplink必须由广告服务端生成然后下发到广告sdk,一定不要在客户端生成。
  3. 一个deeplink只能单台设备使用。

六、常见问题FAQ


Q1:senderPackageName是什么?有没有什么限制?
A1:senderPackageName是媒体APP包名或宿主APP包名,比如从小米浏览器跳转商店详情页,senderPackageName就是小米浏览器的包名。senderPackageName有安全性限制,如果广告平台/媒体有诉求的话,可以将指定的宿主APP包名加入安全授权白名单。


Q2:测试deeplink时是不是直接在小米手机上试就可以?有没有系统版本或者应用商店版本的限制?
A2:小米手机上的应用商店需要4.4.0及以上版本


Q3:为什么deeplink跳转的不是新详情页,而是以下两种详情页之一?

全屏:

半屏:

A3:以上两种详情页是deeplink鉴权失败后会跳转的详情页,出现的原因可能有:

  • 小米手机上的应用商店是4.4.0之前的版本 => 需将商店更新到最新版本,已全量支持各种开放能力
  • deeplink某个或某几个参数的值被修改,如detailStyle,但没有重新生成新的签名 => 修改参数值后需生成新的签名和deeplink
  • deeplink中的senderPackageName与宿主APP包名不一致 => 需将senderPackageName改成宿主APP包名,然后生成新的签名和deeplink
  • deeplink已在其它手机上使用过 => 需生成新的deeplink
  • deeplink已过期 => 需将时间戳更新成当前时间,然后生成新的签名和deeplink
  • deeplink验签失败 => 请确保deeplink是按照生成deeplink一节中的步骤生成的,是的话请联系小米应用商店的同学,并提供跳转失败的deeplink,以便排查问题

Q4:证书、密钥B、appClientId和密钥A有没有过期时间?
A4:都是一次性生成好的,没有过期时间


Q5:如果宿主APP没有广告SDK,sdkVersion该如何填?
A5:可以填宿主APP的版本号


Q6:referrer如何填写?source, medium, campaign, term, content 中哪些是必须参数?
A6:source, medium, campaign, term, content是归因参数的模板规范,根据合作方需求可以选择其中任何一个或几个,非必填


content://com.xiaomi.marked.provider.DirectMailProvider 接口
Q7:此接口是客户端需要调用的接口吗?
A7:是的,调用provider标准接口查询商店是否支持相应功能


Q8:此接口的调用是否会影响性能?
A8:不会,本地测试的查询时间平均5ms左右


Q9:“客户端接入时,客户端调用请在自身的任务栈中发起下载,不要设置intent的 FLAG_ACTIVITY_NEW_TASK”,这个是不是必须的?
A9: 非必须,客户端调用请在自身的任务栈中发起下载,不要设置intent的 FLAG_ACTIVITY_NEW_TASK,目的是这样不会跳转到商店而中断用户行为,提高下载率。


Q10:接入状态回传了,但没收到状态回传结果,只收到了1005(状态回传鉴权失败)code .
A10:这是由于状态回传鉴权失败了。具体原因可能有:

  • 实际调起Deeplink的包名校验失败
  1. )实际调起Deeplink的包名和Deeplink中的senderPackageName不一致
  2. )实际调起Deeplink的包名没有加入直投配置中媒体白名单的授权
  • Deeplink已经被使用过,一个deeplink只能单台设备使用,否则可能会鉴权失败
  • 签名校验失败
  1. )签名生成方法错误,具体可根据示例代码检查签名生成方法是否有误
  2. )签名已过期,每个Deeplink是有效期的,根据直投配置中的链接有效期进行判断
  • 直投配置没有授权状态回传能力


Q11:【状态回传】如果在应用商店下载一半,然后从微博导流过来继续下,会向微博发送后续下载完成,开始安装这些状态的广播吗?
A11:不会发送的,状态回传和最开始发起下载任务的callerPackage有关。会回传给最后一次发起下载的callerPackageName。发起下载是指发起原始下载(未下载→开始下载)→开始下载)

附录

附录1.deeplink完整参数

参数名称参数类型是否必须是否参与加签字段说明
idstring需要推广的目标应用包名
appClientIdstring业务编码,是广告平台/媒体接入直投的唯一标识
noncelong时间戳
signstring签名
senderPackageNamestring应用跳转来源,可用于后续定向回传下载状态
detailStyleint详情页交互形式,1:全屏;2:半屏详情页,3:底部弹出式;5:floatcard悬浮卡片,100(内部测试使用)
topMarginint有值时参与加签半屏详情页(detailStyle =2)场景页面距离顶部距离(px)
alphaint有值时参与加签半屏详情页(detailStyle =2)场景顶部的不透明度,范围0(透明)-255(不透明)
sdkVersionstring有值时参与加签广告sdk版本号,用于后续排查问题
refstring商店打点透传参数,用于标识被点击广告投放的位置,如广告投放在banner或者其他组件,由合作方自己定义
referrerstring有值时参与加签归因参数,标准的归因数据格式,最多500字符,需要进行url编码,如source%3Dxxx%26medium%3Dcpc%26campaign%3Ddoublescore%26term%3Ddatadriven%26content%3Da
startSubscribeboolean有值时参与加签是否自动预约
startDownloadboolean有值时参与加签是否自动下载,默认为true
urlstring有值时参与加签H5页面地址
ext_apkChannelstring渠道号

附录2.deeplink生成示例(java)

package rsa;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;

public class Example {
private static final int KEY_SIZE = 2048;
private static final int GROUP_SIZE = KEY_SIZE / 8;
private static final int ENCRYPT_GROUP_SIZE = GROUP_SIZE - 11;

/**
* 私钥加密
*
* @param data
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return codeImpl(data, cipher, ENCRYPT_GROUP_SIZE);
}
/**
* 公钥解密
*
* @param data
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return codeImpl(data, cipher, GROUP_SIZE);
}

private static byte[] codeImpl(byte[] data, Cipher cipher, int groupSize) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int idx = 0;
while (idx < data.length) {
int remain = data.length - idx;
int segsize = remain > groupSize ? groupSize : remain;
baos.write(cipher.doFinal(data, idx, segsize));
idx += segsize;
}
return baos.toByteArray();
}

/**
* 获取私钥
*
* @param p12Is
* @param password
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKeyByPKCS12(InputStream p12Is, String password) throws Exception {
try {
KeyStore ks = KeyStore.getInstance("PKCS12");
char[] pwd = null;
if ((password == null) || password.trim().equals("")) {
pwd = null;
} else {
pwd = password.toCharArray();
}
ks.load(p12Is, pwd);

Enumeration<String> aliases = ks.aliases();
String keyAlias = null;
if (aliases.hasMoreElements()) {
keyAlias = aliases.nextElement();
}
return (PrivateKey) ks.getKey(keyAlias, pwd);
} finally {
if (p12Is != null) {
try {
p12Is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static PrivateKey getPrivateKeyByPKCS12(String p12File, String password) throws Exception {
return getPrivateKeyByPKCS12(new FileInputStream(p12File), password);
}

/**
* 获取公钥
*
* @param cerfile
* @return
* @throws Exception
*/
public static PublicKey getPublicKeyByX509Cer(String cerfile) throws Exception {
return getPublicKeyByX509Cer(new FileInputStream(cerfile));
}

private static PublicKey getPublicKeyByX509Cer(InputStream x509Is) throws Exception {
try {
CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(x509Is);
System.out.println(JSONObject.toJSONString(cert));
System.out.println(cert.getIssuerDN().getName());
System.out.println(cert.getIssuerX500Principal().getName());
return cert.getPublicKey();
} finally {
if (x509Is != null) {
try {
x509Is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

/**
* sha256_HMAC加密
*
* @param message
* 消息
* @return 加密后字符串
*/
public static String sha256Hmac(String message, String serverkey) {
String hash = "";
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(serverkey.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] bytes = sha256_HMAC.doFinal(message.getBytes());
hash = byteArrayToHexString(bytes);
} catch (Exception e) {
System.out.println("Error HmacSHA256 ===========" + e.getMessage());
}
return hash;
}
/**
* 将加密后的字节数组转换成字符串
*
* @param b
* 字节数组
* @return 字符串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder hs = new StringBuilder();
String stmp;
for (int n = 0; b != null && n < b.length; n++) {
stmp = Integer.toHexString(b[n] & 0XFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toLowerCase();
}

public static void main(String[] args) throws Exception {

// 原始数据(所有按照按照ascii升序排列)
String info = "appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-8162956393368422160:27234970&senderPackageName=com.android.browser";
// 加签
String originalSign = sha256Hmac(info, "${商店分配的密钥A}");
System.out.println("生成的签名:" + originalSign);
// 对签名利用私钥加密保护
// 获取私钥
PrivateKey privateKey = getPrivateKeyByPKCS12("${密钥B对应的p12格式文件路径}", "${密钥B的密码}");
// RSA加密
byte[] result = encryptByPrivateKey(originalSign.getBytes(), privateKey);
// base64编码
String strBase64 = Base64.encodeBase64String(result);
// url编码
String sign = URLEncoder.encode(strBase64, "utf-8");

// 传输 到商店。。。。。。。。。。。。。。。。。。。经过网络传输

// url解码
String enResult = URLDecoder.decode(sign, "utf-8");
// 获取公钥
PublicKey publicKey = getPublicKeyByX509Cer("${密钥B对应的公钥证书的文件路径}");
// 使用公钥解密
byte[] decodedResult = decryptByPublicKey(Base64.decodeBase64(enResult), publicKey);
String decodedSign = new String(decodedResult, "utf-8");
System.out.println("解密后的签名:" + decodedSign);
//判断解密结果与原始签名是否一致
System.out.println(decodedSign.equals(originalSign));
}

}

附录3.常见错误码对照表

编码说明
0成功
11参数非法
12无效的链接
13无效的签名
14无效的设备
15链接使用超频
16缺少关键参数
17媒体+应用 或 媒体 或 应用 在全局封禁名单中
18错误的调起方包名
19业务未接入
101配置已失效
102链接使用超频
103缺少必要参数
104请求次数超限
21样式能力未接入
22业务没有合作详情页样式
23样式信息不匹配
24媒体包名 在媒体黑名单中 或者 不在媒体白名单中
25跳转V2详情页
26实验样式配置为空
27实验样式不匹配
28不支持游戏
29不支持应用
201应用包名 在应用黑名单中 或者 不在应用白名单中
31自动下载能力未接入
32媒体包名 在媒体黑名单中 或者 不在媒体白名单中
33应用包名 在应用黑名单中 或者 不在应用白名单中
34自动下载未授权
41自动预约能力未接入
42媒体包名 在媒体黑名单中 或者 不在媒体白名单中
43应用包名 在应用黑名单中 或者 不在应用白名单中
44自动预约未授权
51促激活能力未接入
52媒体包名 在媒体黑名单中 或者 不在媒体白名单中
53应用包名 在应用黑名单中 或者 不在应用白名单中
54促激活未授权
71状态回传能力未接入
72媒体包名 在媒体黑名单中 或者 不在媒体白名单中
73应用包名 在应用黑名单中 或者 不在应用白名单中
74状态回传未授权
999系统异常

文档内容是否有帮助?
有帮助
无帮助