Java生鮮電商平台-通知模塊設計與架構
說明:對於一個生鮮的B2B平台而言,通知對於我們實際的運營而言來講分為三種方式:
1. 消息推送:(采用極光推送)
2. 主頁彈窗通知。(比如:現在有什么新的活動,有什么新的優惠等等)
3. 短信通知.(對於短信通知,這個大家很熟悉,我們就說下我們如何從代碼層面對短信進行分層的分析與架構)
1. 消息推送
說明:目前市場上的推送很多,什么極光推送,環信,網易雲等等,都可以實現秒級別的推送,我們經過了市場調研與穩定性考察,最終選擇了極光推送。
極光推送,市面上有很大的文檔與實例,我這邊就不詳細講解了,因為文檔很清晰,也的確很簡單。
相關的核心功能與代碼如下:
1. 功能划分
1.1向所有的人推送同一個消息。
1.2 具體的某個人,或者某類人推送消息,自己簡單的進行了一個SDK等封裝
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.jiguang.common.ClientConfig;
import cn.jiguang.common.resp.APIConnectionException;
import cn.jiguang.common.resp.APIRequestException;
import cn.jpush.api.JPushClient;
import cn.jpush.api.push.PushResult;
import cn.jpush.api.push.model.Options;
import cn.jpush.api.push.model.Platform;
import cn.jpush.api.push.model.PushPayload;
import cn.jpush.api.push.model.audience.Audience;
import cn.jpush.api.push.model.notification.AndroidNotification;
import cn.jpush.api.push.model.notification.IosNotification;
import cn.jpush.api.push.model.notification.Notification;
/**
* 激光推送
*/
public class Jdpush {
private static final Logger log = LoggerFactory.getLogger(Jdpush.class);
// 設置好賬號的app_key和masterSecret
public static final String APPKEY = "";
public static final String MASTERSECRET = "";
/**
* 推送所有
*/
public static PushPayload buildPushObjectAndroidIosAllAlert(String message){
return PushPayload.newBuilder()
.setPlatform(Platform.android_ios())
.setAudience(Audience.all())//推送所有;
.setNotification(Notification.newBuilder()
.addPlatformNotification(AndroidNotification.newBuilder()
.addExtra("type", "infomation")
.setAlert(message)
.build())
.addPlatformNotification(IosNotification.newBuilder().setSound("callu")
.addExtra("type", "infomation")
.setAlert(message)
.build())
.build())
.setOptions(Options.newBuilder()
.setApnsProduction(false)//true-推送生產環境 false-推送開發環境(測試使用參數)
.setTimeToLive(90)//消息在JPush服務器的失效時間(測試使用參數)
.build())
.build();
}
/**
* 推送 指定用戶集合;
*/
public static PushPayload buildPushObjectAndroidIosAliasAlert(List<String> userIds,String message){
return PushPayload.newBuilder()
.setPlatform(Platform.android_ios())
.setAudience(Audience.alias(userIds))//推送多個;
.setNotification(Notification.newBuilder()
.addPlatformNotification(AndroidNotification.newBuilder()
.addExtra("type", "infomation")
.setAlert(message)
.build())
.addPlatformNotification(IosNotification.newBuilder().setSound("callu")
.addExtra("type", "infomation")
.setAlert(message)
.build())
.build())
.setOptions(Options.newBuilder()
.setApnsProduction(false)//true-推送生產環境 false-推送開發環境(測試使用參數)
.setTimeToLive(90)//消息在JPush服務器的失效時間(測試使用參數)
.build())
.build();
}
/**
* 推送單個人;
*/
public static PushPayload buildPushObjectAndroidIosAliasAlert(String userId,String message){
return PushPayload.newBuilder()
.setPlatform(Platform.android_ios())
.setAudience(Audience.alias(userId))//推送單個;
.setNotification(Notification.newBuilder()
.addPlatformNotification(AndroidNotification.newBuilder()
.addExtra("type", "infomation")
.setAlert(message)
.build())
.addPlatformNotification(IosNotification.newBuilder().setSound("callu")
.addExtra("type", "infomation")
.setAlert(message)
.build())
.build())
.setOptions(Options.newBuilder()
.setApnsProduction(false)//true-推送生產環境 false-推送開發環境(測試使用參數)
.setTimeToLive(90)//消息在JPush服務器的失效時間(測試使用參數)
.build())
.build();
}
/**
* 推送所有
*/
public static PushResult pushAlias(String alert){
ClientConfig clientConfig = ClientConfig.getInstance();
JPushClient jpushClient = new JPushClient(MASTERSECRET, APPKEY, null, clientConfig);
PushPayload payload = buildPushObjectAndroidIosAllAlert(alert);
try {
return jpushClient.sendPush(payload);
} catch (APIConnectionException e) {
log.error("Connection error. Should retry later. ", e);
return null;
} catch (APIRequestException e) {
log.error("Error response from JPush server. Should review and fix it. ", e);
log.info("HTTP Status: " + e.getStatus());
log.info("Error Code: " + e.getErrorCode());
log.info("Error Message: " + e.getErrorMessage());
log.info("Msg ID: " + e.getMsgId());
return null;
}
}
/**
* 推送 指定用戶集合;
*/
public static PushResult pushAlias(List<String> userIds,String alert){
ClientConfig clientConfig = ClientConfig.getInstance();
JPushClient jpushClient = new JPushClient(MASTERSECRET, APPKEY, null, clientConfig);
PushPayload payload = buildPushObjectAndroidIosAliasAlert(userIds,alert);
try {
return jpushClient.sendPush(payload);
} catch (APIConnectionException e) {
log.error("Connection error. Should retry later. ", e);
return null;
} catch (APIRequestException e) {
log.error("Error response from JPush server. Should review and fix it. ", e);
log.info("HTTP Status: " + e.getStatus());
log.info("Error Code: " + e.getErrorCode());
log.info("Error Message: " + e.getErrorMessage());
log.info("Msg ID: " + e.getMsgId());
return null;
}
}
/**
* 推送單個人;
*/
public static PushResult pushAlias(String userId,String alert){
ClientConfig clientConfig = ClientConfig.getInstance();
JPushClient jpushClient = new JPushClient(MASTERSECRET, APPKEY, null, clientConfig);
PushPayload payload = buildPushObjectAndroidIosAliasAlert(userId,alert);
try {
return jpushClient.sendPush(payload);
} catch (APIConnectionException e) {
log.error("Connection error. Should retry later. ", e);
return null;
} catch (APIRequestException e) {
log.error("Error response from JPush server. Should review and fix it. ", e);
log.info("HTTP Status: " + e.getStatus());
log.info("Error Code: " + e.getErrorCode());
log.info("Error Message: " + e.getErrorMessage());
log.info("Msg ID: " + e.getMsgId());
return null;
}
}
}
2. 業務通知
說明:有些事情,我們希望用戶打開APP就知道某些事情,這個時候我們就需要做一個首頁通知機制,由於這種機制是用戶主動接受,因此,我們需要進行系統設計與架構
2.1 存儲用戶的推送消息。
2.2 統計那些用戶看了與沒看。
數據庫設計如下:
CREATE TABLE `buyer_notice` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自動增加ID', `buyer_id` bigint(20) DEFAULT NULL COMMENT '買家ID', `content` varchar(60) DEFAULT NULL COMMENT '內容', `status` int(11) DEFAULT NULL COMMENT '狀態,0為未讀,1為已讀', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `update_time` datetime DEFAULT NULL COMMENT '最后更新時間,已讀時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=262 DEFAULT CHARSET=utf8 COMMENT='買家通知';
說明:字段相對比較簡單,就是買家ID,內容,讀取狀態等等,
業務邏輯為:當用戶進入系統,我們系統代碼查詢業務邏輯的時候,也查詢 下這個表是否存在通知,如果已經有的,就不用彈窗,沒有就彈窗,強迫用戶選擇已讀或者未讀。
相對而言業務比較簡單
/**
* 買家進入首頁,看到的通知
*/
@RestController
@RequestMapping("/buyer")
public class NoticeController extends BaseController{
private static final Logger logger = LoggerFactory.getLogger(MyController.class);
public static final String CONTENT="平台下單時間調整為上午10:00到晚上23:59";
@Autowired
private NoticeService noticeService;
/**
* 查詢消息
*/
@RequestMapping(value = "/notice/index", method = { RequestMethod.GET, RequestMethod.POST })
public JsonResult noticeIndex(HttpServletRequest request, HttpServletResponse response,Long buyerId){
try
{
if(buyerId==null)
{
return new JsonResult(JsonResultCode.FAILURE, "請求參數有誤,請重新輸入","");
}
Notice notice=this.noticeService.getNoticeByBuyerId(buyerId);
if(notice==null)
{
int result=this.noticeService.insertNotice(buyerId, CONTENT);
if(result>0)
{
notice=this.noticeService.getNoticeByBuyerId(buyerId);
}
}
return new JsonResult(JsonResultCode.SUCCESS, "查詢信息成功", notice);
}catch(Exception ex){
logger.error("[NoticeController][noticeIndex] exception :",ex);
return new JsonResult(JsonResultCode.FAILURE, "系統錯誤,請稍后重試","");
}
}
/**
* 更新消息
*/
@RequestMapping(value = "/notice/update", method = { RequestMethod.GET, RequestMethod.POST })
public JsonResult noticeUpdate(HttpServletRequest request, HttpServletResponse response,Long buyerId){
try
{
if(buyerId==null)
{
return new JsonResult(JsonResultCode.FAILURE, "請求參數有誤,請重新輸入","");
}
int result=this.noticeService.updateBuyerNotice(buyerId);
if(result>0)
{
return new JsonResult(JsonResultCode.SUCCESS, "更新成功","");
}else
{
return new JsonResult(JsonResultCode.FAILURE, "更新失敗","");
}
}catch(Exception ex){
logger.error("[NoticeController][noticeUpdate] exception :",ex);
return new JsonResult(JsonResultCode.FAILURE, "系統錯誤,請稍后重試","");
}
}
}
3. 短信通知模塊的設計
說明:市面上短信供應商很多,可能大家就是關注一個價格與及時性的問題,目前我們找的一個稍微便宜點的供應商:http://api.sms.cn/
內容其實就是短信的發送而言。
接口文檔很簡單:
參數名 參數字段 參數說明
ac 接口功能 接口功能,傳入值請填寫 send
format 返回格式 可選項,有三參數值:json,xml,txt 默認json格式
uid 用戶賬號 登錄名
pwd 用戶密碼 32位MD5加密md5(密碼+uid)
如登錄密碼是:123123 ,uid是:test;
pwd=md5(123123test)
pwd=b9887c5ebb23ebb294acab183ecf0769
encode 字符編碼 可選項,默認接收數據是UTF-8編碼,如提交的是GBK編碼字符,需要添加參數 encode=gbk
mobile 接收號碼 同時發送給多個號碼時,號碼之間用英文半角逗號分隔(,);小靈通需加區號
如:13972827282,13072827282
mobileids 消息編號 可選項
該參數用於發送短信收取狀態報告用,格式為消息編號+逗號;與接收號碼一一對應,可以重復出現多次。
消息編號:全部由數字組成接收狀態報告的時候用到,該消息編號的格式可就為目標號碼+當前時間戳整數,精確到毫秒,確保唯一性。供收取狀態報告用 如: 1590049111112869461937;
content 短信內容 變量模板發送,傳參規則{"key":"value"}JSON格式,key的名字須和申請模板中的變量名一致,多個變量之間以逗號隔開。示例:針對模板“短信驗證碼{$code},您正在進行{$product}身份驗證,請在10分鍾內完成操作!”,傳參時需傳入{"code":"352333","product":"電商平台"}
template 模板短信ID 發送變量模板短信時需要填寫對應的模板ID號,進入平台-》短信設置-》模板管理
對此,我們如何進行業務研究與處理呢?
1. 短信驗證碼的長度與算法。
2. 代碼的模板進行封裝。
3. 短信工具類的使用方便
1. 短信驗證碼生成算法:
import org.apache.commons.lang3.RandomStringUtils;
/**
* 短信驗證碼
*
*/
public final class SmsCode {
/**
* 默認產生的驗證碼數目
*/
private static int DEFAULT_NUMBER = 6;
/**
* 產生的隨機號碼數目
*
* @param number
* @return
*/
public static String createRandomCode(int number) {
int num = number <= 3 ? DEFAULT_NUMBER : number;
return RandomStringUtils.randomNumeric(num);
}
}
簡單粗暴的解決問題:
2. 短信內容的封裝:
/***
* 短信消息對象
*/
public class SmsMessage
{
/**
* 賬號,目前就是手機號碼,采用的是手機號碼登陸
*/
private String account;
/*
* 產生的驗證碼
*/
private String code;
/**
* 對應的短信模板,目前短信驗證碼是401730
*/
private String template;
public SmsMessage() {
super();
}
public SmsMessage(String account, String code, String template) {
super();
this.account = account;
this.code = code;
this.template = template;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
@Override
public String toString() {
return "{\"username\":\""+account+"\",\"code\":\""+code+"\"}";
}
3.短信發送結果的封裝:
/**
* 短信發送結果
*/
public class SmsResult implements java.io.Serializable{
private static final long serialVersionUID = 1L;
private boolean success=false;
private String message;
public SmsResult() {
super();
}
public SmsResult(String message)
{
super();
this.success=false;
this.message=message;
}
public SmsResult(boolean success, String message) {
super();
this.success = success;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("SmsResult [success=");
builder.append(success);
builder.append(", message=");
builder.append(message);
builder.append("]");
return builder.toString();
}
}
4. 短信發送工具類的封裝
/**
* 短信工具
*/
@Component
public class SmsUtil {
private static final Logger logger=LoggerFactory.getLogger(SmsUtil.class);
@Value("#{applicationProperties['environment']}")
private String environment;
/**
* 默認編碼的格式
*/
private static final String CHARSET="GBK";
/**
* 請求的網關接口
*/
private static final String URL = "http://api.sms.cn/sms/";
public boolean sendSms(SmsMessage smsMessage)
{
boolean result=true;
logger.debug("[SmsUtil]當前的運行環境為:"+environment);
//開發環境就直接返回成功
if("dev".equalsIgnoreCase(environment))
{
return result;
}
Map<String, String> map=new HashMap<String,String>();
map.put("ac","send");
map.put("uid","");
map.put("pwd","");
map.put("template",smsMessage.getTemplate());
map.put("mobile",smsMessage.getAccount());
map.put("content",smsMessage.toString());
try
{
String responseContent=HttpClientUtil.getInstance().sendHttpPost(URL, map,CHARSET);
logger.info("SmsUtil.sendSms.responseContent:" + responseContent);
JSONObject json = JSONObject.fromObject(responseContent);
logger.info("SmsUtil.sendSms.json:" + json);
String stat=json.getString("stat");
if(!"100".equalsIgnoreCase(stat))
{
result=false;
}
}catch(Exception ex)
{
result=false;
logger.error("[SmsUtil][sendSms] exception:",ex);
}
return result;
}
}
補充說明;其實我可以用一個工具類來解決所有問題,為什么我沒采用呢?
1 。代碼耦合度高,不變管理與擴展.(業務分析,其實就是三種情況,1,發送,2,內容,3,返回結果)
2. 我采用代碼拆分,一個類只做一件事情,幾個類分別協同開發,達到最高程度的解耦,代碼清晰,維護度高。
總結:關於這個消息的推送,我們也可以采用微信來通知,比如:你的訂單到了,你的訂單已經接受,錢已經到賬等等,還有業務線上的推送等等,我這邊
只是根據實際的運行情況,起到一個拋磚引玉的作用,目的只有一個原因,讓大家有獨立思考與分析業務的能力。
