Java開源生鮮電商平臺-通知模塊設計與架構(源碼可下載) 說明:對於一個生鮮的B2B平臺而言,通知對於我們實際的運營而言來講分為三種方式: 1. 消息推送:(採用極光推送) 2. 主頁彈窗通知。(比如:現在有什麼新的活動,有什麼新的優惠等等) 3. 簡訊通知.(對於簡訊通知,這個大家很熟悉,我們就 ...
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. 我採用代碼拆分,一個類只做一件事情,幾個類分別協同開發,達到最高程度的解耦,代碼清晰,維護度高。
總結:關於這個消息的推送,我們也可以採用微信來通知,比如:你的訂單到了,你的訂單已經接受,錢已經到賬等等,還有業務線上的推送等等,我這邊
只是根據實際的運行情況,起到一個拋磚引玉的作用,目的只有一個原因,讓大家有獨立思考與分析業務的能力。
Java開源生鮮電商平臺-通知模塊設計與架構(源碼可下載),如果需要下載的話,可以在我的github下麵進行下載。