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

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

分类: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