:云同步失败记录与重试队列)
一、网络失败不能把球场上的比分一起回滚实时计分发生在球场网络却可能随时切换、变弱或完全中断。点击保存后如果应用先等云端成功再写本地失败会让用户怀疑本次保存的比分是否丢失如果失败后无条件重建云端对局又可能出现重复房间和重复事件。羽球搭子的顺序是本地优先比分变更先进入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 }这段逻辑把本地数据当作现场操作的第一真相源。云端同步仍然重要因为另一台设备需要看到最新比分但它不应该让单机核心流程依赖瞬时网络。阶段成功结果失败结果用户是否能继续计分本地校验得到规范比分变更拒绝不存在的场次否先修正输入本地持久化摘要和详情同时更新保留旧值并提示否避免假成功云端事件提交版本推进并清除失败项记录失败项是主动重试云端与本地场次收敛保留失败提示是三、失败项使用复合键去重失败记录至少需要sessionId、matchId、提示信息和更新时间。添加记录时先解析本地 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.setOrCreateSyncFailure[](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.setOrCreateSyncFailure[](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.setOrCreateSyncFailure[](cloud_score_sync_failures, next) }清理动作本身也应当是幂等的。找不到目标项时返回同一个数组语义不抛出异常不把一次成功同步变成新的 UI 错误。五、409 冲突先拉取版本再重放事件网络恢复后不代表原事件仍能直接提交。另一台设备可能已经推进服务端版本旧请求携带的baseVersion会得到 409。仓库层读取服务端版本更新本地云状态再用新的客户端事件 ID 重放一次。async function submitWithConflictRetry( change: ScoreChange, retryCount: number 0 ): Promiseboolean { 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(): Promisevoid { 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 提示数组直接扩成不可靠的调度器。