
HarmonyOS 表单重复保存怎么治提交锁挡点击幂等键兜住重试摘要保存按钮禁用只能拦住当前页面的一部分连点网络重试、页面恢复和重复事件仍可能把同一份数据写入两次。本文以课程反馈为例把一次提交标识为稳定幂等键页面用提交锁保护交互服务层复用幂等键Repository 按键查找后更新或插入最终让“同一份业务意图”只落一条记录。一次慢网提交最容易暴露问题用户点“保存”按钮还没来得及变灰就连点两次或者请求完成前页面退到后台恢复后又触发一次保存。列表出现两条相同反馈时先别急着加更多if先问清楚两次请求是不是同一份业务意图本文解决四件事让按钮在执行期间只接受一次点击。让一次编辑会话拥有可复用的幂等键。让 Repository 用幂等键完成“查找后更新或插入”。在失败、重试和返回页面时保留正确的处理边界。先看清“重复”究竟从哪来页面连点只是最显眼的一种来源。下面四种路径如果没有共同的业务身份都会被误当作两次新建。触发源典型表现应由谁处理用户连点一秒内发出两次保存页面提交锁网络重试同一请求重新发送服务层复用同一幂等键生命周期恢复页面恢复后重复执行提交回调页面仅恢复状态不重建 payload多入口调用草稿页和编辑页都能保存Repository 按幂等键去重只靠禁用按钮时后三种仍会漏掉。更稳的设计是交互层减少重复数据层定义重复。幂等键要标识一次意图不要标识一次点击幂等键不是每点一次按钮都重新生成的随机数。它应在用户创建这份草稿或进入编辑会话时生成并随 payload 一直传到持久层。exportinterfaceSaveFeedbackPayload{idempotencyKey:stringcourseId:stringcontent:stringupdatedAt:number}exportinterfaceFeedbackRecordextendsSaveFeedbackPayload{id:stringcreatedAt:number}idempotencyKey表示“保存这份反馈”的业务意图id才是落库后的记录身份。用户修正文字后再次保存仍应使用同一把幂等键以更新原记录而不是新增一条。在进入编辑页时生成并保留键把键放在页面会话或草稿模型中而不是写在点击事件里。下面用一个简单工厂表达边界生产项目可替换为 UUID 生成器。exportclassFeedbackDraftFactory{create(courseId:string):SaveFeedbackPayload{constnowDate.now()return{idempotencyKey:feedback_${courseId}_${now},courseId,content:,updatedAt:now}}}页面重建草稿前应优先恢复已有草稿及其idempotencyKey。否则“返回后继续编辑”会被伪装成一份新提交Repository 无从判断它是否重复。页面锁只负责交互时序提交锁应在调用服务之前设置并在finally里释放。它不承担持久化去重职责不能因为有锁就取消数据层的幂等设计。StateprivateisSubmitting:booleanfalseprivateasyncsubmitFeedback():Promisevoid{if(this.isSubmitting||!this.isDraftValid()){return}this.isSubmittingtruetry{awaitthis.feedbackService.save(this.draft)this.showSavedToast()this.navPathStack.pop()}catch(error){this.showSaveError()}finally{this.isSubmittingfalse}}按钮也要绑定同一个状态避免视觉上还能点、逻辑上却被吞掉Button(this.isSubmitting?保存中…:保存).enabled(!this.isSubmittingthis.isDraftValid()).onClick(()this.submitFeedback())服务层只编排校验和写入服务先校验业务输入再把完整 payload 交给 Repository。不要让 ArkUI 页面直接操作存储也不要在 Repository 里拼页面提示文案。exportclassFeedbackService{constructor(privatereadonlyrepository:FeedbackRepository){}asyncsave(payload:SaveFeedbackPayload):PromiseFeedbackRecord{constcontentpayload.content.trim()if(content.length0){thrownewError(反馈内容不能为空)}returnthis.repository.upsertByIdempotencyKey({...payload,content,updatedAt:Date.now()})}}这一层信任的是已存在的幂等键和经过校验的内容它阻止空内容或临时 UI 状态直接写入并把持久化细节留在下一层。Repository 用 upsert 收口重复写入无论底层是 RDB、Preferences 还是远端接口关键语义都是“同一个幂等键只对应一条记录”。下面用内存数组展示最小算法。asyncupsertByIdempotencyKey(payload:SaveFeedbackPayload):PromiseFeedbackRecord{constexistingthis.records.find(itemitem.idempotencyKeypayload.idempotencyKey)if(existing){constupdated{...existing,...payload}this.replace(updated)returnupdated}constcreated:FeedbackRecord{...payload,id:this.idGenerator.next(),createdAt:Date.now()}this.records.push(created)returncreated}如果接的是服务端幂等键还应写进请求头或请求体并让服务端建立唯一约束本地锁无法阻止两个不同设备同时提交。失败时保留草稿不要悄悄重建写入失败后正确的回退是保留当前草稿、展示可理解的错误并让用户主动重试。重试要复用原idempotencyKey。结果页面处理幂等键处理写入成功退出或刷新列表本次会话结束可重试失败保留内容并显示重试保持不变不可恢复失败提示原因允许返回编辑保持不变直到用户放弃草稿用户新建另一份反馈创建新草稿生成新键这样网络恢复后的重试会更新同一记录若每次失败都重新建键就等于主动绕过了幂等保护。列表刷新只读 Repository 的事实保存后不要在列表页手工push一条“猜出来的记录”再等后台写入完成。列表在onPageShow或明确的刷新事件中从服务读取才能保证新增和更新都展示同一份事实。asynconPageShow():Promisevoid{this.feedbackItemsawaitthis.feedbackService.listByCourse(this.courseId)}服务读模型是唯一来源避免“页面本地多一条、存储里又一条”的第二类重复问题。用真实节奏验证而不是只点一次保存至少覆盖下列路径连续快速点击保存最终只出现一条记录。人为让保存延迟再点击按钮按钮文案和可点击状态正确。保存失败后点击重试记录数量仍为一。保存成功返回列表、再进入列表内容与更新时间一致。退出编辑页重新创建反馈确认新草稿确实生成新键。排查存量调用时可执行rgsubmitFeedback|upsertByIdempotencyKey|idempotencyKeyentry features common rg\.push\(.*Feedback|insertFeedbackentry features common第二条搜索是为了找出绕过统一入口的旧写入路径。常见误区误区后果修复只禁用按钮重试与多入口仍可重复写入Repository 按键 upsert每次点击生成键重试变成新建草稿创建时生成并复用成功后列表手工追加页面与存储容易不一致从服务重新读取失败后清空表单用户丢失内容且无法安全重试保留草稿和原键小结重复保存的解决方案不是一个按钮状态而是两层边界页面锁负责一次交互中的节流幂等键负责整条写入链路的业务去重。只要每个保存入口最终都走同一个upsertByIdempotencyKey连点、重试和恢复就不会轻易制造两条记录。在接入服务端时还应把幂等键与当前用户、业务类型一起纳入唯一约束仅按随机键去重无法处理错误复用。这样同一用户的同一次保存可安全重放不同用户的正常提交仍互不影响。