根据目前使用的极光推送,
设计一个合理的服务端,而且满足一切需求的数据结构很重要,其实也很简单
可能会有的需求: 推送消息给已注册用户、推送消息给所有用户、推送消息给匿名用户
这样的一个需求下,我们需要在app启动时,即保存用户设备did,不管有没有登录
上传机制: 用户设备did 上传的时机很重要
1. app启动时上传
2. 用户登录/切换登录时上传
数据结构如下:
CREATE TABLE `mz_android_profile` ( `pid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id, 自增长', `uid` bigint(20) NOT NULL COMMENT '用户id', `dtype` tinyint(4) NOT NULL COMMENT '设备类型 1:android phone 2: android pad', `did` varchar(128) NOT NULL COMMENT '设备唯一标识', `createtime` datetime NOT NULL COMMENT '创建时间', `updatetime` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`pid`), UNIQUE KEY `AK_Key_2` (`did`), KEY `AK_Key_3` (`uid`,`dtype`) ) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULT CHARSET=utf8 COMMENT='推送相关信息表'
以上是Android 设备token did的数据结构, ios也可以设计类似的结构,这样一个结构即可满足以上所有的需求(实现自己的推送逻辑,而不是使用极光的后台)
接口实现:
/** * Android push 主函数 * * @author sky * @date 2015-11-20 */ public class AndroidPushMain { private static final Logger logger = LoggerFactory.getLogger(AndroidPushMain.class); private String appkey; private String appMasterSecret; private static AndroidPushMain instance = null; /** * phone 的推送client */ private static JPushClient phoneClient; /** * pad 的推送client */ private static JPushClient padClient; private static final int maxRetryTimes = 3;// 重连次数 private AndroidPushMain() { } /** * 获取AndroidPushMain实例<br> * * singleton * * @return */ public static synchronized AndroidPushMain get() { if (instance == null) { instance = new AndroidPushMain(); } return instance; } /** * 根据android 设备类型 获取不同的 jpush client * * @param dtype 设备类型,当设备类型为PC时, 不支持推送 * @return */ private static JPushClient getClient(short dtype) { if (dtype == DeviceType.PHONE.getValue()) { if (phoneClient != null) { logger.info("android#push#getPushClient | JPush phoneClient 已经实例化过, Get | dtype: {}", getDeviceTypeName(dtype)); return phoneClient; } } else if (dtype == DeviceType.PAD.getValue()) { if (padClient != null) { logger.info("android#push#getPushClient | JPush padClient 已经实例化过, Get | dtype: {}", getDeviceTypeName(dtype)); return padClient; } } else { logger.error("android#push#getPushClient | 不支持的推送设备类型 PC | dtype: {}", getDeviceTypeName(dtype)); return null; } // TODO need to cache it String appkey = getKey(dtype); String appMasterSecret = getSecret(dtype); if (dtype == DeviceType.PAD.getValue()) { padClient = new JPushClient(appMasterSecret, appkey, maxRetryTimes); Args.check(null != padClient, "Init AndroidJpush padClient Failure."); return padClient; } else { phoneClient = new JPushClient(appMasterSecret, appkey, maxRetryTimes); Args.check(null != phoneClient, "Init AndroidJpush phoneClient Failure."); return phoneClient; } } /** * 发送消息给单个用户(一对一的业务 ,如: 商家有新单的消息提示) <br> * <b>注: 此处发送的JPush 类型为自定义的,可 通知栏显示, 可APP内部处理, 区别于 只是通知栏显示的消息</b> * * @param uid 用户id * @param dtype 设备类型 1:phone 2:pad 3:pc * @param msg 消息内容 * @param title 消息标题 * @param customFields 自定义数据 (可传null表示没有自定义内容) 与前端约定的常用的自定义数据有消息类型 type 默认使用 customFields.put("type", * MessagePushType.DEFAULT.getValue()); 其他如消息ID, msgId则暂未启用 */ public void push2One(long uid, short dtype, String msg, String title, Map<String, Object> customFields) { // 获取当前用户 登录的设备信息 Map<String, String> profile = getAndroidProfile(uid, dtype); if (profile == null || profile.size() <= 0) { logger.error("AndroidPushMain#notify | 获取用户推送相关信息时发生错误, 没找到设备信息 | uid: {}, dtype: {}", uid, dtype); return; } logger.info("AndroidPush#push2One | 发送消息给单个用户 | uid: {}, dtype: {}, msg: {}, title: {}, customFields: {}", uid, dtype, msg, title, customFields); String deviceToken = profile.get("deviceToken"); sendPush(deviceToken, title, msg, customFields, dtype, uid); } /** * * 底层发送接口 * * @param deviceToken 设备唯一标识 * @param title 消息标题 * @param msg 消息内容 * @param customFields 自定义数据 (可传null表示没有自定义内容) 与前端约定的常用的自定义数据有消息类型 type 默认使用 customFields.put("type", MessagePushType.DEFAULT.getValue()); * 其他如消息ID, msgId则暂未启用 * @param dtype 设备类型 phone、pad * @param uid 用户uid * @author sky 2016-03-11 */ public void sendPush(String deviceToken, String title, String msg, Map<String, Object> customFields, short dtype, long uid) { // 目前通过 registrationId 来发送消息 消息类型为自定义类型 JPushClient jpushClient = getClient(dtype); if (jpushClient == null) { logger.error("AndroidPush#push2One | 获得的JPush client 为空 | dtype: {}", getDeviceTypeName(dtype)); return; } String type = ""; Map<String, String> extras = null; if (MapUtils.isNotEmpty(customFields)) { // // 获取需要跳转的业务类型:跳转至APP首页 / 跳至商家新单列表/ 跳至... <br> // 具体业务类型 由 MessagePushType 中定义, 类型的定义需要服务端与客户端实现对接,扩展性不是很好 type = String.valueOf(customFields.get("type")); if (customFields.containsKey("params")) { extras = new HashMap<String, String>(); @SuppressWarnings("unchecked") Map<String, Object> tmp = (Map<String, Object>) customFields.get("params"); for (Entry<String, Object> entry : tmp.entrySet()) { extras.put(entry.getKey(), entry.getValue().toString()); } } } PushPayload payload = (null == extras ? JPushPayloadWrapper.messageWithRegId(deviceToken, title, type, msg) : JPushPayloadWrapper .messageWithRegId(deviceToken, title, type, msg, extras)); try { PushResult result = jpushClient.sendPush(payload); logger.info("AndroidPushMain#jpush | 消息推送完毕, 推送结果 | result: {}", result); } catch (Exception e) { if (e instanceof APIRequestException) { APIRequestException ee = (APIRequestException) e; logger.error( "AndroidPushMain#jpush | 发送推送时发生错误 | uid: {}, deviceToken: {}, title: {}, pushMsg: {}, httpStatus: {}, errorCode: {}, errorMsg: {}, msgId: {}", uid, deviceToken, title, msg, ee.getStatus(), ee.getErrorCode(), ee.getErrorMessage(), ee.getMsgId()); } else if (e instanceof APIConnectionException) { logger.error("AndroidPushMain#jpush | 发送推送Connection error. Should retry later | uid: {}, deviceToken: {}, errorMsg: {} ", uid, deviceToken, e.getMessage(), e); } } } /** * 获取用户的android设备 push 相关信息 * * @param uid 用户id * @param dtype 设备类型 1:phone 2:pad * @return * @author sky */ public static Map<String, String> getAndroidProfile(long uid, short dtype) { try { String json = HttpClientUtils.doGet(CommonConstants.USER_DOMAIN + "/api/v1/deviceToken/android/queryByUidAndDtype?uid=" + uid + "&dtype=" + dtype); RestResult<Map<String, String>> result = JsonUtils.parseObject(json, new TypeReference<RestResult<Map<String, String>>>() { }); return result.getObject(); } catch (Exception e) { logger.error("AndroidPushMain#getAndroidProfile | 获取用户Android设备信息发送错误 | uid: {}, dtype: {}, errorMsg: {}", uid, dtype, e.getMessage()); } return null; } private static String getKey(short dtype) { return (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get(getDeviceTypeName(dtype) + "_pushAppkey"); } private static String getSecret(short dtype) { return (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get(getDeviceTypeName(dtype) + "_pushAppMasterSecret"); } private static String getDeviceTypeName(short dtype) { String type = ""; if (dtype == DeviceType.PC.getValue()) type = "pc"; else if (dtype == DeviceType.PAD.getValue()) type = "pad"; else type = "phone"; return type; } public String getAppkey() { return appkey; } public void setAppkey(String appkey) { this.appkey = appkey; } public String getAppMasterSecret() { return appMasterSecret; } public void setAppMasterSecret(String appMasterSecret) { this.appMasterSecret = appMasterSecret; } }
/** * 对JPush PushPayload 的本地业务需求包装 * * @author sky * */ public class JPushPayloadWrapper { /** * android 平台 */ private static final Platform Android = Platform.android(); /** * 可通知栏显示, 可APP 内部显示(怎么显示由APP决定:当APP处于后台时,通知栏显示; 当APP处于前台时, 内部方式显示) <br> * 自定义消息 push * * @param regId 注册id(唯一标识) * @param title 消息标题 * @param contentType 内容类型 , 该值表达了这条push的业务类型, contentType的值在MessagePushType中被定义 * @param content 消息内容 * @return */ public static PushPayload messageWithRegId(String regId, String title, String contentType, String content) { return PushPayload.newBuilder().// setAudience(Audience.registrationId(regId)).// setPlatform(Android).// setMessage(Message.newBuilder().// setTitle(title).// setContentType(contentType).// setMsgContent(content).// build()).// build(); } /** * 可通知栏显示, 可APP 内部显示(怎么显示由APP决定:当APP处于后台时,通知栏显示; 当APP处于前台时, 内部方式显示) <br> * 自定义消息 push * * @param regId 注册id(唯一标识) * @param title 消息标题 * @param contentType 内容类型 , 该值表达了这条push的业务类型, contentType的值在MessagePushType中被定义 * @param content 消息内容 * @param extras 附件信息体 extras中包含了该条消息的业务类型数据:以type为键, value 为 MessagePushType 中定义的值 * @return */ public static PushPayload messageWithRegId(String regId, String title, String contentType, String content, Map<String, String> extras) { return PushPayload.newBuilder().// setAudience(Audience.registrationId(regId)).// setPlatform(Android).// setMessage(Message.newBuilder().// setTitle(title).// setContentType(contentType).// setMsgContent(content).// addExtras(extras).build()).// build(); } /** * 可通知栏显示, 可APP 内部显示(怎么显示由APP决定:当APP处于后台时,通知栏显示; 当APP处于前台时, 内部方式显示) <br> * 自定义消息 push * * @param alias 别名 * @param title 消息标题 * @param contentType 内容类型 , 该值表达了这条push的业务类型, contentType的值在MessagePushType中被定义 * @param content 消息内容 * @return */ public static PushPayload messageWithAlias(String alias, String title, String contentType, String content) { return PushPayload.newBuilder().setAudience(Audience.alias(alias)).// setPlatform(Android).// setMessage(Message.newBuilder().// setTitle(title).// setContentType(contentType).// setMsgContent(content).// build()).// build(); } /** * * <b>通知栏显示</b><br> * 广播式消息 push, 通过标签来发送给用户 * * @param tag 用户标签 * @param title 消息标题 * @param content 消息内容 * @param extras 附件信息体 extras中包含了该条消息的业务类型数据:以type为键, value 为 MessagePushType 中定义的值 * @return */ public static PushPayload notifyWithTag(String tag, String title, String content, Map<String, String> extras) { return PushPayload.newBuilder().// setPlatform(Android).// setAudience(Audience.tag(tag)).// setNotification(Notification.android(content, title, extras)).// build(); } /** * <b>通知栏显示</b><br> * 广播式消息 push,通过别名来发送给用户 * * @param alias 别名 * @param title 消息标题 * @param content 消息内容 * @param extras extras中包含了该条消息的业务类型数据:以type为键, value 为 MessagePushType 中定义的值 * @return */ public static PushPayload notifyWithAlias(String alias, String title, String content, Map<String, String> extras) { return PushPayload.newBuilder().// setPlatform(Android).// setAudience(Audience.alias(alias)).// setNotification(Notification.android(content, title, extras)).// build(); } /** * <b>通知栏显示</b><br> * 广播式消息 push,通过注册deviceToken来发送给用户 * * @param regId 注册id(唯一标识) * @param title 消息标题 * @param content 消息内容 * @param extras extras中包含了该条消息的业务类型数据:以type为键, value 为 MessagePushType 中定义的值 * @return */ public static PushPayload notifyWithRegId(String regId, String title, String content, Map<String, String> extras) { return PushPayload.newBuilder().// setPlatform(Android).// setAudience(Audience.registrationId(regId)).// setNotification(Notification.android(content, title, extras)).// build(); } /** * <b>通知栏显示</b><br> * 广播式消息 push * * @param content 消息内容 * @return */ public static PushPayload notifyAll(String content) { return PushPayload.alertAll(content); } }