HarmonyOS 7 排队异常与多任务一致性:连载07(共08篇)【共创稿事节】

排队信息更新看起来只是把旧序号替换成新序号,但展览入场系统往往同时存在网络重试、活动切换、多个凭证和窗口恢复等情况。如果事件先后顺序发生变化,主应用显示“请前往入口”,闪控球仍停留在“排队中”,用户就可能错过叫号。更棘手的是同一版本收到两份内容不同的结果:这不是简单的刷新延迟,而是需要明确处置的数据冲突。

前面的工程已经形成 EntryPhase、QueueSnapshot、reduceQueue() 和 QueueStore,并让主应用、闪控球与闪控窗订阅同一份业务快照。本篇继续沿用城市艺术夜展的 entry-7264 凭证,但关注的是异常事件如何进入现有业务链路,以及多场活动同时存在时怎样保持状态彼此独立。

排队异常归约与多窗口一致性

一、用凭证、版本和业务阶段确定唯一状态

处理队列事件时需要区分三个维度。ticketId 负责将消息定位到正确的入场凭证;seq 负责区分同一凭证消息的新旧;phase 则决定入场业务的合法流转。三者缺一不可。仅比较消息抵达时间会受网络重传影响,仅比较排队数字则无法识别重新编排队列后的序号变化。对于已经到达 ADMITTED、CANCELLED 或 EXPIRED 的凭证,后续阶段变更必须经过明确的业务重新授权流程。

一个容易忽略的情况是序列出现缺口。例如本地记录 seq=40,接着收到 seq=43。这条消息可能包含最新完整快照,也可能只是一次增量更新;没有确定消息协议之前,直接接纳可能跳过“已叫号”或者“已取消”等关键事实。这里采用稳妥的策略:增量事件必须连续,发现缺口就要求读取权威快照;读取完成后再通过独立的快照校验入口更新。这样还可以把需要人工关注的差异与普通网络抖动分开。

情况处理动作用户可见信息
相同版本、相同内容当作重复投递保持当前进度
低版本消息丢弃旧变更保持当前进度
相同版本、内容不同标记冲突显示“正在核对状态”
版本不连续请求完整快照继续显示最后有效信息
凭证不匹配拒绝写入其他活动不受影响
非法阶段跳转保留旧状态并核对避免错误展示入场结果

二、给现有归约器增加可诊断的守卫

之前的 reduceQueue() 用返回旧对象的方式拒绝不合法输入,这对保证状态安全很有帮助,但不方便上层判断究竟是重复事件、越序消息还是业务冲突。现在在归约器外再加一层守卫,只负责分类与决策,不修改底层的阶段规则。

// logic/QueueConsistencyGuard.ets
import { QueueEvent, QueueSnapshot } from '../model/QueueModel';
import { reduceQueue } from './QueueReducer';

export enum ReviewCode {
  ACCEPT = 'ACCEPT', DUPLICATE = 'DUPLICATE',
  STALE = 'STALE', CONFLICT = 'CONFLICT',
  GAP = 'GAP', INVALID = 'INVALID'
}

export interface GuardResult {
  code: ReviewCode;
  value: QueueSnapshot;
}

function sameBusiness(a: QueueSnapshot, e: QueueEvent): boolean {
  return a.phase === e.phase && a.position === e.position &&
    a.waitMinutes === e.waitMinutes;
}

export function guardQueue(current: QueueSnapshot,
  event: QueueEvent): GuardResult {
  if (current.ticketId !== event.ticketId) {
    return { code: ReviewCode.INVALID, value: current };
  }
  if (!Number.isSafeInteger(event.seq) || event.seq < 0) {
    return { code: ReviewCode.INVALID, value: current };
  }
  if (event.seq < current.seq) {
    return { code: ReviewCode.STALE, value: current };
  }
  if (event.seq === current.seq) {
    return { code: sameBusiness(current, event) ?
      ReviewCode.DUPLICATE : ReviewCode.CONFLICT, value: current };
  }
  if (event.seq > current.seq + 1) {
    return { code: ReviewCode.GAP, value: current };
  }
  const next: QueueSnapshot = reduceQueue(current, event);
  return next === current ?
    { code: ReviewCode.INVALID, value: current } :
    { code: ReviewCode.ACCEPT, value: next };
}

这里比较的是阶段、位置和预计等待时间等业务内容。两次投递的本地接收时间即使不同,只要同一序列对应的业务内容一致,就不必触发界面刷新。reduceQueue 仍承担数值合法性检查与终止状态保护,守卫不重复维护另一份状态机。若服务端将等待时间作为独立推送通道,则应使用该通道自己的版本而非强行复用排队序列。

三、所有窗口只从协调器读取有效快照

如果每个窗口自行维护“最后一次收到的消息”,状态漂移就会持续出现。更稳妥的做法是把新事件交给单一协调器分类,只有 ACCEPT 才交给上一阶段的 QueueStore。异常判断也按 ticketId 保存,避免一场活动的异常遮住另一场活动的进度。

// logic/QueueConsistencyCoordinator.ets
import { QueueEvent, QueueSnapshot } from '../model/QueueModel';
import { queueStore } from './QueueStore';
import { guardQueue, GuardResult, ReviewCode } from './QueueConsistencyGuard';

export class QueueConsistencyCoordinator {
  private notices: Map<string, string> = new Map();

  ingest(event: QueueEvent): ReviewCode {
    const current: QueueSnapshot | undefined =
      queueStore.snapshot(event.ticketId);
    if (!current) {
      this.notices.set(event.ticketId, '请重新读取入场凭证');
      return ReviewCode.INVALID;
    }
    const result: GuardResult = guardQueue(current, event);
    if (result.code === ReviewCode.ACCEPT) {
      queueStore.ingest(event);
      this.notices.delete(event.ticketId);
    } else if (result.code === ReviewCode.GAP ||
      result.code === ReviewCode.CONFLICT ||
      result.code === ReviewCode.INVALID) {
      this.notices.set(event.ticketId, '状态需要刷新核对');
    }
    return result.code;
  }

  notice(ticketId: string): string {
    return this.notices.get(ticketId) ?? '';
  }

  resetNotice(ticketId: string): void {
    this.notices.delete(ticketId);
  }
}
export const queueCoordinator = new QueueConsistencyCoordinator();

QueueStore 在同一执行上下文中同步处理一次事件与版本比较,既能降低推送与主动刷新交叉触发时的竞争,也能使后续窗口收到的是已经归约的数据。服务端完整快照的读取应交给独立仓储:先检查凭证所有权、活动关联和版本,再经明确的快照恢复入口落库。不能将网络请求成功直接当作入场资格有效。

开发环境中的状态守卫与业务页面

四、多活动并发时,把选择与订阅分开

同一个用户可能持有城市艺术夜展和科学互动体验日两张凭证。列表选中项属于当前页面上下文,快照则按凭证独立存储。点击另一场活动时,应先释放旧凭证的订阅,再根据新 ticketId 获取快照。如果旧网络响应在切换后到达,它仍可交给原凭证的仓储处理,却不应更新新活动的选中卡片。

// logic/QueueSelectionSession.ets
import { QueueSnapshot } from '../model/QueueModel';
import { queueStore } from './QueueStore';

export class QueueSelectionSession {
  private dispose?: () => void;
  private generation: number = 0;

  select(ticketId: string,
    present: (value: QueueSnapshot) => void): void {
    this.close();
    const currentGeneration: number = this.generation;
    this.dispose = queueStore.listen(ticketId,
      (snapshot: QueueSnapshot): void => {
        if (this.generation !== currentGeneration) return;
        if (snapshot.ticketId !== ticketId) return;
        present(snapshot);
      });
  }

  close(): void {
    this.dispose?.();
    this.dispose = undefined;
    this.generation += 1;
  }
}

窗口消失与业务取消并不是一回事。轻量窗口关闭时调用 close() 释放监听即可;业务排队资格保留在服务端与快照仓储中。重新打开页面之后,先读取当前凭证最新有效快照,再绑定订阅。需要注意,前面的 QueueStore 初始化时只装入了第一张凭证;多凭证场景应由凭证仓储为每张有效票建立初始快照,不能凭空接受陌生 ticketId 的事件。

五、冲突状态需要明确的恢复出口

发现同版本冲突时,不应立即把所有排队信息清空。界面可以继续显示最后一次有效的序号和时间,同时增加“状态需要核对”的文字提示,提供“重新获取状态”和“查看入场凭证”两个动作。刷新失败时保留原值,记录错误类别并允许再次请求;如果服务端确认凭证已失效,则应按新的权威版本更新成终止状态,而不是让客户端自行改变业务事实。

发生版本差异时的排队核对界面

诊断时建议分别检查凭证 ID、旧版本、新版本、阶段迁移、归约结果以及窗口投影版本。业务日志只保留必要的脱敏标识与数值,避免输出完整票据和个人信息。对同一张凭证可以构造以下顺序作为回归用例:seq=40 被接受、seq=39 被忽略、seq=40 同内容被去重、seq=40 不同内容触发核对、seq=42 进入补齐流程。再加入第二张凭证交错更新,观察第一张凭证是否保持不变。

对消息的处理还应区分“异常记录”与“页面告警”。同一版本的重复投递可以记入诊断计数,却不必每次打断用户;只有需要重新读取权威状态的冲突才进入告警区域。若后台持续发送相同错误,可按凭证和错误类别合并提示,避免界面反复弹出信息。用户切换活动时,告警应跟随原凭证保留,新活动只读取自己的核对状态。

对于多窗口,同一阶段内的进度变化通常属于普通更新,叫号和凭证失效则是更高优先级的业务变化。协调器可以让这两类事件走同一个版本守卫,再由展示层决定文字强调程度。任何一条消息都不应该越过状态守卫直接修改闪控球标题,这一点比某个具体窗口刷新得多快更重要。

六、收束为可复用的可靠性规则

稳定的入场状态系统不是让每个窗口都尽快显示最新一条网络消息,而是让所有界面只呈现经过统一规则确认的业务快照。版本连续性、同版本冲突、跨凭证隔离和订阅生命周期可以由一组清楚的守卫规则承担;交互层则负责向用户解释目前可操作的内容。下一篇将在这套状态基础上,解决大字号、朗读、键盘焦点、异常提示与轻量窗口可达性问题。

相关文档:ArkTS 基础与类型规范、ArkUI 开发框架、焦点控制。

Logo

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

更多推荐