Azure Notification Hub 的 Test Tag 功能我盯了很久,这次总算把“定向发送消息到指定 Android 设备”这个场景完整跑通了。先说结论:门户页面上的 Test Send(测试发送)配合 Tag 表达式,确实是可以不用写一行代码就能完成定向推送验证的,前提是你把设备注册、Tag 打标、连接字符串这些前置工作理顺。本文会把整个实验从头到尾拆开讲,适合正在做 Android 推送、想用 Tag 做精细化触达,又不想一上来就写服务端代码的团队参考。
1. 先说清楚:Test Tag 到底解决了什么问题
1.1 安卓推送里“定向”为什么这么难搞
做 Android 推送的人基本都经历过这个阶段:刚开始是全员广播,把消息怼给所有设备,产品经理一看觉得太粗暴,要求“只发给北京地区的用户”“只发给最近 7 天活跃的 VIP”,然后你就开始折腾了。
FCM 本身提供的定向手段是 Topic(主题)和单设备 Token。Topic 是一种订阅关系,客户端主动订阅某个主题,服务端往主题发消息;Token 是每个安装实例唯一的设备标识,服务端要维护一份 token 列表,逐个下发。问题在于:业务上的定向条件往往是动态组合的,你要“北京地区 + VIP + Android”,Topic 就得拆成 N 个排列组合,Token 方案则要求你每次推送前先算一遍目标设备集合。
Azure Notification Hub 里的 Tag 机制,本质是把“定向条件”这件事下沉到了推送服务层。你不用管具体是哪些设备,只要在注册时给设备打上标签,发送时写一个标签表达式,服务自动匹配所有带这些标签的注册项。Tag 可以叠加,表达式可以组合,这就是它和 Topic、Token 方案最大的区别。
1.2 Tag 机制:给设备贴上一张“数字快递单”
用快递来打比方是最容易理解的。你在电商下单,仓库发货时包裹上会贴一张面单,写着收货城市、配送站点、商品类型,分拣线根据面单上的标签把包裹推进不同的格口。这里每一个标签就是一个 Tag。
在 Notification Hub 里,设备通过 SDK 注册时会携带一组 Tag,比如:
android:平台标识user-10086:用户IDcity-beijing:地域vip:用户等级news:感兴趣的内容频道
这些标签被挂在注册项(Registration)上。一个注册项可以挂多个 Tag,一个 Tag 也可以对应多个注册项,多对多的关系。发送消息时你不需要指定 device token,只要写一句 “发给同时带有 city-beijing 和 vip 这两个标签的设备”,服务端自动从注册表里筛出满足条件的设备,然后把消息推过去。
顺带说明一下,Notification Hub 现在有两套实体:老牌的 Registration(注册项)和较新的 Installation(安装项)。Registration 是手动管理标签集合,Installation 则是把设备作为一个整体对象管理,支持更丰富的属性更新。本文实验用的是 Registration 这一套,但 Tag 表达式匹配的逻辑对两套实体都生效。
1.3 Test Tag 在整条推送链路里的位置
正常情况下,定向推送的完整链路是这样:App 启动 → SDK 注册设备到 Hub(带 Tag)→ 业务后端调用 Notification Hub REST API 发送 → 服务端匹配 Tag → 推送给 FCM → 到达 Android 设备。
这里有个很现实的痛点:联调阶段,你可能后端还没写好、消息格式还没定稿、甚至服务端代码还没开始写,只是想验证“我打的 Tag 对不对”“我的表达式写出来之后能不能命中预期的设备”。为了这点验证去写一套完整后端,太重了。
Azure 门户页面上的 Test Send 就是干这个用的。它给运维、测试、客户端开发提供一个入口:在网页上选平台、写 Tag 表达式、填 payload,点一下发送,马上能看到匹配了多少注册项、推送结果如何。Test Tag 功能特指这个入口里用 Tag 表达式定向发送的那一块,也是我今天实验的主角。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实验前置:把环境和设备准备到“一发即达”
2.1 创建 Notification Hub 命名空间
Test Send 是门户功能,前提是你得先有一个能用的 Notification Hub。
在 Azure 门户里,层级关系是:命名空间(Namespace)→ 通知中心(Notification Hub)。你可以把命名空间理解为一个容器,里面可以建多个通知中心,每个中心对应一个 App 或一个业务线。
我这次是纯实验,直接创建了一个免费层(Free)的命名空间。要注意免费层的限制:每个月有 100 万条消息推送的额度,设备注册数量上也会有一些限制,但对测试来说绰绰有余。创建位置我选了 Southeast Asia,主要考虑国内设备到 Azure 的链路延迟相对低一些,这个看你自己网络情况,选 East Asia 之类的也都没问题。
创建完成之后,集群会自动分配一个连接字符串(Connection String)。在命名空间的“访问策略”(Access Policies)里能看到:
RootManageSharedAccessKey:管理权限,什么都能干DefaultListenSharedAccessSignature:只监听,一般客户端注册用这个DefaultFullSharedAccessSignature:完整权限,服务端发送用
我做 Test Send 实验不需要手动粘连接字符串,门户自己已经持有管理凭证。但后面如果要写代码调用发送 API,就得用带 Send 权限的字符串。建议熟悉的连接字符串格式长这样:
code复制Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=xxxxxxxxxxxx=
注意区分:Listen 权限的 key 只能做注册、接收,Test Send 是服务端行为,实验阶段直接在门户操作即可,不需要关心 key 权限。但如果你是调 REST API 发消息,key 必须带 Send 权限,这是个经常被忽略的坑。
2.2 Android 端接入与设备注册
门户那边准备好了,Android 端就得把设备注册成 Hub 里的一个 Registration,并且打上 Tag。我这边实验用的是一台真机,Android Studio 环境,集成了 FCM SDK 和 azure-notificationhubs-android SDK。
展开说一下注册流程。首先 Firebase 控制台创建一个项目,拿到 google-services.json 放进 App 的 app/ 目录,然后在 build.gradle 里引入依赖:
gradle复制implementation 'com.google.firebase:firebase-messaging:23.2.0'
implementation 'com.microsoft.azure:notification-hubs-android-sdk:1.1.4'
接着在 FirebaseMessagingService 里处理 token,并在 token 刷新时注册到 Notification Hub。这里我用 Kotlin 写了个最小示例:
kotlin复制class PushRegistrationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
registerWithNotificationHub(token)
}
private fun registerWithNotificationHub(fcmToken: String) {
val hub = NotificationHub(
"your-hub-name",
"Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=xxxx",
applicationContext
)
// 把业务标签作为可变参数传进去,这里演示三个 tag
val tags = arrayOf("android", "user-10086", "city-beijing")
hub.register(fcmToken, *tags)
}
}
这段代码做的事情很直白:拿到 FCM 的推送 token,把 token 和三个 Tag 一起交给 Hub,Hub 维护这条注册记录。之后每次 App 冷启动或者 token 刷新,SDK 都会重新注册,保证 Registration 是新鲜的。
这里要提一个细节:register 方法的第二个参数是可变长 Tag 数组。SDK 底层会把这些 Tag 和 token 绑定在一起,生成一条 Registration,并返回一个 Registration ID。你可以把 Registration ID 打印到日志里,后面排查会用到。
2.3 确认注册信息与 Tag 状态
设备跑起来之后,一定要确认注册确实成功了,不然 Test Send 那一步就是空谈。三个确认手段:
第一,看 App 日志。onNewToken 被触发、register 方法返回成功,日志里能看到类似 Registration successful, id = 1234567890123456789-789456123 的输出,这种就是注册成功。
第二,去门户看。在 Notification Hub 的“注册”(Registrations)资源管理页面里,输入设备 token 的前几位或直接浏览,能看到这个设备对应的 Registration ID,点进去能看到它挂着哪些 Tag。这一步最重要——你以为你打了 city-beijing,实际上可能因为传参顺序、拼写问题压根没打上,在门户里一眼就能看穿。
第三,用服务端 SDK 或 REST API 查询注册列表,这个比较重型,实验阶段没必要。我这次就是靠前两个手段确认的:设备日志显示注册成功,门户里看到这条注册项带着 android、user-10086、city-beijing 三个标签。
提示:注册成功不等于推送一定可达。Registration 的存活期大约 90 天,期间如果 App 被卸载、token 更新后没有重新注册,这条 Registration 就成了“僵尸注册”。后面在 Test Send 里看到匹配数量但不真实到达,大概率就是这种僵尸注册作祟。
3. 核心实操:在门户页面通过 Test Tag 定向发送
3.1 找到 Test Send 入口并选择平台
前置工作做好,终于到了重头戏。登录 Azure 门户,进入你创建的 Notification Hub 资源页,左侧菜单往下滑,在“帮助”(Help)或“故障排除”(Troubleshoot)分组下,能看到一个叫“测试发送”(Test send)的入口。有些版本的 Portal 直接就在顶部工具栏放了一个 Test send 按钮,位置可能略有差异,但名字不会变。
点进去之后,第一个要设置的就是“平台”(Platform)。我这次要发给 Android 设备,就选 Android。选完之后,页面会动态调整下方的 Payload 输入框——Android 平台对应的是 FCM 消息格式的 JSON。
这里提一个很多人会忽略的点:Notification Hub 对 Android 的推送走的是 FCM 通道,所以门户里配置 Android 平台凭证(Firebase 的 API Key 或服务账号 JSON)是必须的前置条件,没配好的话 Test Send 这一步会直接报平台配置错误。配置入口在 Notification Hub 的“设置”里有个 “Google (GCM/FCM)” 选项,把 Firebase 项目的服务账号 JSON 传上去或填入 API 密钥即可。
3.2 构造 Tag 表达式:从“人人都收”到“只有你要的人能收”
平台选好之后,你会看到一个标签表达式(Tag Expression)输入框。默认是空的,空表示广播,也就是发给这个 Hub 下所有注册的设备。
我第一次实验就是先空着发了一条,确认链路通,然后再来测试 Tag 定向。我的目标很明确:只发给那台测试机上挂着 user-10086 标签的设备。所以我在表达式框里填了:
code复制user-10086
这一句就表示:匹配所有带有 user-10086 这个 Tag 的注册项。如果你希望更精确一点,可以组合多个条件。例如只发给“北京地区”且“VIP”的设备:
code复制city-beijing && vip
这里的 && 是逻辑与。如果只想发给“北京”或者“上海”中任一地域的设备:
code复制city-beijing || city-shanghai
|| 是逻辑或。括号也可以用来分组,表达式的解析规则和大多数编程语言一致。
了解语法之后,我在表达式框里先写了 user-10086 发了一条,然后又清空发了一条广播,两条消息分别确认了“定向”和“广播”的行为差异。这里最直观的体现就是页面显示的结果里会有一个匹配计数。
3.3 填 FCM Payload 并发送
Tag 表达式写好后,下方就是 FCM 消息的 JSON 输入框。Android 平台在门户里通常用旧版 FCM 格式,因为它在兼容性上覆盖最广:
json复制{
"data": {
"message": "这是一条定向测试消息",
"source": "azure-portal-test"
},
"notification": {
"title": "定向推送测试",
"body": "这条消息只发给 user-10086 这台设备"
}
}
这里有两个层级需要区分:notification 层是让 FCM 直接展示系统通知的,收起应用也能看到通知栏弹出;data 层是自定义键值对,App 在后台或前台收到后可以自己处理,比如跳转页面、弹窗提示。如果只做展示型推送,写 notification 就够了;如果需要客户端根据内容做业务动作,data 里传结构化数据更合适。
把 JSON 填进输入框,点击页面底部的“发送”(Send)按钮。
注意:Test Send 是真实发送,不是模拟。它会真的通过 FCM 把消息推到你的测试设备上。如果你在办公环境做实验,记得别用生产 Hub 里挂着一堆真实用户的注册项来跑定向测试,不然消息会真的打到用户手机上。我建议实验用单独的测试 Hub,或者 Tag 用
testdevice这类绝不会在生产出现的标签值。
3.4 结果验证:如何确认这条消息确实发给了指定的 Android 设备
发送完成之后,门户会返回一个结果区域。我这次实验看到的情况是:
- 消息状态:已发送成功
- 匹配到的注册数:1
- 失败数:0
- 服务端消息 ID:一串类似
4d4a53f2-...的编号
这个“匹配到的注册数”就是 Test Tag 功能最核心的价值。它直接告诉你:在 Hub 的注册表里,到底有多少注册项满足了你写的这条 Tag 表达式。如果填 user-10086 显示 1,说明你打的 Tag 确实有效,并且只命中了这台设备;如果显示 0,说明要么 Tag 没打上,要么设备注册有问题,要么表达式写错——这正是排障的出发点。
设备端怎么验证?Android 设备这个时候通知栏会弹出一条消息。如果你用 data 字段且没有 notification 字段,客户端需要自己在 FirebaseMessagingService.onMessageReceived 里处理并创建通知,否则设备上是没反应的,这点别搞混了。
还有更精细的验证方式:在测试设备上把 App 切到前台,观察 Logcat。FCM 消息到达时会有系统日志,SDK 的回调也会触发 onPushNotificationReceived 之类的事件。把日志打出来,就能确认消息不仅推到了 FCM,而且确实被设备进程接收了。
4. Tag 表达式匹配逻辑与生产环境设计
4.1 匹配规则:注册标签集与表达式求值
跑通实验是一回事,理解背后的匹配逻辑才是真正能在生产环境里不翻车的前提。
每一个 Registration 在创建时会带上一组 Tag 集合,比如:
code复制{ "android", "user-10086", "city-beijing" }
你写的 Tag 表达式本质上是一个布尔表达式。服务端遍历所有 Registration,逐个把 Tag 集合代入表达式求值。集合里有 user-10086,且表达式是 user-10086,那结果为真;如果表达式是 city-beijing && vip,而这个设备没有 vip,结果为假,就不会被选中。
要特别注意的是 || 和 && 的优先级。Azure 的 Tag 表达式遵循常规逻辑:&& 优先级高于 ||。例如:
code复制a || b && c
等价于:
code复制a || (b && c)
如果你想把“a 或 b”作为一组再与 c 组合,必须加括号:
code复制(a || b) && c
我实验时特意验证过括号分组,因为实际业务里最常见的场景就是 “(体育 || 财经) && VIP 用户”。不加括号,表达式结果就会和预期完全相反。这种问题在 Test Send 页面里能第一时间暴露,因为匹配计数会明显对不上。
另外一个容易踩坑的点:Tag 匹配是精确匹配,大小写敏感。User-10086 和 user-10086 是两个完全不同的 Tag。还有,表达式的引号、空格也有讲究。门户页面的 Tag 表达式输入框里,Tag 值一般不需要自己额外加引号,直接用标识符或加上双引号均可,但一旦用了引号,必须是英文半角引号,中文引号在解析时会直接报错。
4.2 真实业务中的 Tag 体系设计
实验只是验证功能,但真实场景里 Tag 怎么规划,比功能本身更值得琢磨。我基于自己做过推送运营项目的经验,分享一套比较通用的打标维度:
- 用户维度:
user-{userId}、user-group-{groupId}、vip、new-user - 设备维度:
android、ios、language-zh、language-en - 地域维度:
city-beijing、city-shanghai、country-cn - 业务维度:
topic-news、topic-sports、promo-active
这套体系的设计原则是:Tag 只标记“相对稳定的属性”,不要把时间敏感、一次性的事件塞进 Tag 里。比如“今天要发优惠券”这种就适合每次推送时算设备列表,而不是打个 coupon-today 标签,第二天又得清理。
另一个实际经验是“少量有价值的 Tag 优于大量花哨的 Tag”。每一个 Tag 都对应注册时的上报逻辑和维护成本,Tag 太多,客户端代码乱、服务端组合复杂,排障成本直线上升。我在生产项目里一般限制单个设备最多 10 个 Tag,超过就回查设计合理性。
4.3 Test Tag 在排障和联调里的实际用法
很多人把 Test Send 当成一个“发送按钮”就完事了,其实它最值钱的用法是排障。
举一个真实场景:业务反馈“明明发了一波定向推送,但有一部分用户没收到”。你用后端的发送日志只能看到 REST API 返回成功,但根本不知道到底有多少注册项被匹配了。这时候打开 Test Send,输入和线上相同的 Tag 表达式,发送同样的 payload,看匹配计数。如果匹配到的注册数远小于你预估的设备数,说明注册表本身就有问题——大量设备没有成功注册、Tag 没打上、Registration 过期被清理。
再举一个联调场景:客户端要验证 data 消息的字段解析逻辑,但后端还没就绪。测试人员直接进 Test Send,填上约定的 JSON,定向发给测试设备的 user-{id},客户端就能立刻进入调试流程。这一步省的可不是一星半点的沟通成本。
我还要强调一点:Test Send 只负责“把消息发出去”,它不负责“消息一定到达”。匹配到了注册项、服务端返回成功,只代表 Hub 已经把它交给了 FCM。后面 FCM 到设备这一段是另一条链路,出了问题不要在产品上一味怪推送服务。
5. 常见问题与排查技巧实录
5.1 测试发送显示“0 个匹配”
这是最容易遇到的第一个坑。表达式写了 user-10086,匹配计数却是 0。按优先级排查:
第一,确认设备确实注册成功了。回 App 看日志,没有 Registration ID 就说明注册失败了,多半是连接字符串或 SDK 配置问题。
第二,确认门户里能查到这条注册。去 Registration 管理页面搜一下,搜不到就是注册没落库。
第三,确认 Tag 拼写完全一致。包括大小写、下划线、连字符,不要想当然。门户上搜到注册项后,点开看它身上挂着的 Tag 列表,跟表达式里写的逐字对比。我实验时第一次就栽在这里:代码里打的是 city-beijing,表达式框里手滑打成 city_beijing,结果自然是 0。
第四,确认 Hub 和注册是在同一个命名空间。实验时开了好几个资源组,容易搞混,注册发到了 Hub A,Test Send 在 Hub B 里发,那就永远匹配不到。
5.2 匹配到了但设备收不到
匹配计数是 1,但手机上没有任何响应。这种情况往往是“最后一段链路”的问题:
- Notification Hub 里的 FCM 凭证配置过期或错误,导致消息提交到 FCM 被拒。但注意,这种返回结果通常不会显示成功,一般会报错。
- 设备的 FCM token 失效。App 卸载重装、清除数据、系统更新都可能导致 token 变化,如果旧 token 还挂在 Registration 上,匹配是匹配到了,但 FCM 转发时发现 token 无效,直接丢弃。
- 国产 ROM 的后台限制。小米、华为、OPPO 等机型,App 被清理后进程不存活,通知收不到是常态,和推送服务本身无关。测试设备要避免这类问题,可以在系统设置里把 App 的自启动、后台运行权限打开。
还有一种情况:你只写了 data 字段,客户端没有实现 onMessageReceived 里的通知显示逻辑。FCM 的消息确实到了,但你的代码没把它变成用户能看到的东西。排查时先用带 notification 字段的 payload 发一条做对比,就能定位是不是这个原因。
5.3 Tag 表达式写错的高频雷区
我在第 4 章讲了 ||、&&、括号、大小写,这里再补充几个实际踩过的:
- 中英文标点混用。特别是从 Word、微信里复制表达式过来,中文引号“”会被解析器拒收。
- 表达式里夹了空格。比如
user-10086 && vip这种写法能不能解析看版本,某些解析器会把空格纳入 Tag 值的一部分,导致匹配失败。保险起见,表达式里不要有多余空格,直接user-10086&&vip。 - Tag 值里带特殊字符。比如
user:10086的冒号、user/10086的斜杠,虽然理论上允许,但这类 Tag 放进表达式里很容易引发解析问题。打标时要预先约束字符集,我一般只允许字母、数字、中划线、下划线。
5.4 权限、过期与跨区域问题
最后一个容易忽略的坑:Registration 过期。前面说了 Registration 的存活时间大约 90 天,如果设备超过 90 天没有用 SDK 重新注册,这条注册项会被清理。你的活跃用户设备可能因为长期不用,注册项已经被清掉了,导致定向推送匹配数永远比预期少。这种问题在 Test Send 里看匹配计数就能发现,但修复需要靠客户端在 App 启动时重新注册,无法靠服务端单方面解决。
还有区域问题。Notification Hub 创建在哪个区域,注册的数据和服务就落在哪个区域。如果设备在海外而 Hub 建在国内,或者反过来,链路延迟和服务可用性都会有影响。实验阶段无所谓,但生产环境一定要选对区域,这个决策在创建命名空间的时候就要想清楚。
我整理了一份快速排查表,逐个对照可以省不少时间:
| 现象 | 优先排查方向 | 验证手段 |
|---|---|---|
| 匹配数为 0 | Tag 拼写、注册是否落库 | 门户注册管理页查询 |
| 匹配数正常但设备无响应 | FCM token 失效、ROM 后台限制 | 换一台干净的真机测试 |
| 发送接口报 401/403 | FCM 凭证配置、连接字符串权限 | 检查 Hub 设置里的 FCM 配置 |
| 报“无注册项” | Hub 选错、注册过期 | 确认命名空间和 Hub 名称 |
| 匹配数远小于预期 | Registration 过期、Tag 体系混乱 | 统计注册数和 Tag 分布 |
做这个实验给我最大的感受是,Test Tag 这个功能看起来不起眼,但它把“推送定向”这个原本黑盒的过程亮了底牌。过去你发一条定向推送,心里是不踏实的,不知道 Tag 匹配逻辑对不对、到底命中了哪些设备。有了 Test Send 的匹配计数和实时结果,整个链路变得可见、可验证、可调试。我建议所有接了 Azure Notification Hub 的团队,把 Test Tag 的使用写进日常联调流程里,客户端报问题、测试验功能、运营配策略,都可以先在这一个页面上快速过一遍。
最后再分享一个小技巧:如果团队里有多个测试设备,我习惯给每台测试机打一个独立的 device-test-01、device-test-02 这类标签,同时在业务标签之外强制加一个 debug 标签。排查问题时,表达式写 debug || user-10086,不管后端有没有传对用户 ID,都能精准命中测试设备。这个小习惯在多人协作的项目里特别好用,谁在测、测的是哪台机器,看 Tag 一目了然。
