HarmonyOS 消息推送闭环实战:订阅、分层、限频与退订
HarmonyOS 消息推送闭环实战:订阅、分层、限频与退订
推送最容易被做成“服务端发一条,客户端弹一下”。短期看能触达用户,长期看会带来反效果:用户不知道为什么收到消息、营销和服务通知混在一起、同一活动一天推好几次、退订后仍收到提醒。推送不是发送能力,而是一套用户沟通闭环。
本文从 HarmonyOS 应用侧整理推送治理方法:订阅场景、消息分层、Token 状态、限频去重、点击追踪和退订生效。具体推送服务可以接入 Push Kit 或团队已有服务,工程结构保持一致。

1. 推送先区分消息类型
| 类型 | 示例 | 处理原则 |
|---|---|---|
| 服务通知 | 订单状态、审核结果 | 与用户行为直接相关 |
| 功能提醒 | 预约开始、任务到期 | 用户主动订阅或触发 |
| 运营消息 | 活动、内容推荐 | 必须可退订和限频 |
| 系统消息 | 安全提醒、账号异常 | 文案克制,避免营销化 |
如果所有消息都走同一个通道,后续就很难解释订阅来源和退订规则。

2. 推送资料边界和模块落点
| 资料或位置 | 作用 |
|---|---|
| 华为开发者文档中心:https://developer.huawei.com/consumer/cn/doc/ | 查询 Push Kit、通知、应用服务能力入口 |
| HarmonyOS 指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ | 查询通知、权限、后台和应用生命周期资料 |
services/push/PushScene.ets |
定义消息场景和频率 |
services/push/PushTokenStore.ets |
维护设备 token 状态 |
services/push/MessageRouter.ets |
处理到达、点击和跳转 |
services/push/PushAudit.ets |
记录发送结果和用户动作 |
推送涉及用户体验和合规说明,建议和隐私政策、权限说明、运营策略一起维护。
工程上可以把推送拆成几个稳定模块:
| 项目位置 | 建议职责 |
|---|---|
services/push/PushScene.ets |
定义消息类型、订阅理由和限频 |
services/push/PushTokenStore.ets |
维护用户、设备和 token 关系 |
services/push/PushFrequencyGuard.ets |
控制重复消息和每日上限 |
services/push/MessageRouter.ets |
校验 payload 并完成安全跳转 |
services/push/PushAudit.ets |
记录到达、点击、退订等动作 |
这样推送问题出现时,能分清是 token 失效、频率配置错误、payload 错误,还是用户已退订。
3. PushScene 定义订阅场景
type PushCategory = 'service' | 'reminder' | 'marketing' | 'security';
interface PushScene {
sceneId: string;
category: PushCategory;
title: string;
userReason: string;
maxPerDay: number;
canUnsubscribe: boolean;
}
const pushScenes: PushScene[] = [
{
sceneId: 'order_status',
category: 'service',
title: '订单状态通知',
userReason: '用于提醒订单处理进度',
maxPerDay: 6,
canUnsubscribe: false,
},
{
sceneId: 'activity_recommend',
category: 'marketing',
title: '活动推荐',
userReason: '用于推荐用户可能感兴趣的活动',
maxPerDay: 1,
canUnsubscribe: true,
},
];
场景定义让推送可解释。服务通知和营销消息不应该共享同一套频率和退订策略。
4. Token 状态要可追踪
interface PushTokenState {
userId: string;
deviceId: string;
token: string;
valid: boolean;
updatedAt: number;
}
class PushTokenStore {
private readonly tokens = new Map<string, PushTokenState>();
update(state: PushTokenState): void {
this.tokens.set(`${state.userId}:${state.deviceId}`, state);
}
invalidate(userId: string, deviceId: string): void {
const key = `${userId}:${deviceId}`;
const old = this.tokens.get(key);
if (old) {
this.tokens.set(key, { ...old, valid: false, updatedAt: Date.now() });
}
}
}
Token 不是拿到后就不管。退出登录、注销账号、切换设备、系统权限变化时,都要同步状态。
5. 限频和去重必须在发送前判断
interface PushSendRecord {
userId: string;
sceneId: string;
messageId: string;
sentAt: number;
}
class PushFrequencyGuard {
private readonly records: PushSendRecord[] = [];
canSend(scene: PushScene, userId: string): boolean {
const today = new Date().toDateString();
const count = this.records.filter((item) =>
item.userId === userId &&
item.sceneId === scene.sceneId &&
new Date(item.sentAt).toDateString() === today
).length;
return count < scene.maxPerDay;
}
mark(record: PushSendRecord): void {
this.records.push(record);
}
}
限频不是运营后台口头约束,应用侧也应有保护。尤其是本地提醒或端侧触发消息,更要避免重复打扰。

6. MessageRouter 处理点击和跳转
interface PushPayload {
messageId: string;
sceneId: string;
targetPage: string;
params: Record<string, string>;
}
class MessageRouter {
route(payload: PushPayload): string {
if (!payload.targetPage) {
return 'home';
}
if (payload.sceneId === 'order_status' && payload.params['orderId']) {
return `order/detail?id=${payload.params['orderId']}`;
}
return payload.targetPage;
}
}
点击跳转要校验参数,不能把远程 payload 原样作为路由执行。这样可以避免坏消息导致页面异常。
7. 退订记录要立即生效
class PushSubscriptionStore {
private readonly unsubscribed = new Set<string>();
unsubscribe(userId: string, sceneId: string): void {
this.unsubscribed.add(`${userId}:${sceneId}`);
}
subscribed(userId: string, sceneId: string): boolean {
return !this.unsubscribed.has(`${userId}:${sceneId}`);
}
}
营销和提醒类消息必须尊重用户退订。退订后仍继续发送,是推送体验里最伤信任的问题之一。
8. 推送上线前的验证动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 未订阅营销 | 发送活动消息 | 不展示 |
| 服务通知 | 订单状态变化 | 可正常提醒 |
| 超过限频 | 连续发送两条活动消息 | 第二条被拦截 |
| Token 失效 | 用户退出登录 | 不再给旧设备发送 |
| 点击跳转 | payload 缺少参数 | 回到安全页面 |
推送验证不要只看“手机能收到”。还要看频率、退订、点击、Token 失效和异常 payload。
9. 消息触达问题排查表
| 现象 | 优先检查 | 修复建议 |
|---|---|---|
| 用户说被打扰 | maxPerDay 和退订状态 | 给运营消息限频 |
| 退出后仍收到 | Token 是否失效 | 登出时更新 token 状态 |
| 点击打开错误页 | payload 参数是否校验 | 通过 MessageRouter 统一跳转 |
| 服务通知收不到 | 场景是否误判为营销 | 分清 category |
| 退订无效 | 是否只改本地状态 | 同步服务端订阅状态 |
推送上线前建议做一轮“反向验收”:用户退订后发送营销消息、Token 失效后发送服务通知、payload 缺少目标参数后点击通知。只有这些失败场景都能安全处理,推送才不会变成用户打扰源。
interface PushReleaseCheck {
sceneRegistered: boolean;
frequencyVerified: boolean;
unsubscribeWorks: boolean;
tokenInvalidationWorks: boolean;
clickRouteSafe: boolean;
}
const pushReleaseCheck: PushReleaseCheck = {
sceneRegistered: true,
frequencyVerified: true,
unsubscribeWorks: true,
tokenInvalidationWorks: true,
clickRouteSafe: true,
};
这份检查表可以和运营活动一起走。每次新增消息类型,都应该先确认它属于哪一类、是否可退订、频率是多少、点击后去哪里。
推送专项证据包:触达、限频和退订要一起验收
推送不是发出去就结束。读者真正关心的是:用户是否订阅、是否命中分层、是否超过频率、是否能退订、失败后是否有补偿。缺少这些证据,推送文章就会像运营配置说明。
| 证据项 | 说明 |
|---|---|
subscribeState |
是否允许接收 |
segment |
命中的用户分层 |
dailyCount |
当日触达次数 |
unsubscribeAt |
退订时间 |
interface PushEvidence {
userId: string
subscribeState: 'on' | 'off'
segment: string
dailyCount: number
}
function canSendPush(e: PushEvidence): boolean {
if (e.subscribeState !== 'on') return false
if (e.dailyCount >= 3) return false
return e.segment.length > 0
}
这段代码把推送合规和体验边界放到发送前,避免只在发送失败后补救。
推送触达复现场景:给读者一组可执行核验
推送治理必须验证退订和限频。否则文章只证明消息能发出去,没有证明不会打扰用户。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface PushReplayCase {
userId: any
scene: any
dailyCount: any
unsubscribeState: any
}
const replay68: PushReplayCase = {
userId: 'sample',
scene: 'sample',
dailyCount: 'sample',
unsubscribeState: 'sample',
}
function assertReplay68(item: PushReplayCase): void {
if (item.dailyCount > 3) throw new Error('单日推送次数超过限制')
}
这组核验把推送发送前的用户状态、分层和频次合并判断,避免触达能力变成打扰来源。
推送退订回放表:把文章方法变成可复现动作
消息推送的关键不是送达率,而是用户能否控制打扰。建议用一个已退订用户、一个高频触达用户、一个新订阅用户分别回放发送链路,确认每类用户的处理不同。
| 回放动作 | 核验方式 |
|---|---|
| 已退订用户不发送 | 准备输入、执行操作、记录结果、给出结论 |
| 高频用户被限流 | 准备输入、执行操作、记录结果、给出结论 |
| 新订阅用户正常接收 | 准备输入、执行操作、记录结果、给出结论 |
| 失败回执进入补偿队列 | 准备输入、执行操作、记录结果、给出结论 |
推送回放时不要只看设备是否收到通知,还要看用户是否应该收到。读者可以把订阅状态、用户分层、当天已发送次数和失败回执放到同一条记录里:订阅关闭时不发送,限频命中时不发送,服务端失败时进入补偿。这样写出来的推送链路才是完整的触达治理,而不是单纯证明消息通道可用。
推送触达的落地边界:不要把边界留给读者猜
推送链路要区分系统通知、运营触达和业务提醒。业务提醒通常要求及时,运营触达更强调限频,系统通知更强调可解释和可退订。读者实现时不要把三类消息放进同一个发送函数,否则后续限频、退订和投诉处理都会混在一起。
| 落地项 | 处理要求 |
|---|---|
| 业务提醒优先保证时效 | 需要有明确输入、处理边界和失败兜底 |
| 运营触达优先尊重频次 | 需要有明确输入、处理边界和失败兜底 |
| 系统通知必须说明来源 | 需要有明确输入、处理边界和失败兜底 |
| 退订状态优先级最高 | 需要有明确输入、处理边界和失败兜底 |
这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。
10. 小结:推送要尊重用户节奏
HarmonyOS 消息推送要做成闭环,而不是只看发送成功率。订阅说明让用户知道为什么收到,消息分层避免服务通知和营销混杂,限频保护用户体验,退订体现尊重,点击追踪帮助分析效果。这样的推送能力才适合长期运营。
更多推荐



所有评论(0)