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 消息推送要做成闭环,而不是只看发送成功率。订阅说明让用户知道为什么收到,消息分层避免服务通知和营销混杂,限频保护用户体验,退订体现尊重,点击追踪帮助分析效果。这样的推送能力才适合长期运营。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐