1. 先对齐一个核心认知:在线推送和离线推送根本不是一回事
做App开发这几年,推送功能我至少接过七八次,每次排期表上都写着"简单,半天搞定",最后没有一次不加班的。尤其是UniPush 2.0这类把在线推送和离线推送打包在一起的方案,表面看就是个SDK接入,真正跑起来全是细节:证书、包名、厂商密钥、通知渠道、离线消息时效,哪个环节断链,消息就静悄悄丢了,用户那边毫无感知,你这边还在纠结"为什么送达率这么低"。
开始动工之前,我觉得有必要先把一个概念掰扯清楚:在线推送和离线推送,本质上走的是完全不同的两条路。
1.1 App活着的时候,推送反而最简单
当App进程活着、用户在前台或者处于后台但没被杀掉的时候,UniPush 2.0走的是个推自建的长连接通道。App启动时会和推送服务器建立一条TCP长连接,服务器有消息就直接往这条连接上怼,客户端收到后回调到业务层,由你决定是弹通知栏还是走自己的逻辑。
这条链路的特点是快、可控、不受手机厂商限制。只要是前台活着的App,消息基本都是秒达,延迟在几百毫秒级别,体验非常好。而且这时候你可以发透传消息——不弹通知栏,纯粹把数据丢给App内部处理,比如刷新页面、同步状态、触发某个业务动作。这是在线推送独有的能力。
但问题就在于"App活着"这个前提。移动端生态发展到今天,Android系统对后台进程的限制已经收得非常紧,iOS更是从系统层面就掐死了App在后台长时间运行的可能。用户上滑划掉应用、手机厂商的省电策略杀掉进程、系统内存回收……任何一个动作都会让你的长连接断开。长连接一断,在线推送就失效了,这时候必须有人接盘。
1.2 离线推送的真正难点:厂商不让你活着
离线推送的本质,是App进程已经死了,你的长连接已经断开了,但消息还是得让用户看到。怎么做到?答案只有一个:借系统的通道。
Android这边,小米有小米推送、华为有华为推送、OPPO有OPPO推送、vivo有vivo推送、荣耀有荣耀推送,这些统称厂商通道。它们由手机厂商自己维护,属于系统级服务,优先级极高,即使你的App进程被杀掉,厂商的推送服务也能把消息送到系统通知栏。iOS那边就更简单粗暴了,没有厂商之分,只有苹果自家的APNs,走的是苹果推送服务。
所以离线推送的实际情况是:你每台手机都要对接对应的厂商通道,小米的用小米SDK,华为的用华为SDK……如果全部自己接,一个App接五个厂商通道,光适配和联调就得掉一层皮。而且各大厂商对推送的审核要求还不一样,有的要求你申请自分类权益,有的要求App在应用市场达到一定等级,不然不给开通推送权限。
1.3 UniPush 2.0 把三条通道拧成一条API
UniPush 2.0解决的正是这个"多通道地狱"问题。它是DCloud联合个推推出的基于uni-app生态的推送服务,在底层把个推长连接、Android各大厂商通道、iOS的APNs全部封装好,对外只暴露一套API。你在服务端调用一个接口发消息,UniPush会自动判断用户当前在线状态:
- 在线:走长连接,直接到达App;
- 离线且是Android设备:自动路由到对应厂商通道,通过系统通知栏展示;
- 离线且是iOS设备:走APNs;
- 厂商通道没配置或不可用:消息进入离线存储,在有效期内用户联网后补偿下发。
这个设计对于uni-app项目来说几乎是量身定做的。因为你不需要再单独集成个推SDK,也不需要关心各厂商SDK的版本冲突问题,只需要在manifest.json里勾选Push模块,把厂商参数填进开发者中心,剩下的交给封装层处理。
用一句话概括UniPush 2.0的定位:**它是把在线推送的实时性和离线推送的触达性缝在一起的一层胶水,让你不用关心消息到底走哪条路。
2. UniPush 2.0 的消息流转架构:谁在发、走哪条路、怎么落地
2.1 三条通道的分工与降级逻辑
理解了在线和离线的本质区别后,我们再来看UniPush 2.0的整体消息流转架构,这样后续排查问题的时候,你能迅速定位是哪条通道掉链子了。
第一条通道是个推长连接。这条通道覆盖的是"App在线"场景。它的优势是支持透传消息,可以做静默更新、业务数据同步等操作,而且不受厂商通知栏权限的影响。缺点是只要App进程死了,这条通道就断了。
第二条通道是Android厂商通道。这条通道覆盖的是"Android App离线"场景。它的特点是即使App被杀,系统级推送服务依然活着,消息能稳定到达通知栏。缺点是只能发通知消息,不支持纯静默透传,而且各厂商对推送类型有限制(比如小米要求新闻资讯类App申请自分类权益,否则只能发通知消息,不能发应用内消息)。
第三条通道是iOS APNs。苹果生态只认这一条,消息从你的服务端发到UniPush,UniPush转投到APNs,最后落到用户手机通知栏。它的限制是要配置推送证书(开发证书和发布证书),而且用户可以在系统设置里单独关闭某个App的通知权限。
UniPush 2.0的降级逻辑大概是这样的:发消息时带上目标用户的cid(UniPush客户端标识),服务器先查一下这个cid对应的长连接是否在线。在线就走长连接;不在线就判断设备类型,Android设备查这个cid有没有配置厂商通道,配置了就走厂商通道,没配置或配置失效就走离线存储(Android也支持走自有服务离线存),iOS设备直接走APNs。
2.2 通知消息和透传消息,选择决定命运
在UniPush 2.0里,消息分为两大类,门道很深。
通知消息就是用户能在通知栏看到的那个:有标题、有内容、带个图标,点击后可以唤起App或跳转到指定页面。这类消息不管是长连接通道还是厂商通道都能发,是所有离线推送的基本盘。
透传消息是只把数据传给App、不经过通知栏的消息,App在onPushMessage回调里自己处理。这类消息的价值在于灵活,可以做静默操作。但注意,透传消息在离线场景下基本是发不出去的。厂商通道的侧重点就是通知栏展示,OPPO和vivo这些厂商对透传消息的限制非常严格,很多情况下根本不给下发。所以设计消息策略的时候,必须有一个清醒的认知:如果你想让离线用户看到内容,请用通知消息;如果你只需要在线用户进行数据同步,再用透传消息。不要指望一条透传消息能搞定离线触达,那是不现实的。
2.3 离线消息的"有效期"是你必须算清楚的一笔账
UniPush 2.0的离线存储不是无限期的。你在服务端发消息时可以指定expireTime,意思是这条消息在多少秒内有效。用户在这段时间内联网,消息会补偿下发;超过这个时间,消息就丢弃了。
实际项目里怎么设这个值?我见过不少团队随便填个值,结果要么消息太晚到达没有意义,要么过期太短导致用户下午才打开App,上午的推送已经丢了。我的经验是分场景:时效性强的运营消息(比如限时活动、验证码类通知),建议设30分钟到2小时;普通的交易类通知(发货、到账、退款),建议设24到72小时。这类消息即使晚了一点,用户看到仍然有业务价值。这个参数直接影响你的真实触达率,做数据统计的时候要把它算进去,不然你统计出来的"送达率"会低得离谱——不是通道问题,是你自己把消息设过期了。
3. 接入实操:开发者中心参数、证书包名和客户端API一次讲透
3.1 开通服务与应用信息配置
第一步是在DCloud开发者中心操作。登录dev.dcloud.net.cn,找到你的应用,在"uni-push 2.0"下面点击开通。这里会获取到三个核心凭证:appId、appKey、masterSecret。三者分工如下:
appId:应用唯一标识,服务端发消息时用;appKey:接口调用身份标识,等同于你的应用在UniPush体系里的"用户名";masterSecret:签名密钥,等同于"密码",绝不能暴露到客户端代码里,只保存在服务端。
开通之后不要急着写代码,先把"Android厂商推送设置"这块配好。表格里逐个填:小米的appId/appKey/appSecret,华为的appId/appSecret,OPPO的appKey/appSecret/masterSecret,vivo的appId/appKey/appSecret,荣耀的appId/appSecret。
去各厂商开放平台申请这些参数的时候,有一个特别容易踩的坑:应用包名必须和你App的Android包名完全一致,包括大小写和点号。厂商平台注册的是com.example.app,你本地打包的包名是com.example.app2,那离线推送百分百收不到。签名证书(SHA1/SHA256指纹)也要和正式发布包一致。这些平台审核一般一两天,所以建议在项目排期早期就去申请,不要等开发完了再弄。
iOS端则是到Apple Developer后台创建推送证书。开发环境用.p12证书,生产环境用.p8密钥证书(推荐p8,因为支持多个应用复用且不会过期)。把证书内容传到开发者中心的iOS推送配置里,然后把Bundle Identifier核对清楚。
3.2 manifest.json里的Push模块配置
在uni-app项目中,打开manifest.json,进入"App模块配置",勾选Push(消息推送),选择uniPush 2.0版本。同时记得配置推送图标,这个图标会显示在通知栏消息的左侧,不配置的话有可能显示成默认的白色方块,非常影响观感。
实际配置类似这样:
{ "app": { "distribute": { "sdkConfigs": { "push": { "unipush": { "version": "2.0.0", "icons": { "push": { "url": "/static/push_icon.png" } }, "description": "消息推送" } } } } } }填写url时用绝对路径,放在static目录下最稳妥。配置完后,用HBuilderX重新生成App资源,然后打自定义调试基座或正式包测试。注意:厂商通道离线推送能力只在正式打包或使用自定义基座时生效,标准HBuilderX基座没法验证厂商通道,这是很多新手第一次自测就翻车的地方。
3.3 客户端API:获取cid、监听消息、处理点击
客户端的核心逻辑就三个:拿cid、监听消息、处理点击。先看获取cid:
uni.getPushClientId({ success(res) { const cid = res.cid // cid是这台设备在UniPush体系里的唯一身份标识 // 拿它去请求你自己的服务端,绑定到当前登录用户 uni.request({ url: 'https://api.yourserver.com/user/bind_cid', method: 'POST', data: { cid }, }) }, fail(err) { console.error('获取cid失败', err) } })cid的获取时机建议放在App启动后、用户登录成功后各做一次。用户未登录状态也建议获取cid,这样可以用cid做游客维度的触达;登录后重新绑定,把cid关联到用户ID上。服务端保存这个映射关系,后续发消息就是"给用户ID发消息"而不是"给设备发消息",逻辑清晰很多。
监听消息和点击的回调如下:
uni.onPushMessage((res) => { // res.type: 'click' 表示用户点击了通知栏消息;'message' 表示App收到了透传/通知数据 if (res.type === 'click') { // 点击通知栏消息触发 const payload = res.payload if (payload) { try { const data = JSON.parse(payload) // 根据data里的字段跳转对应页面 uni.navigateTo({ url: data.page }) } catch (e) { console.error('payload解析失败', e) } } } else if (res.type === 'message') { // 透传消息或通知消息的数据到达,此时通知栏可能还没展示 // 可以做业务层的静默处理 } })一个非常重要的经验:onPushMessage回调要在App启动最早的时机注册,最好放在App.vue的onLaunch里。否则用户冷启动App、点击通知栏的时候,如果回调还没注册,type === 'click'的事件你就收不到,跳转逻辑就丢了。这个坑我踩过一次,线上用户反馈"点击推送没反应",排查半天发现是注册时机晚了。
4. 服务端推送逻辑:单推群推、过期策略和送达回执
4.1 cid绑定与用户体系的映射
服务端的第一个任务是维护cid和用户ID的映射关系。推荐表结构里至少要包含:用户ID、cid、平台(android/ios)、最后活跃时间、绑定状态。用户换手机、卸载重装、登录其他账号,都会生成新的cid,所以绑定接口要做成"一个用户对应多条cid"的模型,发消息时遍历该用户所有有效cid。
服务端收到客户端上报的cid后,还需要做一次合法性校验,防止恶意刷接口。简单做法是让客户端带着登录态token,服务端校验通过后再绑定。
4.2 单推和群推的API调用细节
UniPush 2.0的服务端HTTP API调用方式,以单推为例,大致是这样一个流程:组装请求头(包含appKey、timestamp、sign),sign的计算规则是用appKey + timestamp + masterSecret做MD5加密(具体以官方最新文档为准),然后把消息体以JSON格式POST到对应接口。
单推请求的核心参数如下:
| 参数 | 说明 |
|---|---|
appId | 应用标识 |
pushToken | 目标设备的cid |
title | 通知栏标题 |
content | 通知栏内容 |
payload | 自定义JSON字符串,用于客户端跳转 |
forceNotification | true表示强通知消息,false表示静默透传 |
options.channel | Android通知渠道ID |
options.expireTime | 离线消息有效期(秒) |
一条完整的单推请求大概长这样:
curl -X POST 'https://api.unipush.dcloud.net.cn/rest/v3/unicast' \ -H 'Content-Type: application/json' \ -H 'appKey: your-app-key' \ -H 'timestamp: 1700000000000' \ -H 'sign: your-md5-sign' \ -d '{ "appId": "your-app-id", "pushToken": "目标cid", "title": "订单通知", "content": "您的订单已发货,请留意查收", "payload": "{\"page\":\"/pages/order/detail\",\"orderId\":\"20250201\"}", "forceNotification": true, "options": { "channel": "default", "expireTime": 86400 } }'群推接口把pushToken换成tag或调用广播接口,核心逻辑一致。我习惯在服务端封装一层公共方法,把sign生成、请求头拼接、错误码处理统一收敛起来,业务方只需要传{ userId, title, content, payload, expireTime },可读性和维护性都好很多。
4.3 通知渠道(channel)是Android 8.0以后的必修课
options.channel这个参数,很多第一次做Android推送的人会忽略。Android 8.0开始,所有通知必须归属于某个通知渠道(Notification Channel)。渠道由App在代码里创建,比如"默认通知""订单消息""活动促销",每个渠道有独立的优先级、震动、声音设置,用户也可以在系统设置里单独关闭某一个渠道。
UniPush 2.0在集成时会自动创建默认渠道,但如果你想精细控制,比如把活动消息和交易消息分成两个渠道,让用户可以只关促销不关交易提醒,就需要在客户端调用uni.createPushMessage或原生插件提前创建渠道。服务端发消息时指定对应的options.channel,消息就会归到该渠道下展示。如果指定的渠道不存在,部分厂商会直接丢弃消息,所以在发消息前要确认渠道ID已经在客户端创建过。
4.4 回执、统计和"是否真的送达"的验证方式
推送发出去了,不代表用户看到了。UniPush 2.0提供了送达回执数据,在开发者中心的推送统计里能看到:发送量、在线送达量、厂商通道送达量、点击量。
这里有几个指标要分清楚:
- 发送量:服务端成功接受的消息数;
- 到达量:消息到达设备(长连接或厂商通道)的数量;
- 展示量:真正展示到通知栏的数量;
- 点击量:用户点击通知栏的数量。
从发送到点击,每一步都有损耗。正常运营级App的点击率一般在2%到5%之间算是健康。如果到达量正常但展示量低,大概率是Android通知渠道被用户关了;如果发送量就异常低,说明cid绑定环节出了问题;如果厂商通道到达量为0,去查对应厂商平台的密钥和包名签名是否配置正确。
5. 我在生产环境踩过的坑:从收不到通知到点击无响应
5.1 离线消息收不到:锁定厂商通道配置三重关
这是最高频的故障,现象是"App在线能收到,一杀掉就收不到"。排查链路基本固定,按顺序检查三个点。
第一,包名和签名。厂商平台填的包名、签名指纹,必须和正式包完全一致。用keytool -list -v -keystore your.keystore查看正式签名的SHA256,和厂商平台后台比对。记住,HBuilderX云打包用"公共测试证书"跑出来的包,和正式证书包的指纹不一样,测厂商通道一定要用正式证书包或自己的自定义基座。
第二,厂商密钥状态。有些厂商平台密钥申请后不是立即生效的,小米需要等应用通过审核,OPPO需要创建消息服务并绑定应用。这些状态在厂商后台都能看到,如果显示"审核中"或"未启用",离线推送必然失败。
第三,设备端通知权限和电池策略。Android 13及以上,通知权限需要在运行时动态申请,用户拒绝后所有通知都收不到。小米、华为、OPPO的系统还有各自的电池优化策略,会限制App的"自启动"和"后台运行"。厂商通道是系统级服务,理论上不影响,但用户在系统设置里把App的通知彻底关掉,那谁都救不了。App里需要做一个权限引导页面,检测到通知权限被关就提示用户开启。
5.2 通知栏不弹通知:多半是渠道或分类的锅
明明消息到设备了,通知栏就是不显示。出现这个情况,先看手机设置里App的通知是否允许,再往下排查通知渠道。
如果你用forceNotification: false发的是透传消息,那消息到达后App进程已死,根本没有人处理它,自然也不会有通知栏展示。想离线也弹通知,必须发forceNotification: true的通知消息。
另一个隐蔽问题是厂商的消息分类。小米推行"消息分类"制度,把通知分为"即时通讯类""资讯类""营销类"等,营销类消息在部分MIUI版本上默认不弹横幅、不响铃、直接收进通知栏折叠区。华为也有类似机制,新闻资讯类App如果没申请到高优先级分类,通知展示会被降级。如果你的App属于内容资讯类,务必提前去厂商平台申请相应的消息分类权益。
5.3 点击通知无响应:payload和路由是重灾区
点击通知栏消息,App倒是被唤醒了,但没有跳转到目标页面。这个问题九成出在payload上。
UniPush 2.0的payload是字符串类型,不是对象。服务端传的时候必须JSON.stringify成一个字符串,客户端收到后再JSON.parse解析。很多团队服务端直接传了个JSON对象,到了客户端拿到的payload就变成了[object Object],解析必然失败。
跳转用uni.navigateTo时,目标页面必须是已经在pages.json里注册过的页面,且不能是tabBar页面。tabBar页面要用uni.switchTab跳,这个区分一定要做,否则点击那个瞬间控制台会报错,用户看到的就是"点了没反应"。
冷启动的场景也要单独测。用户杀掉App后点击通知,App冷启动,此时uni.onPushMessage如果注册晚了,type === 'click'的事件会丢失。稳妥的做法是同时在App.vue的onLaunch里注册监听,并且把cid获取和消息监听放在同一个时机,确保冷启动时监听先于业务页面执行。
5.4 iOS收不到推送:证书环境和token的对齐
iOS用户反馈收不到推送,优先检查两件事。
第一,证书环境。开发证书对应开发环境,发布证书对应生产环境,二者不能混用。用HBuilderX真机运行调试时走的是开发环境,这时候需要在开发者中心配好开发证书;线上发布包走的是生产环境,必须配发布证书。最稳的方式是使用.p8密钥,一个密钥同时支持开发和发布环境,省去来回切换的麻烦。
第二,APNs token。iOS每次安装App,系统都会生成一个deviceToken,UniPush把它映射到cid。如果用户在系统设置里关闭了通知权限,APNs会认为该设备不可推送。还有一种情况:用户在App内不同意通知权限弹窗,系统直接不给deviceToken,那么cid都拿不到。所以在iOS端,首次启动就要引导用户允许通知权限。
5.5 我维护的一套自测清单
踩了这些坑之后,我给自己定了一套发版前的推送自测清单,每次上线前跑一遍,基本能挡住九成问题:
- 前台在线:通知消息和透传消息各发一条,确认秒达;
- 后台运行(不杀进程):确认通知正常展示;
- 杀掉进程(Android):用正式签名包测试华为、小米、OPPO、vivo四个主流厂商的离线推送,确认通知栏展示和点击跳转;
- 杀掉进程(iOS):确认APNs到达,点击跳转正常;
- 关闭通知权限:确认App有权限引导提示,而不是用户完全不知道去哪开;
- 弱网环境:确认消息不重复、不丢失,过期时间生效;
- 多设备:同一账号绑定了手机和平板时,确认所有设备都能收到(或按业务需求做单端限定)。
这套清单看起来繁琐,但每次上线前跑一遍也就半小时,比线上出问题再紧急热修划算多了。
最后分享一个小技巧:开发期间我会在服务端保留一个"测试推送"的调试接口,界面上可以填写cid、标题、内容和payload,一键发送,不用每次都拼curl。配合真机日志,排查消息问题的时候效率至少翻一倍。UniPush 2.0这套东西,原理其实不复杂,复杂的是它横跨了服务端、客户端、厂商平台三端,任何一端的信息不对称都会让你多熬几个通宵。把通道原理吃透,把参数当成契约来管理,这套推送体系就能稳稳地运转下去。