手機(jī)上形形色色的app會(huì)給我們推送各種消息取逾,那么一條消息的推送是如何實(shí)現(xiàn)的呢逆趣?下面讓我從某個(gè)app的python后端開(kāi)發(fā)的角度來(lái)解析一下荆几。
一吓妆、背景
- 推送的消息包括兩大類:運(yùn)營(yíng)人員手動(dòng)編輯、推送的公告吨铸、活動(dòng)等行拢,與用戶行為(比如交易)相關(guān)的通知,這部分的消息是在代碼執(zhí)行過(guò)程中自動(dòng)生成诞吱、推送舟奠。
- 涉及到三個(gè)服務(wù)器:A是生產(chǎn)服務(wù)器,部署Django房维,負(fù)責(zé)與app交互沼瘫,采用前后端分離開(kāi)發(fā),能拿到用戶數(shù)據(jù)咙俩;B是公共接口服務(wù)器耿戚,部署的Flask湿故,拿不到用戶數(shù)據(jù),但儲(chǔ)存消息數(shù)據(jù)膜蛔;C是提供給運(yùn)營(yíng)人員使用的服務(wù)器坛猪,部署Flask,通過(guò)指令訪問(wèn)B的數(shù)據(jù)庫(kù)皂股。
- 在具體的消息推送步驟上采用第三方的小米推送服務(wù)
二墅茉、 小米推送的實(shí)現(xiàn)
小米推送服務(wù)的具體文檔可以自行查閱
- 它定義了一套完整的消息格式,同時(shí)也支持“透?jìng)鳌?/strong>模式呜呐,這就方便開(kāi)發(fā)者自己約定消息的內(nèi)容就斤,我們這次就采用透?jìng)鳎?/li>
- 它同時(shí)支持渠道訂閱和指定alias兩種推送方式,前者一對(duì)多蘑辑、后者一對(duì)一战转,兩者的接口不一樣;
- 在開(kāi)發(fā)前需要先注冊(cè)app的包名以躯,拿到secret存儲(chǔ)起來(lái),執(zhí)行推送時(shí)需要使用與包名對(duì)應(yīng)的secret啄踊,否則不會(huì)成功忧设;
- IOS與Android的參數(shù)是不一樣;
假設(shè)包名為com.theapp.ios颠通、com.theapp.android址晕,topic表示訂閱主題,msg表示消息內(nèi)容顿锰,username用來(lái)生成alias(別名)谨垃,secret配置在字典MIPUSH_SECRETS中,則主要的實(shí)現(xiàn)代碼如下
import hashlib
import json
import requests
def mipush(pkg, msg, username=None, topic=None):
secret = MIPUSH_SECRETS[pkg]
if 'ios' in pkg:
data = {
'extra.content-available': 1,
'extra.payload': json.dumps(msg),
'time_to_live': 3600 * 1000,
}
else:
data = {
'pass_through': 1,
'payload': json.dumps(msg),
'time_to_live': 3600 * 1000,
}
if username:
api_url = "https://api.xmpush.xiaomi.com/v2/message/alias"
user_code = hashlib.md5(username.encode('utf-8')).hexdigest()
data['alias'] = user_code
else:
api_url = "https://api.xmpush.xiaomi.com/v2/message/topic"
data['topic'] = topic
resp = requests.post(api_url, headers={"Authorization": "key=%s" % secret}, data=data)
return resp
三硼控、消息模型
建立消息的數(shù)據(jù)模型時(shí)需要考慮的問(wèn)題有:
- (1. 與app約定好透?jìng)飨⒌膬?nèi)容格式
- (2. 考慮內(nèi)容格式與app版本的解耦性
- (3. H5頁(yè)面中消息列表及內(nèi)容的展示
- (4. 運(yùn)營(yíng)消息與用戶消息推送流程的差異以及消息存儲(chǔ)與未讀處理的取舍
針對(duì)(1)和(2)刘陶,我們已約定公共消息體加上特定類別的方法來(lái)處理,即消息體中包括所有可能用到的字段牢撼,然后再消息體外部再給出一個(gè)消息類別的字段匙隔,用來(lái)和app約定彈窗中的格式;當(dāng)app更新時(shí)熏版,如果不想支持以前的或者想識(shí)別新的消息類別纷责,就可以依據(jù)該字段做過(guò)濾處理。
同時(shí)針對(duì)(3)所展示都是消息體的具體內(nèi)容撼短,可以一并塞進(jìn)去再膳,但同時(shí)要通過(guò)另一個(gè)字段來(lái)與H5約定消息內(nèi)容的展示種類。
得到消息體模型如下:
from mongoengine import *
class MsgBody(EmbeddedDocument):
title = StringField(required=True) # 標(biāo)題
content = StringField(required=True) # 內(nèi)容
img = StringField() # 圖片
url= StringField() # 點(diǎn)擊跳轉(zhuǎn)
remark = DictField() # 其他擴(kuò)展
針對(duì)(4)曲横,有兩種方案:
一喂柒、對(duì)兩個(gè)業(yè)務(wù)流程定義不同數(shù)據(jù)模型,各自開(kāi)發(fā)一套接口,推送和存儲(chǔ)都是獨(dú)立的胳喷。好處在于運(yùn)營(yíng)消息就可以與用戶的id解耦湃番,一條運(yùn)營(yíng)消息可以對(duì)應(yīng)所有的用戶,而用戶消息則只能對(duì)應(yīng)的用戶相關(guān)吭露;推送時(shí)吠撮,一個(gè)采用訂閱方式,一個(gè)采用別名方式讲竿。不利之處則是泥兰,不太方便處理運(yùn)營(yíng)消息的未讀狀態(tài)。
PS:此處的推送訂閱也是考慮過(guò)解耦的题禀,也就是app運(yùn)行后鞋诗,自動(dòng)向推送服務(wù)器訂閱開(kāi)發(fā)者定義好的主題。
當(dāng)用戶不想彈窗迈嘹,關(guān)閉推送時(shí)削彬,并沒(méi)有在推送服務(wù)器上取消訂閱,只是在app上修改了標(biāo)記;
當(dāng)app接收到推送秀仲,發(fā)現(xiàn)標(biāo)記為關(guān)閉時(shí)融痛,不進(jìn)行彈窗操作,對(duì)用戶沒(méi)影響神僵,但實(shí)際也推送了雁刷。
這樣想更換第三方推送服務(wù)時(shí),比較方便保礼。
二沛励、兩者數(shù)據(jù)模型統(tǒng)一,只是通過(guò)字段區(qū)別炮障,公共接口只需要開(kāi)發(fā)一套目派,都是用別名推送方式。好處在于節(jié)省代碼(或者工作量)铝阐,實(shí)現(xiàn)了兼容性址貌,運(yùn)營(yíng)消息也可以統(tǒng)計(jì)未讀了。不好的地方在于徘键,一條運(yùn)營(yíng)消息可能需要為每個(gè)用戶存一條記錄练对,當(dāng)用戶量大的時(shí)候,這個(gè)開(kāi)銷有點(diǎn)大吹害。
方案一的實(shí)現(xiàn)如下:
class OperateMsg(DynamicDocument):
meta = {'db_alias': 'test',
'indexes': [略]}
msg_tag = StringField(required=True) # 與H5約定的標(biāo)簽
msg_body = EmbeddedDocumentField(MsgBody)
msg_type = StringField(required=True) # 與app約定的消息類別螟凭,app根據(jù)此字段來(lái)決定是否識(shí)別消息、從msg_body 讀取哪些字段
enable = BooleanField(default=False) # 用戶可見(jiàn)
class UserMsg(DynamicDocument):
meta = {'db_alias': 'test',
'indexes': [略]}
user_id = IntField(required=True) # 用戶id
msg_tag = StringField(required=True)
msg_type = StringField(required=True)
msg_body = EmbeddedDocumentField(MsgBody)
unread = BooleanField(default=True) # 未讀標(biāo)記
方案二的實(shí)現(xiàn)如下:
class Msg(DynamicDocument):
meta = {'db_alias': 'test',
'indexes': [略]}
user_id = IntField(required=True) # 用戶id
msg_tag = StringField(required=True) # 與H5約定的標(biāo)簽
msg_body = EmbeddedDocumentField(MsgBody)
msg_type = StringField(required=True) # 與app約定的消息類別它呀,app根據(jù)此字段來(lái)決定是否識(shí)別消息螺男、從msg_body 讀取哪些字段
enable = BooleanField(default=False) # 用戶可見(jiàn)
unread = BooleanField(default=True) # 未讀標(biāo)記
最終我決定使用方案一
四棒厘、接口設(shè)計(jì)
約定A、B下隧、C的映射地址為分別為https://test_A.com奢人、https://test_B.com、https://test_C.com
為了A和C能訪問(wèn)到B的公共接口淆院,在B上的接口要進(jìn)行允許跨域訪問(wèn)的處理
1. server B上的公共接口
1) 運(yùn)營(yíng)消息
主要是需要提供創(chuàng)建何乎、查詢、更新土辩、發(fā)布四個(gè)操作接口來(lái)支持運(yùn)營(yíng)的發(fā)布流程(創(chuàng)建消息支救、查詢檢查一下、更新enable=True上線拷淘、執(zhí)行推送);其次各墨,還要考慮提供給H5頁(yè)面中的消息列表和概況展示的需求。
接口設(shè)計(jì)如下启涯,具體的業(yè)務(wù)實(shí)現(xiàn)贬堵、接口訪問(wèn)的安全性處理以及允許跨域訪問(wèn)的處理就不給出了:
import flask
oms_api = flask.blueprints.Blueprint('oms_api', __name__)
@oms_api.route('/create', methods=['POST'])
def oms_create():
pass
@oms_api.route('/update', methods=['POST'])
def oms_update():
pass
@oms_api.route('/query', methods=['POST'])
def oms_query():
pass
@oms_api.route('/push', methods=['POST'])
def oms_push():
# mipush(pkg, msg, topic) # 訂閱推送
pass
@oms_api.route('/list', methods=['POST'])
def oms_list():
pass
@oms_api.route('/survey', methods=['POST'])
def oms_survey():
pass
2)用戶消息
由于用戶消息是程序創(chuàng)建和推送,所以二者應(yīng)該合并為一個(gè)接口结洼,同時(shí)還要支持H5未讀查詢扁瓢、已讀標(biāo)記、概況和列表展示需求补君。
import flask
ums_api= flask.blueprints.Blueprint('ums_api', __name__)
@ums_api.route('/new', methods=['POST'])
def create_and_push_ums():
# mipush(pkg, msg, username): # 別名推送
pass
@ums_api.route('/read', methods=['POST'])
def read_ums():
pass
@ums_api.route('/unread_count', methods=['POST'])
def unread_count():
pass
@ums_api.route('/survey', methods=['POST'])
def ums_survey():
pass
@pri_ums_api.route('/list', methods=['POST'])
def ums_list():
pass
假設(shè)消息的模塊名為message,上述接口代碼在message/views/api.py中實(shí)現(xiàn)昧互,為了外部能訪問(wèn)到這些接口挽铁,還需要在Flask項(xiàng)目根目錄下的app.py中進(jìn)行藍(lán)圖(blueprint)的注冊(cè)才算是完成了路由地址的映射(即接口的url地址)
from message.views.api import oms_api, ums_api
app.register_blueprint(oms_api, url_prefix='/oms')
app.register_blueprint(ums_api, url_prefix='/ums')
這樣運(yùn)營(yíng)消息接口的url地址就是https://test_B.com/oms/ **
相應(yīng)用戶消息接口的url地址就是https://test_B.com/ums/ **
2. server C上的操作
也就是給運(yùn)營(yíng)人員提供創(chuàng)建、更新敞掘、查詢叽掘、發(fā)布四個(gè)操作的接口,不過(guò)需要用到request來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求玖雁,以創(chuàng)建為例:
import request
def create_oms(**data):
resp = requests.post('https://test_B.com/oms/create', json=data)
3. server A
在A上的接口分兩種更扁,一種是與H5交互的接口,包括通過(guò)mst_tag查詢具體消息列表內(nèi)容赫冬、查詢整體消息概況(oms+ums)浓镜、查詢ums未讀、標(biāo)記ums已讀劲厌;還有一種接口就是ums的創(chuàng)建推送接口膛薛,這個(gè)只需要封裝成功能函數(shù)提供調(diào)用即可,有必要的話需要異步處理(比如作為生產(chǎn)者丟給AWS去消費(fèi))补鼻。
這些接口都是和C上的一樣哄啄,首先封裝一下對(duì)B上公共接口的調(diào)用雅任,同時(shí)自身接口提供調(diào)用。
不過(guò)由于server A上部署的是Django咨跌,接口url的路由實(shí)現(xiàn)就與Flask的實(shí)現(xiàn)不太一樣沪么,以未讀消息統(tǒng)計(jì)接口為例吧(視覺(jué)效果就是逼死處女座的那個(gè)圖標(biāo)右上角的未讀小紅點(diǎn)或者統(tǒng)計(jì)數(shù)字)
同樣假設(shè)模塊名為message,接口在message/views/api.py中實(shí)現(xiàn)锌半,未讀統(tǒng)計(jì)接口的名稱為ums_unread_count_api禽车。
首先在message目錄下的api_urls.py中進(jìn)行映射
from django.conf.urls import url
import message.views.api
urlpatterns = [
url(r'unread_count$', message.views.api.ums_unread_count_api),
# To Add
]
然后在Django根目錄下的urls.py中進(jìn)行操作
from django.conf.urls import include, url
urlpatterns = [
url(r'^api/msg/', include('message.api_urls')),
# To Add
]
就可以通過(guò)完整的接口地址 https://test_A.com/api/msg/unread_count 來(lái)獲取數(shù)據(jù)了