立树
立树
  • 发布:2017-12-20 10:14
  • 更新:2021-05-08 11:17
  • 阅读:8823

个推推送的整体解决方案(思路、源码)

分类:HTML5+

.

说明

这是我 DC(DCloud)开发中遇到的第二个问题。
虽然个推功能强大,但是个推官网文档那叫一个简陋,导致众多开发者爬坑无数、心酸无比。

为什么要这样一个技术方案分享?
因为社区还没看到一个关于个推的整体解决方案,包含前台与后台的开发思路,而且开发源码。
希望对其他开发者提供点帮助,少踩几个坑,也希望 DC 更加完善。

.

推送效果

  1. 外推(外部推送)
    简单的说,就是我们能够看到的推送。
    比如,通知栏的推送列表,比如,苹果的临时横幅,我都称之为“外推”。
    .
  2. 内推(内部推送)
    这种推送是隐藏性质的,它们不会出现在手机的通知栏或横幅,它们适用于这种场景:
    ①. APP 在前台时,收到推送时,会出现一个对话框来询问用户是否跳转。
    ②. 完全不提示用户,比如,用户账号在其他手机登录,APP 接收到推送时,强制用户退出。
    .
  3. 智推(智能推送)
    APP 在前台时,发送内推。
    APP 在后台或者关闭时,发送外推。

.

开发流程

  1. 个推注册
    个推官网:http://getui.com/cn/index.html
    .
  2. 配置推送
    ①. 配置个推参数(appid、appkey、appsecret)。
    ②. 配置自定义基座,方便调试。
    .
  3. 后台发送推送
    定义一个推送的数据格式,便于前台、后台进行对接。
    我在开发时,用的是个推官网提供的 PHP SDK(我对其进行了封装,具体见下方)。
    .
  4. APP接收推送并处理
    对推送的数据进行解析,然后执行对应的业务逻辑(我对其进行了封装,具体见下方)。

.

测试数据

开发中使用了 5 台设备。
安卓:小米3(4.4.4)、锤子T2(5.1.1)、海马玩模拟器(4.2.2)、雷电模拟器(5.1.1)。
苹果:iPhone 6(iOS 11.0.3)。

个推支持 4 种推送模板。
①. 透传推送:点击通知打开网页模板(IGtLinkTemplate)。
②. 普通推送:点击通知打开应用模板(IGtNotificationTemplate)。
③. 链接推送:点击通知弹框下载模板(IGtNotyPopLoadTemplate)
④. 下载推送:透传消息模版(IGtTransmissionTemplate)。

因为 链接推送、下载推送 平时基本上不用,所以本方案没有纳入测试和封装,需要的朋友自己处理吧。

透传推送 和 普通推送 能够达到大体一致的效果。
透传推送,后台发送标准推送数据格式的推送时,APP 能收到一条带参数的推送。
普通推送,后台发送带透传数据的推送时,APP 也能收到一条带参数的推送。
所以,我对这两种推送模板做了分组对比测试。

还有一点,关于个推官方定义的 推送数据的标准格式和非标准格式,很多开发者都不是很理解,包括我在内,最开始因为不是很理解,按照自己想当然的思路对 5 台设备进行了测试,经过大量重复操作后,得到一组可观的数据,但是,无意中发现自己理解有误,导致之前的数据作废,也算是一个大坑,所以我还是简单的展示下两种模式的区别吧。

标准格式
必须符合这个样子,{title:"标题",content:"内容",payload:"数据"}。

$Message1 = [  
	"title"=> "健康告知",  
	"content"=> "您的中二病已经很严重了!",  
	"payload"=>[  
		"push"=> "inner",  
		"event"=> "warning",   
		"silent"=> false,  
		"data"=> ""  
	]  
];

非标准格式
只要不是上面的那种格式就满足(比如,缺少 payload 参数)。

$Message2 = [  
	"title"=> "健康告知",  
	"content"=> "您的中二病已经很严重了!",  
	"push"=> "inner",  
	"event"=> "warning",   
	"silent"=> false,  
	"data"=> ""  
];

最重要的一点:
只有发送标准格式数据,APP才能收到推送(外推)!!!
只有发送标准格式数据,APP才能收到推送(外推)!!!
只有发送标准格式数据,APP才能收到推送(外推)!!!

.
好了,下面是我测试的五组数据,如果你发现有些数据是错误的,请在评论里指出,免得误导别人。





.
数据分析

①. 安卓
测试后发现,小米3、锤子T2 关闭应用后,自动清除了个推的后台驻留进程,所以这种情况是没有办法解决的,很显然,真实手机(小米3、锤子T2)的数据已经废了。

海马玩和雷电的数据完全一致,我们就看雷电的数据。
第一组数据方案(带透传信息的普通推送),如果你亲自测试,你会发现,APP 会收到两条推送,普通推送模板默认发送一条,设置了带有标准数据格式的透传信息也会发送一条,很显然,不符合实际场景,排除这种方案。

第四组数据方案(非标准格式的透传推送),实现内推,所以这种方案是有用的。

第二组和第三组数据方案,实现的效果一致,那么既然是方案整合(应该有内推功能),所以应该选第三组,因为它和第四组(内推)共用一个模板(IGtTransmissionTemplate),在实际操作中,只需要切换数据格式(标准、非标准)来实现 内推、外推 的切换,能够节省不少代码。

②. 苹果
普通推送,APP 会自动弹出一个 bug 一样的确认对话框,点击“取消”也会触发确认时间,所以,数据作废。
透传推送,苹果手机比较特别,APP 在前台时,个推会发送内推,在后台或关闭时,个推会借助苹果的 APN 发送苹果的推送。

综上,选择透传推送的两种数据格式(标准、非标准)能够完美解决推送问题。

.

后台源码

.
参数说明

  • clientid
    要发送的用户的 clientid。
    .
  • title
    推送标题。
    .
  • content
    推送内容。
    .
  • push
    推送类型,仅安卓支持,苹果设置无效。
    outer:外推
    inner:内推
    smart:智推
    .
  • system
    接收推送的设备系统类型,因为它决定了工具类选择哪个推送数据格式。
    ios:苹果系统。
    android:安卓系统。
    .
  • event
    事件名称,APP 用来处理业务逻辑。
    .
  • data
    附加数据,可以携带业务数据发送给 APP,PHP 数组类型。
    .
  • silent
    是否启用静默模式,APP 接收到内推时,是否展示推送,布尔值。

.
hhPushClass.php
已经封装好的 PHP 类,逻辑都在里面,自己看吧,有问题可以在评论里指明。
类发送推送后,会将结果保存到 $result 中,包含了 arr、json 两种形式。
类里面的 $Message1、$Message2 是上文所说的定义的数据格式。

<?php  
  
# 名称 : hhGpush  
# 来源 : hhtools.php  
# 版本 : 1.0.0  
# 作者 : 立树  
# 网站 : studio.houheaven.com  
# 日期 : 2017-12-15  
  
class hhGpush  
{  
	public $gp_appid;		// 应用 ID(AppID)  
	public $gp_appkey;		// 应用 key(AppKey)  
	public $gp_token;		// 应用令牌(MasterSecret)  
	public $gp_host;		// 个推服务器  
	  
	private $gpush;         // 个推  
	public $result;         // 推送结果  
	  
	# 构造函数  
	public function __construct($appid,$appkey,$token)  
	{  
		// 属性初始化  
		$this->gp_appid = $appid;  
		$this->gp_appkey = $appkey;  
		$this->gp_token = $token;  
		$this->gp_host = "http://sdk.open.api.igexin.com/apiex.htm";  
		  
		// 个推初始化  
		$this->gpush = new IGeTui($this->gp_host,$appkey,$token);  
		$this->result = [];  
	}  
	  
	# 单个推送(支持安卓、苹果)  
	public function PushMsgToSingle($param)  
	{  
		// 参数检测  
		if( gettype($param)!="array" || !isset($param["clientid"]) )  
		{  
			exit("参数错误");  
		}  
		  
		// 参数初始化  
		// 标题  
		$title = isset($param["title"])?$param["title"]:"";  
		// 内容  
		$content = isset($param["content"])?$param["content"]:"";  
		// 推送类型(外推:outer、内推:inner、智能推送:smart)  
		$push = isset($param["push"])?$param["push"]:"outer";  
		// 事件名称(APP根据此参数决定执行哪些功能)  
		$event = isset($param["event"])?$param["event"]:"";  
		// 内推时,是否给用户展示提示信息(比如强制用户退出就不会展示提示信息)  
		$silent = isset($param["silent"])?$param["silent"]:false;  
		// 推送数据,附加的业务数据  
		$data = isset($param["data"])?$param["data"]:"";  
		  
		// 标准推送数据格式  
		$Message1 = [  
			"title"=> $title,  
			"content"=> $content,  
			"payload"=>[  
				"push"=> $push,  
				"event"=> $event,   
				"silent"=> $silent,  
				"data"=> $data  
			]  
		];  
		  
		// 非标准推送数据格式  
		$Message2 = [  
			"title"=> $title,  
			"content"=> $content,  
			"push"=> $push,  
			"event"=> $event,  
			"silent"=> $silent,  
			"data"=> $data  
		];  
		  
		// 用户状态  
		$aid = $this->gp_appid;  
		$cid = $param["clientid"];  
		$status = $this->gpush->getClientIdStatus($aid,$cid);  
		$this->result["arr"]["client_state"] = $status;  
		  
		// 推送模板  
		$system = isset($param["system"])?$param["system"]:"android";  
		$system = in_array($system,array("android","ios"))?$system:"android";  
		if( $system=="android" )  
		{  
			// 安卓模板  
			switch($param["push"])  
			{  
				case "inner":  
					$Message2["push"] = "inner";  
					$msg = $Message2;  
					break;  
				case "outer":  
					$Message1["payload"]["push"] = "outer";  
					$msg = $Message1;  
					break;  
				case "smart":  
					$status = $this->gpush->getClientIdStatus($aid,$cid);  
					if( $status["result"]=="Online" )  
					{  
						$Message2["push"] = "smart";  
						$msg = $Message2;  
					}  
					else  
					{  
						$Message1["payload"]["push"] = "outer";  
						$msg = $Message1;  
					}  
					break;  
			}  
		}  
		else  
		{  
			// 苹果模板  
			$Message1["payload"]["push"] = "inner";  
			$msg = $Message1;  
		}  
		  
		$tpl = $this->AwesomeTemplate($msg);  
		$this->result["arr"]["push_param"] = $msg;  
		  
		// 推送消息  
		$msg = new IGtSingleMessage();  
		$msg->set_isOffline(true);                     // 是否离线  
		$msg->set_offlineExpireTime(12*3600*100);      // 离线时间  
		$msg->set_data($tpl);                          // 推送消息模板  
		$msg->set_PushNetWorkType(0);                  // 设置是否根据WIFI推送消息,2为4G/3G/2G,1为wifi推送,0为不限制推送  
		  
		// 接收方  
		$target = new IGtTarget();  
		$target->set_appId($aid);  
		$target->set_clientId($cid);  
		  
		// 发送  
	    $ret = $this->gpush->pushMessageToSingle($msg,$target);  
	    $this->result["arr"]["push_state"] = $ret;  
	    $this->result["json"] = json_encode($this->result);  
	}  
	  
	// 透传推送模板  
	function AwesomeTemplate($param)  
	{  
		// 模板初始化  
		$template = new IGtTransmissionTemplate();  
		$template->set_appId($this->gp_appid);         // 应用appid  
		$template->set_appkey($this->gp_appkey);       // 应用appkey  
		  
		// 安卓推送(外推+内推)、苹果内推  
		$template->set_transmissionType(2);  
		$template->set_transmissionContent(json_encode($param));  // 透传内容  
		  
		// 苹果处于后台时的推送(外推)  
		$alertmsg = new DictionaryAlertMsg();  
		$alertmsg->actionLocKey = "ActionLockey";       // 个推官网提供,文档无说明  
		$alertmsg->launchImage = "launchimage";         // 个推官网提供,文档无说明  
		$alertmsg->locArgs = array("locargs");          // 个推官网提供,文档无说明  
		$alertmsg->locKey = $param["title"];            // 消息标题  
		$alertmsg->body = $param["content"];            // 消息内容  
		// iOS8.2 支持  
		$alertmsg->title = $param["title"];             // 消息标题  
		$alertmsg->titleLocKey = $param["title"];       // 消息标题  
		$alertmsg->titleLocArgs = array("TitleLocArg"); // 个推官网提供,文档无说明  
		  
		$apn = new IGtAPNPayload();  
		$apn->alertMsg = $alertmsg;  
		//$apn->badge = 1;  
		//$apn->sound = "";  
		$param["payload"]["push"] = "outer";        // 调用此处代码证明是外推  
		foreach($param as $key=>$val)  
		{  
			$apn->add_customMsg($key,$val);  
		}  
		$apn->contentAvailable = 1;                 // 个推官网提供,文档无说明  
		$apn->category = "ACTIONABLE";              // 个推官网提供,文档无说明  
		$template->set_apnInfo($apn);  
		  
		// 设置通知定时展示时间,结束时间与开始时间相差需大于6分钟  
		// 消息推送后,客户端将在指定时间差内展示消息(误差6分钟)  
		//$begin = "2017-12-14 15:20:00";  
		//$end = "2017-12-14 15:30:00";  
		//$template->set_duration($begin,$end);  
		  
		return $template;  
	}  
	  
}  
  
?>

代码调用

<?php  
  
header("Content-Type: text/html; charset=utf-8");  
  
require_once(dirname(__FILE__)."/IGt.Push.php");      // 个推 sdk  
require_once(dirname(__FILE__)."/hhPushClass.php");   // 封装好的工具类  
  
// 配置(替换成自己的)  
$Appid = "111111";  
$Appkey = "222222";  
$Mastersecret = "333333";  
  
// 实例化  
$gpush = new hhGpush($Appid,$Appkey,$Mastersecret);  
  
// 发送推送  
$gpush->PushMsgToSingle([  
	"clientid"=> "xxxxxxxxxx",  
	"event"=> "warning",  
	"title"=> "健康告知",  
	"content"=> "您的中二病已经很严重了!",  
	"push"=> "smart",  
	"system"=> "android",  
	"silent"=> false  
]);  
  
// 打印结果  
var_dump($gpush->result);

.

前台源码

.
参数

插件的唯一参数是一个回调函数。
收到推送事件(Receive)、推送点击事件(Click)已被内置在插件中,当这两个事件被触发时,插件会先已处理好推送逻辑,然后将推送的数据进行格式化,然后会触发这个回调函数,并将格式化的消息返回,以供开发者处理业务逻辑。

格式化的推送消息:

  • title 消息标题
  • descp 消息内容
  • event 事件名称(开发者根据此参数决定执行业务逻辑)
  • data 推送数据(服务器返回的业务数据)

.
Gpush.js

  
// Gpush.app.dc.1.1.0  
// 作者 : 立树  
// 日期 : 2017-12-19  
// 来源 : hhtools.app.js  
// 文档 : http://studio.houheaven.com  
  
function Gpush(fnPushExec)  
{  
	// 设置应用为前台事件  
	localStorage.setItem("isAppActive",true);  
	// 注册应用切换到后台事件  
	document.addEventListener("pause",function(){  
		localStorage.setItem("isAppActive",false);  
	});  
	// 注册应用切换到前台事件  
	document.addEventListener("resume",function(){  
		localStorage.setItem("isAppActive",true);  
	});  
	  
	// 推送点击事件  
	plus.push.addEventListener("click",function(msg){  
		push_proc(msg);  
	});  
	  
	// 推送接收事件  
	plus.push.addEventListener("receive",function(msg){  
		push_proc(msg);  
	});  
	  
	// 推送消息预处理  
	function push_proc(msg)  
	{  
		// 解析  
		var payload = typeof(msg.payload)=="string"?JSON.parse(msg.payload):msg.payload;  
		  
		// 消息格式化  
		var notice = {  
			title: msg.title,         // 标题  
			descp: msg.content,       // 内容  
			event: payload.event,     // 事件名称(APP根据此参数决定执行哪些功能)  
			data: payload.data,       // 推送数据,附加的业务数据  
			silent: payload.silent    // 内推时,是否给用户展示提示信息(比如强制用户退出就不会展示提示信息)  
		};  
		  
		// 系统检测  
		if( plus.os.name=="Android" )  
		{  
			// 推送检测  
			switch(payload.push)  
			{  
				case "smart":  
					// 智能推送  
					// 能进入这里的都是内推,然后判断APP的状态(前台、后台)  
					// 前台:直接进入内推的业务逻辑  
					// 后台:补发一个本地推送  
					if( localStorage.getItem("isAppActive")=="false" )  
					{  
						// 按照逻辑,或者以我强迫症的调性来说,是不会更改原本推送的状态的,因为我已经监听了用户从后台到前台的事件(把 isAppActive 改为 true),刚刚模拟出来的本地推送被点击后,会直接触发推送业务逻辑,简直完美。  
						// 但是,计划赶不上变化,在所有操作过程中,总会有先后顺序,这里就是,在用户切换应用到前台时(resume),还没来得及将应用状态 isAppActive 改为 true 的时候,这里的判断已经执行了,而 isAppActive 依然是 false,所以会导致再次创建本地推送。  
						// 所以啊,不说了,只好妥协了。  
						payload.push = "outer";  
						plus.push.createMessage(msg.content,JSON.stringify(payload),{  
							title: msg.title  
						});  
					}  
					else  
						push_check(notice);  
					break;  
				case "outer":  
					// 外推  
					push_exec(notice);  
					break;  
				case "inner":  
					// 内推  
					push_check(notice);  
					break;  
			}  
		}  
		else  
		{  
			// 推送检测  
			if( payload.push=="outer" )  
			{  
				// 外推  
				push_exec(notice);  
			}  
			else  
			{  
				// 内推  
				push_check(notice);  
			}  
		}  
	}  
	  
	// 内推时提示信息处理  
	function push_check(notice)  
	{  
		if( notice.silent!=true )  
		{  
			plus.nativeUI.confirm("\n"+notice.descp+"\n\n",function(ret){  
				if( ret.index==1 )  
				{  
					push_exec(notice);  
				}  
			},{  
				title: notice.title,  
				buttons: ["取消","查看"]  
			});  
		}  
		else  
			push_exec(notice);  
	}  
	  
	// 推送点击处理  
	function push_exec(notice)  
	{  
		// 事件处理  
		!fnPushExec || fnPushExec(notice);  
	}  
}

代码调用

<?php  
// 个推推送  
Gpush(function(notice){  
	switch(notice.event)  
	{  
		case "warning":     // 警告  
			alert(notice.descp);  
			break;  
		case "logout":      // 退出登录  
			UserLogout(notice.data);  
			break;  
	}  
});

.
以上,有疑问的可以在评论区留言,希望对你有帮助。

7 关注 分享
太烏 1***@qq.com l***@163.com q***@163.com lhyh guojiahsuai 过时的流行

要回复文章请先登录注册

雪魄

雪魄

离线推送能跳转页面吗
2021-05-08 11:17
jtshushu

jtshushu

厂商推送的接口没有吗?
2020-05-15 11:17
x***@vip.qq.com

x***@vip.qq.com

厂商推送的接口没有吗?
2019-10-09 17:00
guojiahsuai

guojiahsuai

优秀,点赞
2018-12-27 18:28
t***@163.com

t***@163.com

回复 立树 :
问一下大佬,这个标准格式是指必须有且只有这三个字段才算,多一个少一个都不行,是这意思吗?
2018-11-14 22:00
1***@qq.com

1***@qq.com

非常优秀的回答了,看官方的介绍一头雾水,看你这个条理清晰,具体详细,一下就明白了,感谢
2018-08-18 19:14
立树

立树 (作者)

回复 回梦無痕 :
666,表达的很清晰。
实际中确实也是这样,你发个推送,用户不点,除了哭,你能怎么样。
所以这个方案也把这种情况考虑到了,重要的推送用“内推”就行了,万无一失。
2017-12-21 18:09
回梦無痕

回梦無痕

HB推荐使用标准的透传格式,但是实际使用中,推荐使用非标准格式。
如果用户会点击推送框进入app,使用标准格式和使用非标准格式,都一样。
如果用户不会点击推送框进入app,那么,使用标准格式,将拿不到透传数据,使用非标准格式能拿到透传数据。
2017-12-21 14:32