Android / IOS 推送的java服务端这几种设计你知道吗?


 前几个月研究推送,小记一下,网上资料乱糟糟还没有文档说的明白

里面有很多其他功能,比如定时,比如别名,比如拉数据等等这些是后续的动作暂且不说,对于推送本身先多理解完善,后续好办

除了推送其他我就不说了,看文档接入即可,错误码去文档中找,说一说具体推送设计和注意点,我用的是如下推送 

  • 个推
  • 小米推送
  • 华为推送
  • oppo推送
  • vivo推送

个推对于IOS来说是可以的,用的是IOS本身的APN进行推送,app内外都会收到消息;Android的适配场景比较多比较烦,不知道什么时候能够“统一全国”啊哈哈,个推打开app其到达率还是可以的,如果关闭app就收不到消息了,所以就有了后续的补充推送,用其本身的推送提高到达率,

不过四个厂商推送还是有不足的地方,有的推送并不能很好的满足用户的需求,比如透传,下面我会说到

模板:可以建立推送模板,由数据库控制,可以借鉴微信的结构,一个表-结构,一个表-数据,或者结构和数据放在一起进行控制,自己可以设计一下表,导出需要推送的数据后根据模板进行推送即可


个推

官方文档:http://docs.getui.com/getui/server/java/start/

内容比较丰富,比较常用的模板是NotificationTemplate、LinkTemplate和TransmissionTemplate

扫描二维码关注公众号,回复: 9192074 查看本文章
  • NotificationTemplate:(点击通知模板)在通知栏显示一条含图标、标题等的通知,用户点击后激活您的应用(此模板需要点击才可以将透传信息传递给前端进行下一步动作)
  • LinkTemplate:(网页模板)在通知栏显示一条含图标、标题等的通知,用户点击可打开您指定的网页。
  • TransmissionTemplate:(透传模板)透传消息是指消息传递到客户端只有消息内容,展现形式由客户端自行定义。客户端可自定义通知的展现形式,也可自定义通知到达之后的动作,或者不做任何展现。iOS推送也使用该模板。

相关业务场景文档中也有说明,一般用NotificationTemplate和LinkTemplate就可以做到,我用的是TransmissionTemplate,需要推送到达后客户端接收透传信息自动语音播报

/*
    *   Android - ios透传模板 (不用点击直接播报)
    */
    public static TransmissionTemplate getTemplate(String appId, String appKey, PushDTO pushDTO) {
        TransmissionTemplate template = new TransmissionTemplate();
        template.setAppId(appId);
        template.setAppkey(appKey);
        //透传信息
        template.setTransmissionContent(JSON.toJSONString(pushDTO.getTransmissionDTO()));
        template.setTransmissionType(2);
        APNPayload payload = new APNPayload();
        payload.setAutoBadge("+1");
        payload.setContentAvailable(1);
        payload.setSound("default");
        payload.setCategory("$由客户端定义");
        //字典模式使用下者
        payload.setAlertMsg(getDictionaryAlertMsg(pushDTO.getTitle(), pushDTO.getText()));
        payload.addCustomMsg("contentJsonStr", JSON.toJSONString(pushDTO.getTransmissionDTO()));
        template.setAPNInfo(payload);
        return template;
    }

    //apn消息
    private static APNPayload.DictionaryAlertMsg getDictionaryAlertMsg(String title, String text) {
        APNPayload.DictionaryAlertMsg alertMsg = new APNPayload.DictionaryAlertMsg();
        alertMsg.setBody("");
        alertMsg.setActionLocKey("");
        alertMsg.setLocKey("");
        alertMsg.addLocArg("");
        alertMsg.setLaunchImage("");
        // IOS8.2以上版本支持
        alertMsg.setTitle(title);
        alertMsg.setSubtitle(text);
        alertMsg.setTitleLocKey("");
        alertMsg.addTitleLocArg("");
        return alertMsg;
    }

模板我是这样的设置,文档中也有,下面参照文档根据cid推送-单推,Android和IOS都可用这个接口

private Map<String, Object> pushToSingle(String url, String appKey, String masterSecret, String appId,
                                             PushDTO pushDTO) {
        try {
            IGtPush push = new IGtPush(url, appKey, masterSecret);
            TransmissionTemplate template = getTemplate(appId, appKey, pushDTO);

            SingleMessage message = new SingleMessage();
            message.setOffline(true);
            // 离线有效时间,单位为毫秒,可选
            message.setOfflineExpireTime(24 * 3600 * 1000);
            message.setData(template);
            // 可选,1为wifi,0为不限制网络环境。根据手机处于的网络情况,决定是否下发
            message.setPushNetWorkType(0);
            Target target = new Target();
            target.setAppId(appId);
            target.setClientId(pushDTO.getCid());
            IPushResult ret = null;
            try {
                ret = push.pushMessageToSingle(message, target);
            } catch (RequestException e) {
                log.info("推送失败,重新推送:cid:{}", pushDTO.getCid());
                e.printStackTrace();
                //下面是重推接口
                ret = push.pushMessageToSingle(message, target, e.getRequestId());
            }
            if (ret != null) {
                log.info("android/ios cid:{}对单个用户推送消息返回:{}", pushDTO.getCid(), ret.getResponse());
                return ret.getResponse();
            }
            return null;
        } catch (Exception e) {
            log.error("cid:{}对单个用户推送消息错误:{}", pushDTO.getCid(), e);
            return null;
        }
    }

返回形式:{taskId=OSS-0212_60001748948e9d3349048409ca699882, result=ok, status=successed_offline}

群推

private Map<String, Object> pushToList(String url, String appKey, String masterSecret, String appId,
                                           PushDTO pushDTO) {
        try {
            // 配置返回对应cid的用户状态,可选
            System.setProperty("gexin_pushList_needDetails", "true");
            IGtPush push = new IGtPush(url, appKey, masterSecret);
            // 通知透传模板
            TransmissionTemplate template = getTemplate(appId, appKey, pushDTO);
            ListMessage message = new ListMessage();
            message.setData(template);
            // 设置消息离线,并设置离线时间
            message.setOffline(true);
            // 离线有效时间,单位为毫秒,可选
            message.setOfflineExpireTime(24 * 1000 * 3600);
            // 配置推送目标
            List targets = new ArrayList();
            for (String str : pushDTO.getList()) {
                Target target = new Target();
                target.setAppId(appId);
                target.setClientId(str);
                targets.add(target);
            }

            // taskId用于在推送时去查找对应的message
            String taskId = push.getContentId(message);
            IPushResult ret = push.pushMessageToList(taskId, targets);
            log.info("android/ios - cid对指定列表用户推送消息pushMessageToList返回:{}", ret.getResponse());
            return ret.getResponse();
        } catch (Exception e) {
            log.info("cid们{},推送消息pushMessageToList错误:{}", pushDTO.getList(), e);
            return null;
        }
    }

返回形式:

{
    "result":"ok",
    "details":{
        "c281a495eb69d4fbd4f9d795bd003ea9":"successed_offline",
        "c281a495eb69d4fbd4f9d79555555555":"TokenMD5Error"
    },
    "contentId":"OSL-0212_73lrrQZSBP9sPJmEY30Eg8"
}

推送给所有app我就不贴了

这里是Android常见问题,了解一下cid为什么会变等等:http://docs.getui.com/question/getui/android/

个推可以分环境测试,有几个环境就建几个app包


小米推送

官方文档:https://dev.mi.com/console/doc/detail?pId=1278

小米推送比较简单也比较完善,直接贴码构建信息

private Message buildMessageForAndroid(String title, String description, String messagePayload) {
        Message message;
        Message.Builder builder = new Message.Builder()
                .title(title)//标题(注意16字真言限制长度,这段画上重点考)
                .description(description).payload(messagePayload)//描述(注意128限制长度,这段画上重点考,这个描述,我理解为副标题,而且在手机客户端呈现的也是标题+描述,内容不会自己显示出来,如果只是为了通知用户信息,我们可以将信息内容放在此处,显示效果比较明显。但是三个文字区域都不可空。需要补充文字方可使用)
                .restrictedPackageName("com.mo.dr")//APP包名
                .passThrough(0)//是否透传
                .notifyType(1)//设置震动,响铃等等
                .notifyId((int)(Math.random()*90+10))
//                .extra("extend_content", extendContent);//这里要注意下,你可以通过自定义的key传给客户端一段透传参数
                .extra(Constants.EXTRA_PARAM_NOTIFY_EFFECT, Constants.NOTIFY_LAUNCHER_ACTIVITY);
        message = builder.build();
        return message;
    }

根据regid推送消息,regId是app在客户端向小米推送服务注册时, 小米推送服务端根据设备标识和appId以及当前时间戳生成, 因此能够保证每个设备上每个app对应的regId都是不同的, 可以作为每台设备上app的唯一标识;和个推的clientid一个道理

注意自定义key   .extra,预定义通知栏通知的点击行为,一共有三种点击方式,参照文档设置一下

1.打开当前app对应的activity

2.打开当前app内任意一个activity

3.打开网页

单推

public Result sendMessageToRegId(String title, String description,
                                     String messagePayload, String regId) {
        Constants.useOfficial();//这里要注意,这是正式-启动方式,支持IOS跟Android,Constants.useSandbox();这是测试-启动方式,不支持Android,尽量申请正式APP,利用正式环境测试
        Sender sender = new Sender("你的secret");
        Message message = buildMessageForAndroid(title, description, messagePayload);
        Result result = null;
        try {
            result = sender.send(message, regId, 3);
            /**
             * 发送消息返回的错误码,如果返回ErrorCode.Success表示发送成功,其他表示发送失败。
             */
            ErrorCode errorMessage = result.getErrorCode();
            /**
             * 如果消息发送成功,服务器返回消息的ID;如果发送失败,返回null。
             */
            String platformMessageId = result.getMessageId();

            /**
             *  如果消息发送失败,服务器返回错误原因的文字描述;如果发送成功,返回null。
             */
            String reason = result.getReason();
            log.debug("MI错误码:{},服务器返回消息的ID-如果发送成功不是null:{}-->错误原因 成功为null:{}",
                    JSON.toJSONString(errorMessage), platformMessageId, reason);
        } catch (Exception e) {
            log.error("MI推送异常:{}",e.toString());
        }
        return result;
    }

群推

public Result sendMessageToRegIdList(String title, String description,
                                         String messagePayload, List<String> list) {
        log.debug("小米推送list:{}", list);
        Constants.useOfficial();//这里要注意,这是正式-启动方式,支持IOS跟Android,Constants.useSandbox();这是测试-启动方式,不支持Android,尽量申请正式APP,利用正式环境测试
        Sender sender = new Sender("你的secret");
        Message message = buildMessageForAndroid(title, description, messagePayload);
        Result result = null;
        try {
            result = sender.send(message, list, 3);
            log.debug("小米推送list-错误码:{}服务器返回消息的ID成功不是null:{}-->错误原因成功不是null:{}", JSON.toJSONString(result.getErrorCode()), result.getMessageId(), result.getReason());
        } catch (Exception e) {
            log.error("MI推送异常:{}",e.toString());
        }
        return result;
    }

注意:根据regids, 发送消息到指定的一组设备上, regids的个数不得超过1000个


华为推送

官方文档:https://developer.huawei.com/consumer/cn/service/hms/catalog/huaweipush_agent.html?page=hmssdk_huaweipush_devguide_server_agent

分通知栏消息、透传消息,官方说尽量用通知栏消息,降低功耗,提高到达率,且提供了推送流程两者之间的对比,可以看到要先获取token

代码我就不贴了,官方有示例代码,基本也都是用这一套,具体设置参照文档设置满足自己的需要

华为推送因为同一包名不可以建立多个app,所以无法分环境推送,这里记得测试环境要做好白名单(当然了,如果你有办法做到分环境推送最好不过)

官方注意:注意:目前access_token的有效期通过返回的expires_in来传达,access_token的有效时间可能会在未来有调整。access_token在有效期内尽量复用,业务要根据这个有效时间提前去申请新access_token即可。如果业务频繁申请access_token,可能会被流控。业务在API调用获知access_token已经超过的情况下(NSP_STATUS=6),可以触发access_token的申请流程。

我试验了一下token可以做到分环境控制,token都是3600s过期,且第二次请求获取的tokenB与第一次的tokenA都有效,各自有各自的失效时间,刚开始我没发现还以为第一个会失效,这样就可以做到token放入缓存中,测试用测试的token,生产用生产的token了

注意每个群推循环也是不要超过1000个


oppo推送

oppo推送我记得有一个比较坑的一点是oppo安装了app后推送提醒默认是关闭的…… 透传消息也做不到,后来都不想用了…… 还是先整理一下吧

官方文档:http://storepic.oppomobile.com/openplat/resource/201812/03/OPPO%E6%8E%A8%E9%80%81%E5%B9%B3%E5%8F%B0%E6%9C%8D%E5%8A%A1%E7%AB%AFAPI-V1.3.pdf

oppo和华为一样也是需要鉴权token,然后携带token进行动作

获取token

private String tokenMake() {
        try {
            long times = System.currentTimeMillis();
            String sign = appKey + times + masterSecret;
            String shaSign = SHA256Util.getSHA256StrJava(sign);
            String data = "app_key=" + appKey + "&timestamp=" + times + "&sign=" + shaSign;
            JSONObject jsonReturn = httpRequest(url + "/server/v1/auth", "POST", data, null);
            log.debug("oppo获取token返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("code").compareTo("0") == 0) {
                String jsonData = jsonReturn.getString("data");
                String token = (JSONObject.parseObject(jsonData)).getString("auth_token");
                return token;
            }
            log.error("oppo-token没有获取到");
            return null;
        } catch (Exception e) {
            log.error("oppo获取token错误:{}", e.toString());
            return null;
        }
    }

appkey和masterSecret自己去平台找,SHA256加密自己去网上找一下返回形式如下

{
    "code":0,
    "message":"sucess",
    "data":{
        "auth_token":"58ad47319e8d725350a5afd5",
        "create_time":"时间毫秒数"
    }
}

token权限令牌,推送消息时,需要提供 auth_token,有效期默认为 24 小时,过期后无法使用,这里有意思了,和华为不一样,oppo的token有效时间24小时,24小时内你请求多少次都是这个token,并没有变,所以也可以不分环境,大家都用这个token就行

消息设置

private JSONObject noticeMessage(TransmissionDTO transmissionDTO) {
        JSONObject notice = new JSONObject();
        notice.put("app_message_id", System.currentTimeMillis());
        notice.put("title", transmissionDTO.getTitleStr());
        notice.put("sub_title"," ");
        notice.put("content", transmissionDTO.getContent());
        notice.put("click_action_type", 0);// 0,启动应用;1,打开应 用内页 ;2, 打开网页; 4,打开应用内页(activity); 【非必填,默认值为 0】
        notice.put("show_time_type", 0);//展示类型0即使,1定时
//        notice.put("show_start_time", );//展示开始时间
//        notice.put("show_end_time",);
        notice.put("off_line_ttl", 259200);// 离线消息的存活时间 (单位:秒), 【off_line 值为 true 时, 必填,最长 3 天259200秒】
        notice.put("push_time_type", 0);//定时推送 (0, “即时”),(1, “定 时”), 【只对全部用户推送生效】
//        notice.put("push_start_time", )//推送开始时间
        notice.put("network_type", 0);//0不限联网
        return notice;
    }

单推

public JSONObject oppoPushToSingle(TransmissionDTO transmissionDTO, String regId) {
        try {
            String token = getAuthToken();
            if (token == null) {
                return null;
            }
            JSONObject notice = noticeMessage(transmissionDTO);

            JSONObject param = new JSONObject();
            param.put("target_type", 2);
            param.put("target_value", regId);
            param.put("notification", notice);
            String body = "message=" + JSON.toJSONString(param);

            JSONObject jsonObject = httpRequest(url + "/server/v1/message/notification/unicast",
                    "POST", body, token);
            log.debug("oppo-single推送返回:{}", jsonObject);
            return jsonObject;
        } catch (Exception e) {
            log.error("oppo-single推送失败:{}");
            return null;
        }
    }

transmissionDTO是透传信息,regid是设备id,返回形式如下,两种格式,错误情况和成功情况

{"message":"Invalid RegistrationId","code":10000}
{
    "message":"Success",
    "data":{
        "status":"call_success",
        "messageId":"5bf3ad4cfaad176240df4985"
    },
    "code":0
}

群推

public JSONObject oppoPushToList(TransmissionDTO transmissionDTO, List<String> regIds) {
        log.debug("OPPO群推接收:{},----:{}", transmissionDTO, regIds);
        try {
            String token = getAuthToken();
            if (token == null) {
                return null;
            }

            JSONObject jsonObject = pushList(transmissionDTO, regIds, token);
            return jsonObject;
        } catch (Exception e) {
            log.error("oppo群推异常:{}", e.toString());
            return null;
        }
    }

返回形式

{
        "message":"Success",
        "data":[
            {
                "registrationId":"CN_05337b7d20819041980c19955182cdb1",
                "messageId":"5c4c0a7872bc86259df5f557"
            },
            {
                "errorMessage":"Invalid RegistrationId",
                "registrationId":"CN_u2o3hkj54hkj5h3kj4h52k3jh54k3j55",
                "errorCode":10000
            }
        ],
        "code":0
        }

成功的格式和失败的格式如上所示

http请求方法

private JSONObject httpRequest(String reqUrl, String reqMethod, String jsonDataStr, String authToken) {
        JSONObject jsonObject = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 建立连接
            URL url = new URL(reqUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod(reqMethod);
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencode");
            if (StringUtils.isNotBlank(authToken)) {
                connection.setRequestProperty("auth_token", authToken);
            }
            if (jsonDataStr != null) {
                OutputStream out = connection.getOutputStream();
                out.write(jsonDataStr.getBytes("UTF-8"));
                out.close();
            }
            // 流处理
            // read response
            InputStream input = null;
            if (connection.getResponseCode() < 400) {
                input = connection.getInputStream();
            } else {
                input = connection.getErrorStream();
            }
            InputStreamReader inputReader = new InputStreamReader(input, "UTF-8");
            BufferedReader reader = new BufferedReader(inputReader);
            String line;
            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }
            // 关闭连接、释放资源
            reader.close();
            inputReader.close();
            input.close();
            input = null;
            connection.disconnect();
            jsonObject = JSONObject.parseObject(buffer.toString());
        } catch (Exception e) {
            log.error("vivo请求失败喽");
            return null;
        }
        return jsonObject;
    }

还是要注意:regid每次不可超过1000


vivo推送

官方文档:https://swsdl.vivo.com.cn/appstore/developer/uploadfile/20190129/20190129110835218.pdf

获取鉴权token,有效时间24小时,一天限制调用不超过10000次,注意用缓存来做

private String tokenMake() {
        try {
            long times = System.currentTimeMillis();
            String sign = appid + appkey + times + secret;
            String md5Sign = MD5Util.string2MD5(sign);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("appId", appid);
            jsonObject.put("appKey", appkey);
            jsonObject.put("timestamp", times);
            jsonObject.put("sign", md5Sign);
            JSONObject jsonReturn = httpRequest(url + "/message/auth", "POST", JSON.toJSONString(jsonObject), null);
            log.debug("vivo获取authToken返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("result").compareTo("0") == 0) {
                String token = jsonReturn.getString("authToken");
                redisTemplate.opsForValue().set(Constant.VIVO_PUSH_TOKEN, token, 86300, TimeUnit.SECONDS);
                return token;
            }
            log.error("token没有获取到");
            return null;
        } catch (Exception e) {
            log.error("vivo获取token错误:{}", e.toString());
            return null;
        }
    }

消息体设置

private static void requestBody(JSONObject body, TransmissionDTO transmissionDTO) {
        body.put("title", transmissionDTO.getTitleStr());//消息标题
        body.put("content", transmissionDTO.getContent());//消息内容体
        body.put("notifyType", "4");
        body.put("timeToLive", "86400");
        body.put("skipType", "1");//点击跳转类型 1.打开app首页,2.打开链接 3.自定义 4.打开app内指定页面
//        body.put("skipContent", "http://www.vivo.com");
        body.put("networkType", "-1");
        body.put("requestId", String.valueOf(System.currentTimeMillis()));
        Map<String, String> map = new HashMap<String, String>();
        map.put("transmissionDTO", JSON.toJSONString(transmissionDTO));
        body.put("clientCustomMap", map);
    }

获取taskid

private String getTaskId(TransmissionDTO transmissionDTO, String authToken) {
        try {
            JSONObject body = new JSONObject();
            body.put("isBusinessMsg", "0");
            requestBody(body, transmissionDTO);
            JSONObject jsonObject = httpRequest(url + "/message/saveListPayload", "POST", JSON.toJSONString(body), authToken);
            log.debug("取得taskid返回:{}", jsonObject);
            if (jsonObject != null && jsonObject.getString("result").compareTo("0") == 0) {
                return jsonObject.getString("taskId");
            }
            return null;
        } catch (Exception e) {
            log.error("获取taskid错误哦:{}", e.toString());
            return null;
        }
    }

单推

public JSONObject vivoPushToSingle(TransmissionDTO transmissionDTO, String regId) {
        try {
            String token = getAuthToken();
            log.debug("VIVO-token-----》{}", token);
            if (token == null) {
                return null;
            }
            JSONObject param = new JSONObject();
            param.put("callback", "http://www.vivo.com");
            param.put("callback.param", "vivo");

            JSONObject body = new JSONObject();//仅通知栏消息需要设置标题和内容,透传消息key和value为用户自定义
            body.put("regId", regId);

            body.put("extra", param);
            requestBody(body, transmissionDTO);
            log.debug("vivo发送消息体:{}", body);

            JSONObject jsonObject = httpRequest(url + "/message/send", "POST", JSON.toJSONString(body), token);
            log.debug("vivo-single推送返回:{}", jsonObject);
            return jsonObject;
        } catch (Exception e) {
            log.error("vivo-single推送失败:{}", e.toString());
            return null;
        }
    }

群推

public JSONObject vivoPushToList(List<String> regIdList, TransmissionDTO transmissionDTO) {
        try {
            log.debug("批量推送接收:regIdList:{}", regIdList);
            log.debug("transmissionDTO---》》{}", transmissionDTO);
            String authToken = getAuthToken();
            if (authToken == null) {
                return null;
            }
            String taskId = getTaskId(transmissionDTO, authToken);
            if (taskId == null) {
                log.error("获取taskid失败");
                return null;
            }

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("regIds", regIdList);
            jsonObject.put("taskId", taskId);
            jsonObject.put("requestId", String.valueOf(System.currentTimeMillis()));
            JSONObject jsonReturn = httpRequest(url + "/message/pushToList", "POST", JSON.toJSONString(jsonObject), authToken);
            log.debug("vivo-list推送返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("result").compareTo("0") == 0) {
                return jsonReturn;
            }
            return null;
        } catch (Exception e) {
            log.error("vivo-list推送失败:{}", e.toString());
            return null;
        }
    }

注意regid一个推送循环不要超过1000个

vivo推送我记得也是没法透传,然后手机适配型号还多,有些型号还推送不了有些坑……大家可以再了解了解再考虑是否用吧


对接好每一个推送,设计好模板,记录好成功/失败的记录,原因等等,单推还好说,群推要记录每一条数据的是否推送成功失败原因等等,并且要测一下推送的每次推送吞吐量是多少,会不会崩溃等等

白名单要做好,推送对于用户来说最忌讳的就是真实用户收到了测试的乱七八糟的推送那就尴尬了,甚至一条推送循环推送多次,用户体验将会降到冰点,一定一定一定要注意检查这个,没发不要紧,发错了,重复发才是致命的

good luck

发布了45 篇原创文章 · 获赞 44 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/Goligory/article/details/88949061
今日推荐