HarmonyOS 7 排队异常与多任务一致性:连载07(共08篇)【共创稿事节】
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 开发框架、焦点控制。
更多推荐


所有评论(0)