push数据结构设计



根据目前使用的极光推送,

设计一个合理的服务端,而且满足一切需求的数据结构很重要,其实也很简单

可能会有的需求: 推送消息给已注册用户、推送消息给所有用户、推送消息给匿名用户


这样的一个需求下,我们需要在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);
	}

}







猜你喜欢

转载自xiaoliang330.iteye.com/blog/2282922