HarmonyOS 7+ArkTS:精准碰一碰回执幂等判定与确认门禁【鸿蒙心迹】
碰一碰分享完成后,应用界面收到一条“接收成功”的通知,看起来应该立刻把素材加入列表。这里有一个容易忽略的细节:通知抵达和用户确认接收,不一定是同一件事。设备靠近、页面恢复、通信重试可能让业务适配层交出内容相同的回执。如果每次都往数据表里插入一行,用户就会看到重复名片。本文只处理接收回执进入业务应用后的幂等和确认。触发碰一碰、系统传输、授权及系统分享面板属于另一层,不能凭一个自定义字段就声称已经调用了精准碰一碰 SDK。用这条边界组织代码,问题会简单许多。
## 一、一条名片,为什么有三次回执给业务模拟器准备一张名片 CARD-118,属于会话 S-309,任务 TAP-209。在北京时间 2026 年 10 月 19 日零点十九分附近,适配层依次送来三个事件:E-8801 第一次到达,下一秒重放同一个 E-8801,第三次是已经过期的 E-8795。这三个事件是人工构造的输入,不能理解成真实手机碰一碰的测试结果。业务想看到的不是“总共触发三次页面更新”,而是有效 1、重复 1、过期 1。有效的那条只进入待确认区,当前状态 WAIT_CONFIRM;直到用户点击“确认接收”并完成实际写入,才允许成为正式业务记录。这样的统计既便于定位异常,也更容易向用户说明为什么没有立刻保存。把接收分为三层很重要。最外层是系统侧事件或文件交接;中间是应用的业务适配器,它把已经取得、经过合法性检查的数据转换为 ReceiveEnvelope;最里面才是本文的 ReceiptGate。如果项目尚未获取目标设备所支持的正式入口,最里面两层仍然可以对构造数据做单元验证,但不能反推系统入口一定可用。本文回执中的 eventId、sessionId 和 expiresAtMs 全部是业务字段。它们不是华为公开文档规定的碰一碰回调参数。真正落地时,应根据接入能力和数据来源制定转换规则,并且保留来源验签、身份鉴别等更严格的安全措施。## 二、拒绝重放,也不能误删合法通知最小的回执合同需要事件标识、会话标识、卡片标识和有效期。在某个已核验身份的会话里,可以用 sessionId#eventId 建立幂等键;如果会话 ID 不能保证跨设备唯一,就必须在上游补充稳定的发送端身份。单纯按名片 ID 去重并不合适,因为不同用户确实可能再次分享同一张名片。下面先写一个不依赖系统 SDK 的纯应用层门禁。它返回分类结果,不做数据库写入。nowMs 从调用方传入,是为了让过期边界可以复现;seen 要在真实业务里由受保护的持久事务取代,不能仅靠内存集合覆盖进程重启。typescriptexport interface ReceiveEnvelope { sessionId: string; eventId: string; cardId: string; expiresAtMs: number;}export type ReceiptDecision = 'ACCEPT_PENDING' | 'DUPLICATE' | 'EXPIRED' | 'INVALID';export class ReceiptGate { private seen: Set<string> = new Set<string>(); decide(env: ReceiveEnvelope, nowMs: number): ReceiptDecision { if (!env.sessionId || !env.eventId || !env.cardId || !Number.isFinite(env.expiresAtMs)) { return 'INVALID'; } const key: string = env.sessionId + '#' + env.eventId; if (this.seen.has(key)) { return 'DUPLICATE'; } if (env.expiresAtMs <= nowMs) { return 'EXPIRED'; } this.seen.add(key); return 'ACCEPT_PENDING'; }}这里刻意先判断重复,再判断过期。因为第一次成功登记的事件,即使过了有效期再次送达,仍应归到重放计数,而不是让同一条事件忽然变成另一种故障。相反,没有登记过且已经失效的事件会被判成 EXPIRED。这只是界面诊断口径;安全侧可以同时保存两种拒绝原因。Set 的作用只是让示例逻辑一眼看清楚。它没有跨进程持久性,也没有原子性:两个异步处理器若并发查询同一个键,可能都认为自己是首次。上线应当由唯一键约束、事务或带版本条件的写入完成“检查并占位”。不应把上面短短几行误称为跨设备严格一次投递。## 三、待确认不等于已经入库应用侧最好把接收与确认分开处理。收到 ACCEPT_PENDING 时只登记临时条目及过期信息,不触发不可逆的业务操作。用户点击确认时,还要核对所确认的键是否仍是当前待确认项,防止页面上正在显示 A 时,后台已换成 B。下面这段业务类重点表达状态迁移。真正落盘由项目自身的存储适配器负责;这里没有伪造 Share Kit 或文件接口。页面和存储层应使用同一个稳定键,而不能只凭列表行号确认。typescriptexport class PendingInbox { private pending: Map<string, ReceiveEnvelope> = new Map<string, ReceiveEnvelope>(); add(env: ReceiveEnvelope, decision: ReceiptDecision): void { if (decision !== 'ACCEPT_PENDING') { return; } const key: string = env.sessionId + '#' + env.eventId; this.pending.set(key, env); } takeForConfirm(key: string, nowMs: number): ReceiveEnvelope | undefined { const env: ReceiveEnvelope | undefined = this.pending.get(key); if (!env || env.expiresAtMs <= nowMs) { return undefined; } return env; // 此处仅给事务层核验,不代表已写入 }}````takeForConfirm` 不主动删除待确认条目,是有意的。确认按钮的点击成功与持久写入成功不是一回事:如果提交失败却先从临时列表移除,用户可能以为素材已经保存。合理的顺序是“校验当前项→事务内幂等写入→写入成功后清理待确认项→更新 UI”。失败时让用户重试,并且保持同一幂等键,不能每次重试都生成新 ID。时间口径也需统一。若发送端提供的是自身设备上的毫秒时间戳,本地时钟漂移可能导致误判,应结合可信的服务端有效期或协议规定的时间来源;本文 `expiresAtMs` 只为演示固定样本服务。系统通知是否最终到达、丢失后如何恢复,也不属于这个内存示例所能保证的事情。## 四、页面上只强调用户需要决定的事为了让演示图与正文核对得上,主页面名称固定为 `ReceiptInboxPage`,应用名 `TapReceipt`,卡片编号 `CARD-118`,会话 `S-309`,任务 `TAP-209`。日志时间依次显示 `00:19:03`、`00:19:04`、`00:19:05`,三条业务结果分别是 `ACCEPT_PENDING`、`DUPLICATE`、`EXPIRED`。这是一套人工演示数据,并不是从 HiLog 抓出来的真机证据。下面的 ArkUI 页面只负责回显合成状态和按钮禁用逻辑。`@State` 是界面的本地可观察状态;`Button.onClick` 接的是用户意图,不负责替代前文需要的数据库事务。实际工程中,应该把点击交给控制层异步执行,成功回调后再把 `confirmed` 设为 `true`。typescript@Entry@Componentstruct ReceiptInboxPage { @State validCount: number = 1; @State duplicateCount: number = 1; @State expiredCount: number = 1; @State confirmed: boolean = false; build() { Column({ space: 14 }) { Text(‘TapReceipt · TAP-209’).fontSize(22) Text(‘CARD-118 · S-309’) Text(有效 ${this.validCount} / 重复 ${this.duplicateCount} / 过期 ${this.expiredCount}) Text(this.confirmed ? ‘CONFIRMED_UI’ : ‘WAIT_CONFIRM’) Button(‘确认接收’) .enabled(this.validCount > 0 && !this.confirmed) .onClick(() => { this.confirmed = true; }) // 仅演示视觉状态 }.padding(20).width(‘100%’) }}```这段 onClick 特意标注“仅演示视觉状态”,以免读者把按钮点亮误当做已经完成存储。配套手机图展示的是点击之前的 WAIT_CONFIRM,按钮可用,说明栏写明“示例回执,未写入业务库”。如果做成真实产品,成功后应改为“已接收”,失败要展示保留重试入口,而不是静默吞掉错误。
## 五、调试要同时看分类与写入证据同一类回执问题,光看成功提示很难复盘。建议日志只留下业务事件的脱敏尾号、处理结果、任务号和时间,不打印完整名片内容、手机号、真实发送端凭据或文件 URI。事件 E-8801 连续出现两次,第一次 ACCEPT_PENDING,第二次 DUPLICATE;E-8795 是 EXPIRED。这样的日志可以解释“为何只有一个待确认”,也不会泄漏内容本体。验收表要分两个阶段:应用分类阶段核查三条固定输入对应的三个结果;用户确认阶段另行核查事务只写入一次、写入失败可重试、应用重启后不会重复插入。后面三项在本文还没有实际运行,所以不能写成“已验证”。同时应模拟有效期临界点、重复事件跨天到达、会话编号被复用,以及用户连续双击确认的情况。平台接口方面,华为官方 Share Kit 文档说明系统分享面板及共享数据的边界,但这不自动等于精准碰一碰的接收回调合同。对项目而言,只有拿到目标能力的官方签名、系统支持条件和有效授权流程后,才应实现 ReceiveEnvelope 的正式适配器。示例没有写任何未经确认的碰一碰回调方法名。## 六、这道门禁能保证什么这套方案最有用的地方是把“通知多次抵达”与“业务多次保存”切开。应用可以预先对正常、重复、过期三个分支作单元演示,前端也可以如实显示待确认数量和按钮状态。它不能证明底层物理碰一碰发生了几次,也不能代替对消息来源和传输内容的安全校验。如果需要上线,优先补齐事务型幂等登记、可信过期时间、确认后的实际保存结果、会话归属验证,以及回执退订时的生命周期清理。先把数据责任说清楚,再接系统能力,往往比在 UI 上追逐一个“成功”标签更可靠。资料核对: 华为开发者文档中心(Share Kit):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/share-system-share ;ArkUI 按钮组件:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V13/arkts-common-components-button-V13 。本文为业务状态机示例,未进行 SDK 编译、设备传输或实际持久化验证。
更多推荐




所有评论(0)