(原創(chuàng))一個(gè)app消息推送機(jī)制的設(shè)計(jì)與實(shí)現(xiàn)

手機(jī)上形形色色的app會(huì)給我們推送各種消息取逾,那么一條消息的推送是如何實(shí)現(xiàn)的呢逆趣?下面讓我從某個(gè)app的python后端開(kāi)發(fā)的角度來(lái)解析一下荆几。

一吓妆、背景

  1. 推送的消息包括兩大類:運(yùn)營(yíng)人員手動(dòng)編輯、推送的公告吨铸、活動(dòng)等行拢,與用戶行為(比如交易)相關(guān)的通知,這部分的消息是在代碼執(zhí)行過(guò)程中自動(dòng)生成诞吱、推送舟奠。
  2. 涉及到三個(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ù)皂股。
  3. 在具體的消息推送步驟上采用第三方的小米推送服務(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.comhttps://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ù)了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拳喻,隨后出現(xiàn)的幾起案子哭当,更是在濱河造成了極大的恐慌,老刑警劉巖冗澈,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钦勘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡亚亲,警方通過(guò)查閱死者的電腦和手機(jī)彻采,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)捌归,“玉大人肛响,你說(shuō)我怎么就攤上這事∠鳎” “怎么了特笋?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)巾兆。 經(jīng)常有香客問(wèn)我猎物,道長(zhǎng),這世上最難降的妖魔是什么角塑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任蔫磨,我火速辦了婚禮,結(jié)果婚禮上圃伶,老公的妹妹穿的比我還像新娘堤如。我一直安慰自己,他們只是感情好窒朋,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布搀罢。 她就那樣靜靜地躺著,像睡著了一般侥猩。 火紅的嫁衣襯著肌膚如雪魄揉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,246評(píng)論 1 308
  • 那天拭宁,我揣著相機(jī)與錄音洛退,去河邊找鬼瓣俯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛兵怯,可吹牛的內(nèi)容都是我干的彩匕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼媒区,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼驼仪!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起袜漩,我...
    開(kāi)封第一講書(shū)人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤绪爸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后宙攻,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體奠货,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年座掘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了递惋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溢陪,死狀恐怖萍虽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情形真,我是刑警寧澤杉编,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站咆霜,受9級(jí)特大地震影響王财,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜裕便,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望见咒。 院中可真熱鬧偿衰,春花似錦、人聲如沸改览。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宝当。三九已至视事,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庆揩,已是汗流浹背俐东。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工跌穗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虏辫。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓蚌吸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砌庄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子羹唠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容