羽球搭子 HarmonyOS 实战(21):云同步失败记录与重试队列

📅 2026/7/23 17:09:40 👁️ 阅读次数 📝 编程学习
羽球搭子 HarmonyOS 实战(21):云同步失败记录与重试队列

一、网络失败不能把球场上的比分一起回滚

实时计分发生在球场,网络却可能随时切换、变弱或完全中断。点击保存后,如果应用先等云端成功再写本地,失败会让用户怀疑本次保存的比分是否丢失;如果失败后无条件重建云端对局,又可能出现重复房间和重复事件。

羽球搭子的顺序是本地优先:比分变更先进入SessionStore并持久化,随后尝试提交云端事件。提交失败时,不回滚本地结果,而是记录一条“哪个对局的哪个场次同步失败”。页面据此显示明确提示,用户恢复网络后从云端协作入口执行一次完整同步。

这里的“重试队列”是轻量失败清单,不是常驻后台任务调度器。它保存可观察的失败项并去重,真正的重试由用户入口触发。这样的边界与现有实现一致,也避免在没有后台任务、退避算法和持久化任务协议时夸大能力。

二、本地保存与云端提交分成两个结果

计分页先得到本地变更对象,其中包含对局 ID、场次 ID、双方比分和结束时间。只有本地保存成功,才尝试生成云端事件。网络失败改变的是“云端是否收敛”,而不是“本机是否记住比分”。

interface ScoreChange { sessionId: string matchId: string scoreA: number scoreB: number finishedAt: number } function saveScoreLocally( sessionId: string, matchId: string, scoreA: number, scoreB: number ): ScoreChange | undefined { const detail = SessionStore.getDetail(sessionId) if (detail === undefined) { return undefined } const next = updateMatchScore(detail, matchId, scoreA, scoreB) SessionStore.saveDetail(next.detail) return next.change }

这段逻辑把本地数据当作现场操作的第一真相源。云端同步仍然重要,因为另一台设备需要看到最新比分;但它不应该让单机核心流程依赖瞬时网络。

阶段成功结果失败结果用户是否能继续计分
本地校验得到规范比分变更拒绝不存在的场次否,先修正输入
本地持久化摘要和详情同时更新保留旧值并提示否,避免假成功
云端事件提交版本推进并清除失败项记录失败项
主动重试云端与本地场次收敛保留失败提示

三、失败项使用复合键去重

失败记录至少需要sessionIdmatchId、提示信息和更新时间。添加记录时先解析本地 ID 与云端 ID 的别名,再用“规范对局 ID + 场次 ID”查找旧项。存在就覆盖时间和信息,不存在才追加。

interface SyncFailure { sessionId: string matchId: string message: string updatedAt: number } function markFailure( sessionId: string, matchId: string, message: string ): void { const resolvedId = CloudStateStore.resolveSessionId(sessionId) const failures = listFailures().slice() const index = failures.findIndex((item) => CloudStateStore.resolveSessionId(item.sessionId) === resolvedId && item.matchId === matchId ) const next = { sessionId: resolvedId, matchId, message, updatedAt: Date.now() } if (index >= 0) failures[index] = next else failures.push(next) AppStorage.setOrCreate<SyncFailure[]>('cloud_score_sync_failures', failures) }

复合键避免同一个失败因连续保存而堆出多条提示。更新时间仍会刷新,让页面知道最近一次同步尝试发生在何时。失败数组存在 AppStorage 中,进程重启后不会自动恢复,因此它代表运行期提示,不应当被描述成可靠持久化消息队列。

四、提交成功只清理对应场次

同步结果必须精确清理。如果对局中有两场比赛同时失败,A 场重新提交成功不能把 B 场提示一起消掉。成功时按复合键删除;整场对局完成完整同步或被归档后,才按 sessionId 清理全部关联项。

function clearFailure(sessionId: string, matchId: string): void { const resolvedId = CloudStateStore.resolveSessionId(sessionId) const next = listFailures().filter((item) => { const sameSession = CloudStateStore.resolveSessionId(item.sessionId) === resolvedId return !(sameSession && item.matchId === matchId) }) AppStorage.setOrCreate<SyncFailure[]>('cloud_score_sync_failures', next) } function clearSessionFailures(sessionId: string): void { const resolvedId = CloudStateStore.resolveSessionId(sessionId) const next = listFailures().filter((item) => CloudStateStore.resolveSessionId(item.sessionId) !== resolvedId ) AppStorage.setOrCreate<SyncFailure[]>('cloud_score_sync_failures', next) }

清理动作本身也应当是幂等的。找不到目标项时返回同一个数组语义,不抛出异常,不把一次成功同步变成新的 UI 错误。

五、409 冲突先拉取版本,再重放事件

网络恢复后不代表原事件仍能直接提交。另一台设备可能已经推进服务端版本,旧请求携带的baseVersion会得到 409。仓库层读取服务端版本,更新本地云状态,再用新的客户端事件 ID 重放一次。

async function submitWithConflictRetry( change: ScoreChange, retryCount: number = 0 ): Promise<boolean> { try { const result = await CloudRepository.recordScore(change) CloudStateStore.updateVersion(change.sessionId, result.version) clearFailure(change.sessionId, change.matchId) return true } catch (error) { const serverVersion = readConflictVersion(error) if (serverVersion <= 0 || retryCount >= 1) { markFailure(change.sessionId, change.matchId, friendlyMessage(error)) return false } CloudStateStore.updateVersion(change.sessionId, serverVersion) return CloudRepository.recordScore({ ...change, clientEventId: createRetryEventId(change), baseVersion: serverVersion }).then(() => true).catch(() => false) } }

冲突重试只能有限次数。持续 409 可能意味着本地快照已经严重落后,正确做法是拉取服务端详情并重新合并,而不是无限循环发送。

六、手动重试选择“完整场次同步”

运行期失败项只记录了需要提示的比分事件,并没有保存一份可跨重启执行的命令对象。因此“我的 > 云端协作”中的重试入口选择同步当前完整对局:若本地场次尚未建立云端映射,先创建;已存在则比较版本、提交差异或拉取最新详情。

async function retryActiveCloudSync(): Promise<void> { if (cloudBusy || !AuthSessionStore.isSignedIn()) { return } const sessionId = pickActiveSessionId() if (sessionId.length === 0) { showMessage('暂无需要同步的对局') return } cloudBusy = true try { const synced = await CloudRepository.syncLocalSession(sessionId) if (!synced) throw new Error('sync failed') clearSessionFailures(sessionId) reloadCloudState() showMessage('云端同步已重试') } finally { cloudBusy = false } }

完整同步比盲目重发旧事件更符合当前数据模型:本地对局详情仍然存在,仓库层可以重新计算需要提交的内容。未来若要实现自动后台重试,才需要持久化命令、退避时间、最大次数、网络约束和幂等键。

七、提示要告诉用户“本地仍然安全”

错误文案应明确两件事:云端同步失败,但比分已经保存在本地;用户可以稍后重新保存,或到云端协作入口主动重试。只写“请求失败”会让用户不敢离开页面,也无法判断是否需要重新计分。

页面状态文案重点可操作入口禁止行为
单场同步失败本地已保存、云端未同步重新保存或稍后重试自动回滚比分
当前对局存在失败项展示“重试云端同步”完整同步当前对局重复创建本地对局
重试进行中防止连续点击按钮禁用并发发起两次同步
重试成功清除失败提示返回正常状态保留陈旧红色告警
仍然失败保持本地数据和提示稍后再试无限快速重试

页面通过失败数组派生按钮是否显示,而不是维护另一枚容易漂移的布尔值。只要数组中还有当前对局的项目,入口就保持可见。

八、断网、冲突和冷启动分别验收

第一组测试在实时计分页断网后修改比分,确认本地页面立即更新、重进页面仍能看到比分,并出现同步失败提示。恢复网络后点击主动重试,服务端详情与本地一致,提示消失。

第二组测试使用两台设备制造版本冲突:设备 A、B 同时进入同一对局,A 先改分,B 基于旧版本提交。仓库层应读取 409 中的服务端版本并执行有限重试;若仍不能收敛,保留失败项而不是覆盖对方结果。

第三组测试结束进程再启动。比赛数据应从 Preferences 恢复,而运行期失败提示可能为空;此时用户仍可从云端协作页主动同步当前场次。这个结果清楚反映当前实现边界,不把 AppStorage 失败清单误当持久任务。

网络请求与状态管理的实现细节应以 HarmonyOS Network Kit 官方指南 为准,同时结合服务端幂等和版本冲突协议设计。

九、总结

云同步失败处理的核心顺序是:本地先保存,云端后提交;失败项按对局和场次去重;成功只清理对应项;用户通过明确入口同步完整场次;版本冲突执行有限重试。这样弱网不会破坏现场计分,也不会因连续点击制造重复对局。

当前失败数组是运行期可观察清单,而不是后台持久化任务系统。把边界讲清楚,反而能让后续演进更稳:需要自动重试时,再补持久化命令、退避、网络约束和可审计的幂等键,而不是把 UI 提示数组直接扩成不可靠的调度器。