HarmonyOS ArkUI Badge × Node.js:闪控窗提醒的乱序确认与过期门禁【共创稿事节】
问题说明:小窗提醒不是收到事件就加一个红点
做待办类应用时,我经常遇到一种看着不像故障的故障:提示卡片已经被用户确认,几秒后又冒出一条同样的提醒。日志里每条消息都有时间,看起来也没有明显异常。追到业务入口才发现,服务端重试、前台恢复和本地定时器可能先后把同一个事件递交给页面。最晚到达的事件不一定是最新版本,直接按到达次序修改 UI,就会把已经确认的事项重新点亮。
如果这块信息恰好展示在一个面积有限的业务浮层里,问题更难察觉。用户只看到角标从零跳回一,不知道它来自旧消息;开发人员只检查 Badge 数值,又会误以为是绑定失效。本轮不去假设存在一套可以任意调用的“系统闪控窗提醒 API”,而是把可复现的工程问题缩小为:应用拥有自己的任务数据后,如何对打开、到期提醒、确认与关闭事件进行顺序保护。 最终可把处理结果交给普通 ArkUI 页面或合规接入的窗口容器显示。

Demo 数据:两件待办、八个投递事件
项目名称叫 NoticeSeqGate。它不联网、不申请系统窗口权限,也不根据本机时钟猜测过期。测试文件声明 FW-51、FW-52 两件任务,分别在构造时刻一百八十和四百到期。事件带任务标识、动作类型、修订版本及构造时间。
FW-51 先打开,达到到期点后展示一次提醒,随后被确认;一个修订号更旧的 REMIND 在确认之后才到。FW-52 在初始快照里已是 OPEN 状态,先收到未到期的提醒,再收到符合时限的新版提醒,最终关闭后又收到旧版确认。这样的输入刻意制造“乱序”和“抢跑”,比单独点十次确认按钮更能暴露规则漏洞。
第一层约定是业务动作必须有单调递增的 revision,它由可信业务端或同一写入序列维护。第二层是 REMIND 必须在 dueAt 之后才可以展示。第三层是已关闭任务不可因旧事件重新打开。只有满足全部条件,UI 的可见状态才发生变化。注意版本单调并不能自动解决不同设备同时写入的并发冲突;真实多端应用应由服务端确定权威版本,客户端只作为最后一层防护。
先处理版本,不要先处理展示
以前我把 REMIND 的判断写在“显示角标”函数里,ACK 又有单独的一套代码。两处都能成功执行,却没人负责它们的相对顺序。现在把顺序检查抽到纯函数,从业务状态开始决定事件是否可用;无效事件可以记录,但不得推动 UI。核心顺序是先确认任务存在,再判定整数版本是否大于已应用版本,最后才走动作状态机。
// tools/replay.mjs 的关键判断,Node.js >= 18
function acceptVersion(task, event) {
if (!task || !Number.isSafeInteger(event.revision)) return false;
return event.revision > task.revision;
}
function dueResult(task, event) {
if (event.type !== 'REMIND') return 'OTHER';
return event.at < task.dueAt ? 'EARLY' : 'READY';
}
这里没有使用 Date.now(),因为复盘时最忌讳输入相同却得到不同结论。每一条事件都自带构造时刻,回放器按测试文件顺序读取。线上改造时应使用服务端事件发生时间、经过校准的逻辑时钟或者明确的到期字段,不能把手机时间当成防重凭据。遇到字段缺失、修订号不是安全整数或者任务不存在,要产生 INVALID 诊断而不是尝试自动修复。
核心实现:到期和确认各有状态前提
确认并不是任何时候都能落库。只有已经进入 SHOWN 状态的任务才接受 ACK,CLOSE 也必须建立在确实被展示或已经确认的状态上。如果任务还是 NEW,收到 ACK 应被判为非法流程;如果状态已经 CLOSED,晚到的同修订版本事件自然被拒绝。一次规则命中之后同步更新 revision 和 status,不能只改状态、忘记前进版本水位。
function reduceTask(state, event) {
if (event.revision <= state.revision) return 'STALE';
if (event.type === 'REMIND') {
if (event.at < state.dueAt) return 'EARLY';
if (state.status !== 'OPEN') return 'INVALID';
state.status = 'SHOWN';
state.revision = event.revision;
return 'SHOW';
}
if (event.type === 'ACK' && state.status === 'SHOWN') {
state.status = 'ACKED';
state.revision = event.revision;
return 'ACK';
}
return 'INVALID'; // CLOSE、OPEN 的分支见完整可执行源码
}
这段代码故意只摘出主要分支,ZIP 内的 replay.mjs 才是包含 OPEN、CLOSE、错误输入、输出文件和退出码的完整可执行版本。对多个任务不共用一个 revision:FW-51 的第三版不应阻止 FW-52 的第一版,这也是我选用 Map 按 taskId 存状态的原因。每条事件只允许落到它自己的任务水位上。
看 UI:把五种结果分开显示
处理器不是直接控制系统通知,而是输出一份 JSON:每个任务的最终状态、每条输入的接受或拒绝原因、SHOW/ACK/CLOSED/STALE/EARLY 五类计数。小面板可以给每一类结果设置对应的文字,不建议只用颜色表达。比如 STALE 明确写成“旧事件被拒绝”,EARLY 写成“未到提醒时间”,否则对照日志时很难知道究竟拦了什么。

使用 ArkUI Badge 时,只让角标表示当前需要关注的数量,而不是累积接收次数。以这组数据为例,FW-51 最终 ACKED、FW-52 最终 CLOSED,待处理角标应该归零;SHOW 累计是二次,不能把它直接绑成角标二。Badge 的显示由已归约业务状态计算,这样恢复界面时就不必重放所有过期动画。角标计数、弹窗进入动画、页面导航是展示逻辑,不参与状态机的正确性判断。
将状态归约与展示拆开后,ArkUI 页面中保留一个很薄的角标层就够了。下面的 ArkTS 仅展示 Badge 的常用参数,初始未处理数为零,因此角标不应出现。
@Entry
@Component
struct NoticePanel {
@State pendingCount: number = 0
build() {
Column({ space: 12 }) {
Badge({ count: this.pendingCount, position: BadgePosition.RightTop,
style: { badgeColor: '#EE5577', badgeSize: 18 } }) {
Text('待办任务').fontSize(20).padding(12)
}
Text('FW-405 · 构造数据状态展示')
}.padding(20)
}
}
要观察真实效果,需要把业务服务返回的 pendingCount 注入组件并在对应生命周期订阅更新。在 DevEco Studio 中采用的 targetSDKVersion 应与官方组件参考匹配。
const pendingCount = [...states.values()]
.filter(task => task.status === 'SHOWN').length;
const metrics = { pendingCount, result: totals };
console.log('FW-405', JSON.stringify(metrics));
上面的 states、totals 是完整脚本中生成的 Map 与计数字典。独立引用这三行需要先运行状态归约代码。采用这样的展示映射,可以允许浮窗被销毁后重新创建,界面只拿最终快照,而不是在回调里盲目执行“数字加一”。也能避免任务确认后清理不及时,出现已处理记录仍带未读红点的情况。
本地验证与预期结果
进入文章目录后执行 node --check tools/replay.mjs 检查脚本语法,再执行 node tools/replay.mjs。检查器会打印逐条处理结果,同时输出 reports/replay.json。以八条输入为准,OPEN 只负责建立开放状态、不纳入五类计数,最终预期为 SHOW 2、ACK 1、CLOSED 1、STALE 2、EARLY 1,两个任务分别停在 ACKED 和 CLOSED。脚本还内置断言,如果汇总与上述数据不一致,会设置退出码二,便于 CI 判断回归失败。
测试时我更关注负例:把 FW-52 的新版提醒时间从四百零一改成三百九十九,SHOW 应减少、EARLY 应增加;把旧 ACK 修订号提高但保持状态已关闭,应该进入 INVALID,而非接受;把事件的 taskId 换成未登记的值,则不能产生新的任务记录。做这些变体之前先复制 fixtures,避免把原始样本覆盖后无法复现。错误分支有明确原因,日志中的 ERROR 不等于应用异常退出。

生命周期、异常与资源释放
真实小窗口往往在后台切换、权限变化、页面重建时反复展示或消失。我的处理原则是状态先持久化,再把快照交给窗口,窗口关闭不等于任务已确认。订阅窗口消息的监听要和组件生命周期对应解绑;定时器、网络请求以及重试队列在页面消失或任务取消时必须停止,避免一个任务有多个消费者重复写回。若使用持久化,需为 revision 与 status 做原子提交,不应分成两次独立保存。
示例的 Node.js 脚本只做同步文件读取和一次报告写入,没有网络句柄、长驻定时器或后台服务,因此进程结束后无需额外释放系统资源。真实应用如需接入窗口管理能力,要另行核查窗口类型、场景限制、权限及目标 API 版本。
回到工程边界
这个小工具给出的结论是:把到期规则、版本顺序和已确认状态集中在一个可回放的业务边界,才能避免“新 UI 被旧事件复活”。它没有解决服务端幂等键签发、跨设备时钟一致性、离线合并与通知通道可靠性。遇到多端竞争,应由权威存储分配版本;客户端只对收到的事件做保守校验。
官方资料建议阅读: 华为 ArkUI 框架概览;华为开发者文档中心(查阅 Badge、窗口管理和对应 API 版本);华为 DevEco Studio 与工具资源;华为开发者问答 Badge 角标示例。
更多推荐


所有评论(0)