施工现场的进度台账不能把网络稳定当作默认条件。东区一层主体结构施工进入混凝土浇筑阶段后,手机和平板可能在楼层之间切换,现场网络时断时续。项目经理需要在无网络时继续查看10月26日至11月1日的周计划;网络恢复后,再把服务器上新增的进度事件合并进来,同时保护现场尚待提交的修改。把每次恢复网络都处理成“全量替换列表”,界面看似很快变新,实际容易覆盖离线草稿,也可能导致选中的ACT-02任务跳回默认位置。

这一问题与前六篇相连:日历日序数定义时间轴坐标,稳定任务ID保证局部刷新,折叠分组控制可见行,时间窗口负责裁剪,不改变原始任务日期。现在需要给这套模型增加持久快照、服务器同步水位、本地待提交队列和冲突合并规则。沿用ACT-01基础准备、ACT-02混凝土浇筑、ACT-03钢筋安装、ACT-04隐蔽验收四项任务,重点观察ACT-02从修订8的45%收到修订9的50%时,离线编辑和服务器变更应该怎样处理。

施工时间轴离线缓存与增量合并

快照、水位与待提交修改必须独立保存

一个可恢复的离线状态至少由三部分组成。activities是当前可阅读的任务快照,记录每个任务的id、start、end、progress和revision;serverCursor是服务器下发的增量位置,示例值为208;pending保存用户在离线期间产生的修改意图,包括事件ID、任务ID、基础修订和待修改字段。不要把“最近刷新时间”误当同步水位,因为时间戳可能受时钟偏差影响,而服务器游标应有明确的顺序和协议语义。

快照与草稿也不能只用同一份progress字段表示。假设ACT-02远端的修订8对应45%,现场人员离线填写50%,本地草稿记录baseRevision:8和patch:{progress:50}。此时界面可以提示“本地待同步”,但基准快照仍是修订8;如果服务器先把记录改到了修订9、进度50%,应当核查双方意图是否相同,再决定标记已解决或保留待核对状态。直接以客户端时间靠后的记录覆盖远端,会隐藏更早发生却刚送达的重要变更。

适合持久化的数据包括任务主键、服务器修订、任务日期、分组、同步水位与本地待提交事件。页面是否展开某个抽屉、当前悬浮提示是否显示等,可以保留在UI状态中。华为开发者的数据存储方案文档区分了PersistentStorage、Preferences和关系型数据库等类型;对于需要按任务ID查询且具备事务一致性的进度台账,关系型持久层通常更适合。下面使用可执行Node.js业务代码表达合并规则,不把文件写入函数当作HarmonyOS系统接口。

增量事件只按稳定ID和修订前进

服务器给出的增量应该是一组带有水位和任务修订的事件。eventId用于防止相同变更重复应用,revision用于判断同一任务内的新旧版本,cursor表示事件在该服务端增量流中的位置。三者语义不同:事件可能被重传,不能因为游标变大就跳过任务修订判断;任务版本可能连续增长,也不应依赖某台设备的接收时间决定谁更新谁。

'use strict';
const assert = require('node:assert/strict');
function mergeSnapshot(snapshot, events) {
  const byId = new Map(snapshot.activities.map(item => [item.id, { ...item }]));
  const seen = new Set(snapshot.seenEventIds);
  let cursor = snapshot.serverCursor;
  for (const event of events) {
    if (!Number.isInteger(event.cursor)) throw new Error('无效水位');
    if (event.cursor <= cursor) continue;
    if (event.cursor !== cursor + 1) throw new Error('增量水位存在缺口');
    if (!seen.has(event.eventId)) {
      const old = byId.get(event.id);
      if (old && event.revision > old.revision) {
        byId.set(event.id, { ...old, ...event.fields, revision: event.revision });
      }
      seen.add(event.eventId);
    }
    cursor = event.cursor;
  }
  return { ...snapshot, serverCursor: cursor,
    seenEventIds: [...seen], activities: [...byId.values()] };
}
const base = { serverCursor: 208, seenEventIds: [], activities: [
  { id: 'ACT-02', progress: 45, revision: 8, start: '2026-10-28', end: '2026-10-30' },
  { id: 'ACT-03', progress: 20, revision: 3, start: '2026-10-29', end: '2026-10-31' }
]};
const next = mergeSnapshot(base, [
  { cursor: 209, eventId: 'ev-209', id: 'ACT-02', revision: 9, fields: { progress: 50 } },
  { cursor: 210, eventId: 'ev-210', id: 'ACT-03', revision: 3, fields: { progress: 10 } }
]);
assert.equal(next.activities[0].progress, 50);
assert.equal(next.activities[1].progress, 20);
assert.equal(next.serverCursor, 210);

这一段Reducer先复制快照,再按任务ID合并,避免局部改动破坏其余活动的引用。为了方便讲解,代码假设服务端按升序返回同一条增量流中的完整连续批次。真实协议还需确认游标是否存在间隔、是否支持分页、事件是否可重排、删除记录如何表达。若服务端无法保证当前批次从208之后连续可用,就应停止推进水位,转而补拉缺失范围或请求新的全量快照,不能根据最后一条事件的数字盲目提交游标。

本地草稿与远端版本冲突如何相遇

离线编辑往往比读缓存更难。工程师在ACT-02旁边填写了一条现场备注,基础修订是8;网络恢复后,服务器已经到了修订9。如果远端只改动了progress而本地只改了note,可以进入字段级别合并的审核流程;如果双方都改了进度,则应展示差异并要求业务人员决定。决定之前,不能静默覆盖任一方,同时也不要把草稿从待提交队列中删除。

'use strict';
const assert = require('node:assert/strict');
function reconcilePending(remote, pending) {
  const actions = [];
  for (const intent of pending) {
    const current = remote.find(item => item.id === intent.id);
    if (!current) { actions.push({ eventId: intent.eventId, status: 'missing' }); continue; }
    if (intent.baseRevision !== current.revision) {
      actions.push({ eventId: intent.eventId, status: 'review',
        baseRevision: intent.baseRevision, currentRevision: current.revision,
        localPatch: intent.patch });
    } else {
      actions.push({ eventId: intent.eventId, status: 'ready',
        baseRevision: current.revision, patch: intent.patch });
    }
  }
  return actions;
}
const remote = [{ id: 'ACT-02', revision: 9, progress: 50 }];
const pending = [{ eventId: 'local-1', id: 'ACT-02', baseRevision: 8,
  patch: { note: '现场复核强度数据' } }];
const actions = reconcilePending(remote, pending);
assert.equal(actions[0].status, 'review');
assert.deepEqual(actions[0].localPatch, pending[0].patch);

这里的ready是“满足本地提交前置条件”,不等于服务端已接受。真正提交必须携带稳定操作ID和期望修订,由服务端完成原子比较与更新。只有收到明确回执,才能将该本地事件从待提交队列移入历史;遇到请求超时应查询操作ID的最终状态,而不是生成一个新ID无条件重复提交。对于已删除任务、人员权限变更或计划已冻结等情形,返回明确的待核对结果,绝不能仅按数字版本继续写入。

开发环境中的快照合并与同步水位管理

快照与水位的提交顺序必须一致

服务器返回两个事件时,如果应用先保存serverCursor=210,随后在写ACT-02的修订9时崩溃,下次启动会从210之后继续拉取,修订9就永久缺席。反过来,如果先完成活动状态写入却还没更新游标,重启后再收到相同事件,通过事件ID和修订比较仍可安全忽略重复。这提示我们把任务记录、处理过的事件ID和同步水位放到同一个原子提交单元里。

下面的Node.js脚本将整个快照作为一个文件对象替换,便于理解“同时提交”的边界。在HarmonyOS应用里,更适合将活动表、增量事件表、水位表封装在同一关系型存储事务中,并为异常恢复设计一致性检查。文件替换策略还需结合目标文件系统、进程并发以及刷盘要求完善,不能简单等同于任意环境下的数据库事务。

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
function saveSnapshot(file, snapshot) {
  const temp = file + '.pending';
  if (fs.existsSync(temp)) fs.unlinkSync(temp);
  const bytes = JSON.stringify(snapshot);
  try {
    fs.writeFileSync(temp, bytes, { flag: 'wx', encoding: 'utf8' });
    fs.renameSync(temp, file);
  } finally {
    if (fs.existsSync(temp)) fs.unlinkSync(temp);
  }
}
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'timeline-'));
const file = path.join(dir, 'snapshot.json');
try {
  saveSnapshot(file, { serverCursor: 210, activities: [{ id: 'ACT-02', revision: 9 }] });
  assert.equal(JSON.parse(fs.readFileSync(file, 'utf8')).serverCursor, 210);
} finally {
  fs.rmSync(dir, { recursive: true, force: true });
}

持久层除了数据内容,还应保存模式版本。后续字段变化时,启动时先核对快照模式,再决定迁移或重新拉取。数据量较大时不要在界面线程逐条转换全部记录,应以分页或批次方式读取;但每一次批次提交都应满足自己的事务边界。敏感的人员信息与现场照片地址要遵守应用权限及存储安全级别,不把服务器访问凭证与普通进度缓存混在一起。

界面何时展示离线、何时标记冲突

离线不等于数据不可查看,也不等于可以随意编辑。项目时间轴可以继续展示上一次成功保存的ACT-01~04,其中ACT-02显示45%、修订8;网络恢复后收到修订9,变为50%。如果有两条待提交事件,界面应标注“待同步2项”,并在ACT-02旁明确提示最新服务端修订和本地草稿状态。离线用户仍能滑动10月26日至11月1日的周视图,因为日期归一和区间裁剪均是本地业务逻辑。

施工进度离线快照与增量合并状态

当页面离开时,应停止只服务于当前页面的定时刷新、取消监听并清理大图表临时节点,但已写入的任务快照、操作队列及服务器水位必须保留。下次进入先从缓存快速还原任务列表,再根据网络、鉴权与当前服务端游标决定补拉范围。若响应中途失败,不能只更新一部分记录却推进水位;若请求返回的是旧查询代际,则应忽略其UI提交,避免新登录会话被旧会话数据覆盖。

排查时建议记录serverCursorBefore、serverCursorAfter、eventId、activityId、revision、pendingCount及事务提交结果。出现进度倒退,先核查是否把修订9覆盖成8;出现重复本地操作,先查看事件ID是否在超时重试时被重新生成;出现任务突然消失,再检查删除事件语义和缓存模式迁移。选中任务与滚动锚点依旧按ACT-02这样的稳定ID维护,网络变化不应直接重置界面的位置。

可参考华为开发者官方的数据存储方案选择、跨设备数据分级访问控制与ArkData接口版本差异。本篇将增量传输协议与平台存储实现分开:平台负责可靠的数据存取和访问控制,应用负责修订比较、草稿冲突、事件去重和水位推进。这样即使网络状态频繁变化,施工时间轴也能保持可解释的任务版本与连续的现场操作记录。

Logo

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

更多推荐