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

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

羽球搭子的顺序是本地优先:比分变更先进入 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 提示数组直接扩成不可靠的调度器。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐