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: Setstring new Setstring(); 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: Mapstring, ReceiveEnvelope new Mapstring, 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 只为演示固定样本服务。系统通知是否最终到达、丢失后如何恢复也不属于这个内存示例所能保证的事情。![](https://i-blog.csdnimg.cn/direct/0d4d27d89d484aff91e1d8495551a5e3.png)## 四、页面上只强调用户需要决定的事为了让演示图与正文核对得上主页面名称固定为 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。typescriptEntryComponentstruct 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第二次DUPLICATEE-8795是EXPIRED。这样的日志可以解释“为何只有一个待确认”也不会泄漏内容本体。验收表要分两个阶段应用分类阶段核查三条固定输入对应的三个结果用户确认阶段另行核查事务只写入一次、写入失败可重试、应用重启后不会重复插入。后面三项在本文还没有实际运行所以不能写成“已验证”。同时应模拟有效期临界点、重复事件跨天到达、会话编号被复用以及用户连续双击确认的情况。平台接口方面华为官方 Share Kit 文档说明系统分享面板及共享数据的边界但这不自动等于精准碰一碰的接收回调合同。对项目而言只有拿到目标能力的官方签名、系统支持条件和有效授权流程后才应实现ReceiveEnvelope的正式适配器。示例没有写任何未经确认的碰一碰回调方法名。## 六、这道门禁能保证什么这套方案最有用的地方是把“通知多次抵达”与“业务多次保存”切开。应用可以预先对正常、重复、过期三个分支作单元演示前端也可以如实显示待确认数量和按钮状态。它不能证明底层物理碰一碰发生了几次也不能代替对消息来源和传输内容的安全校验。如果需要上线优先补齐事务型幂等登记、可信过期时间、确认后的实际保存结果、会话归属验证以及回执退订时的生命周期清理。先把数据责任说清楚再接系统能力往往比在 UI 上追逐一个“成功”标签更可靠。资料核对华为开发者文档中心Share Kithttps://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 编译、设备传输或实际持久化验证。