HarmonyOS 7 活动入场排队状态机与轻任务模型:连载01(共08篇)【共创稿事节】
HarmonyOS 7 活动入场排队状态机与轻任务模型:连载01(共08篇)【共创稿事节】
大型展览、城市音乐节和预约制体验活动往往会把“已经预约”和“能够入场”分成两个环节。用户可能提前数小时领取了凭证,却只希望在临近入场时了解当前序号、等待时间以及是否已经被叫号。如果把这些字段直接散落到多个页面中,窗口尺寸改变、页面重新打开或进度消息先后到达时,很容易出现一处显示“排队中”,另一处却显示“已入场”的情况。
这类应用适合先建立独立的排队状态模型,再由主页面和轻量窗口分别读取同一份状态快照。HarmonyOS 7 提供了闪控窗等多形态窗口体验,但入场资格、排队序号和状态迁移首先属于业务数据,而不是某个窗口的私有属性。把状态机放在界面之外,才能为后续的闪控球和闪控窗衔接打下基础。

一、先确定排队状态的业务含义
以城市艺术夜展的电子入场凭证为例,一张凭证在生命周期中会经过预约成功、进入排队、叫号、入场等阶段,还可能因为活动结束或者用户取消而终止。状态名称要描述业务事实,不能用“弹窗打开”“按钮被点击”等界面事件代替。按钮只能提交动作请求,最终状态由被确认的业务事件更新。
| 状态 | 业务含义 | 页面展示重点 |
|---|---|---|
| RESERVED | 已领取入场资格,尚未进入排队 | 活动时间、预约信息 |
| WAITING | 处于等待队列 | 排队序号、预计等候时间 |
| CALLED | 已收到入场提醒 | 入口位置、有效提醒 |
| ADMITTED | 入场记录已确认 | 入场完成状态 |
| CANCELLED | 预约或队列已经取消 | 取消结果与重新预约入口 |
| EXPIRED | 超出规定的使用时段 | 凭证失效原因 |
这里有两个值得明确的设计点。其一,WAITING 可以多次收到序号或等候时间变化,但阶段本身无需反复切换。其二,ADMITTED、CANCELLED 和 EXPIRED 是本次凭证的终止阶段,后续普通排队数据不能重新把它们变成等待中。确有业务恢复需求时,应由服务端产生新的凭证或者明确的恢复流程,而不是前端自行修改枚举值。
排队序号也应与“前方还有多少人”分开。序号 28 不必然意味着前面还有 27 人,队伍可能包含分时段分组、不同入口或临时优先通道。预计等候时间同样不是用本地倒计时当成权威值,而是由经过计算的数据快照提供。页面可以提示更新时间,避免用户把过期数据当成最新结果。
二、模型只保留可信业务字段
工程可按 model/QueueModel.ets、logic/QueueReducer.ets、pages/Index.ets 三层拆分。模型定义状态与消息字段;归约器负责决定是否接纳一条更新;页面只负责呈现结果。这样后续切换到紧凑窗口时,不必复制状态判定代码。
model/QueueModel.ets:
export enum EntryPhase {
RESERVED = 'RESERVED',
WAITING = 'WAITING',
CALLED = 'CALLED',
ADMITTED = 'ADMITTED',
CANCELLED = 'CANCELLED',
EXPIRED = 'EXPIRED'
}
export interface QueueSnapshot {
ticketId: string;
eventId: string;
phase: EntryPhase;
position: number;
waitMinutes: number;
seq: number;
updatedAt: number;
}
export interface QueueEvent {
ticketId: string;
phase: EntryPhase;
position: number;
waitMinutes: number;
seq: number;
updatedAt: number;
}
export function initialSnapshot(): QueueSnapshot {
return {
ticketId: 'entry-7264', eventId: 'art-night',
phase: EntryPhase.RESERVED, position: 0,
waitMinutes: 0, seq: 1, updatedAt: 1793475000000
};
}
export function phaseLabel(phase: EntryPhase): string {
switch (phase) {
case EntryPhase.RESERVED: return '已预约';
case EntryPhase.WAITING: return '排队中';
case EntryPhase.CALLED: return '请前往入口';
case EntryPhase.ADMITTED: return '已入场';
case EntryPhase.CANCELLED: return '已取消';
default: return '凭证已失效';
}
}
ticketId 是排队业务的稳定关联键,eventId 用于关联活动详情;seq 是凭证维度的单调递增版本。updatedAt 作为用户可见的更新时间,不能替代版本做冲突判定:不同节点的时钟可能有偏差,而同一毫秒内也可能发生多个事件。这里用数值保存时间,避免因为显示格式差异干扰状态比较。
代码把排队位置和时间设置为非负数约束,终止状态可以统一填入 0。实际数据协议还可以增加入口分区、叫号批次和失效原因字段,但应优先确认这些字段究竟来自哪一方,避免在界面中臆造业务事实。
三、用纯函数约束合法迁移
只比较最新收到的状态并直接覆盖页面,是很危险的写法。网络重试可能让旧事件重新到达,两个订阅源也可能向同一页面提供不同版本的数据。一个更稳妥的方案是让消息先通过纯函数校验,再决定是否产生新快照。
logic/QueueReducer.ets:
import { EntryPhase, QueueEvent, QueueSnapshot } from '../model/QueueModel';
function terminal(phase: EntryPhase): boolean {
return phase === EntryPhase.ADMITTED ||
phase === EntryPhase.CANCELLED || phase === EntryPhase.EXPIRED;
}
function allowed(from: EntryPhase, to: EntryPhase): boolean {
if (from === to) return true;
if (terminal(from)) return false;
if (to === EntryPhase.CANCELLED || to === EntryPhase.EXPIRED) return true;
if (from === EntryPhase.RESERVED) return to === EntryPhase.WAITING;
if (from === EntryPhase.WAITING) return to === EntryPhase.CALLED;
if (from === EntryPhase.CALLED) return to === EntryPhase.ADMITTED;
return false;
}
export function reduceQueue(oldValue: QueueSnapshot,
event: QueueEvent): QueueSnapshot {
if (event.ticketId !== oldValue.ticketId) return oldValue;
if (event.seq <= oldValue.seq || terminal(oldValue.phase)) return oldValue;
if (!allowed(oldValue.phase, event.phase)) return oldValue;
if (!Number.isFinite(event.position) || event.position < 0) return oldValue;
if (!Number.isFinite(event.waitMinutes) || event.waitMinutes < 0) return oldValue;
if (!Number.isFinite(event.updatedAt)) return oldValue;
return {
ticketId: oldValue.ticketId, eventId: oldValue.eventId,
phase: event.phase, position: event.position,
waitMinutes: event.waitMinutes,
seq: event.seq, updatedAt: event.updatedAt
};
}
该实现对同一状态内的进度变化开放更新,例如从 WAITING 的序号 28 更新到序号 24;对不允许的跨越迁移则保持原快照。如果入场服务确实会合并阶段,一次从等待中直接回传已入场,必须先调整协议和迁移表,而不能为了让页面“看起来正确”放宽所有条件。
纯函数有一个重要好处:相同输入会得到相同输出。处理重复事件、倒序事件、跨凭证事件时,无需先启动任何页面就能构造输入数据,检查输出的 seq、phase、position 是否符合预期。新增窗口形态也不会改变这条规则。
四、ArkUI 页面如何消费快照
页面的 @State 保存当前不可变快照,接收到新版本后整体赋值。初学者常见的写法是直接修改对象的深层字段,再依赖界面自动刷新;这种处理容易引入不同状态管理机制下的观察差异。用整体替换的方式,页面刷新意图更清晰。
pages/Index.ets:
import { EntryPhase, QueueEvent, QueueSnapshot,
initialSnapshot, phaseLabel } from '../model/QueueModel';
import { reduceQueue } from '../logic/QueueReducer';
@Entry
@Component
struct Index {
@State snapshot: QueueSnapshot = initialSnapshot();
private next: number = 0;
private records: QueueEvent[] = [
{ ticketId: 'entry-7264', phase: EntryPhase.WAITING,
position: 28, waitMinutes: 12, seq: 2, updatedAt: 1793475060000 },
{ ticketId: 'entry-7264', phase: EntryPhase.WAITING,
position: 24, waitMinutes: 9, seq: 3, updatedAt: 1793475120000 },
{ ticketId: 'entry-7264', phase: EntryPhase.CALLED,
position: 0, waitMinutes: 0, seq: 4, updatedAt: 1793475180000 }
];
private readNext(): void {
if (this.next >= this.records.length) return;
this.snapshot = reduceQueue(this.snapshot, this.records[this.next]);
this.next += 1;
}
build() {
Column({ space: 18 }) {
Text('城市艺术夜展').fontSize(26).fontWeight(FontWeight.Bold)
Text(phaseLabel(this.snapshot.phase)).fontSize(24)
.fontColor('#1265C8')
if (this.snapshot.phase === EntryPhase.WAITING) {
Text(`当前序号:${this.snapshot.position}`).fontSize(34)
Text(`预计等候:${this.snapshot.waitMinutes} 分钟`).fontSize(20)
}
Text(`进度版本:${this.snapshot.seq}`).fontSize(14)
Button('读取下一条状态记录').onClick(() => this.readNext())
}
.width('100%').padding(24)
}
}
这里的记录数组是本地示例数据,用来观察合法迁移、连续进度更新与叫号后的显示变化。接入实际消息源时,应把 readNext() 的输入替换为经过解析、校验和鉴权的队列事件,仍复用 reduceQueue()。不要让 UI 按钮直接把阶段改成已入场,否则服务端确认与屏幕文字会失去因果关系。

五、为多种窗口形态保留同一份状态
闪控球的内容通常适合提炼成一个状态和一个数字,例如“排队中 · 28”;闪控窗可展示活动名称、预计等待、入口说明;主页面则保留票据详情与取消预约等复杂操作。三者的内容密度不同,却应该从同一份 QueueSnapshot 导出。推荐将显示文案、强调色、操作可用性封装成选择函数,而不是在不同窗口各维护一套独立状态机。
当主页面重新打开时,先展示最近一次持有的快照,再请求权威最新版本;如果返回事件版本较旧,归约器会保持现有状态。对于入场后仍有旧消息抵达的情形,终止状态保护同样能够减少错误回跳。窗口收起与展开只是呈现方式变化,不应触发排队身份重新生成。
真实业务中还要处理取消请求。用户点击取消后,页面可以暂时禁用重复提交,但不能立刻把 phase 写成 CANCELLED;只有请求收到确认,或者服务端事件到达,才更新凭证状态。若取消和叫号同时发生,应使用服务端最终决议及其对应版本处理,而不是比较两个按钮的点击时刻。
六、排查进度异常的顺序
| 现象 | 优先排查项 | 建议处理 |
|---|---|---|
| 序号回退为旧值 | seq 是否倒序或重复 | 保留较新快照并记录事件版本 |
| 详情和紧凑入口显示不同 | 是否各自维护状态副本 | 统一快照源和派生文案 |
| 已入场后再次出现排队中 | 终止状态是否仍被更新 | 阻止终止阶段普通迁移 |
| 更新时间不断变化但数字不变 | 是否重复接收同一事件 | 以凭证与版本去重 |
| 切换活动后出现他人进度 | ticketId 是否匹配 | 严格校验事件归属 |
调试时应先记录 ticketId 的受控缩略值、事件版本、旧状态、新状态及拒绝原因。不要把完整凭证密钥或可直接核验的二维码内容写入日志。若仍无法复现问题,可在测试数据中人为调整事件顺序,检查归约器对 seq 变小和跨阶段跳转的响应。

七、把状态设计留给后续交互复用
完成这层设计后,活动入场系统已经拥有明确的数据中心:一个凭证对应一条可更新快照,状态变更经过版本和迁移规则判断,窗口只选择需要展示的字段。下一步可以把活动信息、凭证摘要和排队进度组合为列表卡片,再依据不同窗口形态输出不同密度的信息,避免多处手写业务判断。
更多推荐


所有评论(0)