HarmonyOS港口箱位连载05:批量回执与待确认清单【共创稿事节】
港口箱位的批量复核已经把“选中哪些箱位”和“哪些记录符合提交条件”分开。上一阶段的跨页清单包含三条不同结局的记录:south-02-04对应箱体043,修订版3进入确认流程;south-03-01对应箱体051,修订版1发起确认后连接中断;north-01-08对应箱体063,勾选时为修订版5,提交前因仓储数据变化而进入冲突队列。若界面只统计按钮发出的请求数量,很容易把“已请求三条”展示成“已完成三条”,让现场复核与仓储记录脱节。
更稳妥的做法是为每条审核命令建立可核对的回执身份。已经取得明确确认回执的043进入已确认列表;051由于结果不明继续留在待确认清单,恢复连接后查询同一操作键;063保留原有冲突原因,等待重新查看当前修订版。三条记录的生命周期互不覆盖,批次汇总只是一张读模型,不负责直接修改仓储事实。

命令、回执和展示状态分别存储
业务上至少需要三个对象:ReviewCommand表示一次核对意图,含稳定箱位ID、期望版本与幂等键;ReviewReceipt表示后台对该命令的最终或暂定答复;ReceiptRow表示当前页面能解释的行状态。不要拿箱体编号当回执主键,因为同一集装箱可能移动到其他箱位;也不要把网络超时解释成仓储写入失败。HTTP连接超时只说明客户端没有得到明确结果,后台是否处理必须另行核对。
沿用前篇的SlotRepository.confirmIfRevision()约定,本章新增model/BatchReceipt.ets和service/ReceiptReconciler.ets。后台应保存操作键与处理结果之间的映射,并保证同一键重复查询得到对应命令的结果。该查询属于仓储业务接口,由后端实现,客户端不假设HarmonyOS提供箱位核对专用接口。
export enum ReceiptState {
APPLIED = 'APPLIED',
UNKNOWN = 'UNKNOWN',
CONFLICT = 'CONFLICT'
}
export interface ReviewReceipt {
slotId: string;
containerNo: string;
commandKey: string;
expectedRevision: number;
state: ReceiptState;
confirmedRevision?: number;
checkedAt: number;
}
export interface ReceiptRepository {
queryByCommandKey(key: string): Promise<ReviewReceipt | undefined>;
}
本组记录中,043的回执可以记为APPLIED且新版本为4;051在确认答复抵达前记为UNKNOWN;063保持CONFLICT,不会因为其他两条的进展被改成已确认。checkedAt是客户端本次查询的时间标记,用于说明信息新鲜程度,不应被当作后台提交时间。若后台返回明确的提交时间,还可以增加单独的服务端时间字段。
对结果不明的项目按原键查询
网络恢复处理不能简单地遍历所有UNKNOWN记录再次发送确认命令。原请求可能已经完成;如果重试时生成新的操作键,即使服务端提供去重机制也无法认定两次是同一个意图。优先查询原键对应的处理结果;仓储服务明确要求重试时,才带原来的commandKey和expectedRevision继续调用,并以最终回执覆盖本地待确认状态。
处理顺序采用串行或受控并发都可以,关键是每项独立产出。下面的合并函数不改变原始箱位身份,只对仍处于UNKNOWN的回执行状态核对;查询异常时保留旧值,给下次恢复留下继续点。它与前一章“批量首次提交”不同,只负责已经存在的命令后续对账。
import { ReceiptRepository, ReceiptState, ReviewReceipt } from '../model/BatchReceipt';
export async function reconcileReceipts(
rows: ReviewReceipt[], repository: ReceiptRepository
): Promise<ReviewReceipt[]> {
const updated: ReviewReceipt[] = [];
for (const row of rows) {
if (row.state !== ReceiptState.UNKNOWN) {
updated.push({ ...row });
continue;
}
try {
const latest = await repository.queryByCommandKey(row.commandKey);
updated.push(latest && latest.slotId === row.slotId
? { ...latest, checkedAt: Date.now() }
: { ...row });
} catch (error) {
updated.push({ ...row });
}
}
return updated;
}
服务端返回的slotId必须与命令所属箱位一致。若键对应了意料之外的其他箱位,本地不能拿那条结果覆盖当前行,应记录数据关联异常并提示检查仓储服务。已确认回执也不应被后到的旧查询答复降级,因此可以为客户端增加单调的回执更新序号,或者由后端保证查询结果不会回退。页面刷新不等于生成一个新的确认意图。
把待确认清单存到本地以支持页面恢复
核对员可能在仓库楼层间切换网络,也可能先离开该页面去查看箱位明细。如果未完成命令只保存在组件的普通数组中,页面重建后就丢失了查询入口。这里适合保存体积较小的待对账记录,包括箱位ID、操作键、期望版本、回执状态和查询时间。Preferences提供键值存储及put()、flush()等接口;图片、完整批次报表和大规模仓储数据仍应存放在更适合的文件或业务存储中。
用@kit.ArkData取得Preferences对象后,加载与写入集中在同一个仓储服务里。一个页面生命周期内只保留一个写入协调者,避免多个写入者覆盖整份JSON。只有落盘动作完成后,页面才把这份清单标记为可恢复状态;出错则保留内存中的待处理数据,同时向用户呈现保存失败的原因。
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { ReviewReceipt } from '../model/BatchReceipt';
export class ReceiptJournal {
private readonly key: string = 'pending_review_receipts';
async load(context: common.UIAbilityContext): Promise<ReviewReceipt[]> {
const store = await preferences.getPreferences(context, 'port_receipt_store');
const value = await store.get(this.key, '');
if (typeof value !== 'string' || value.length === 0) {
return [];
}
const rows = JSON.parse(value) as ReviewReceipt[];
if (!Array.isArray(rows)) {
throw new Error('回执清单格式错误');
}
return rows;
}
async save(context: common.UIAbilityContext, rows: ReviewReceipt[]): Promise<void> {
const store = await preferences.getPreferences(context, 'port_receipt_store');
await store.put(this.key, JSON.stringify(rows));
await store.flush();
}
}
JSON.parse()可能因旧版本格式或不完整内容抛出异常,调用处应转入清单恢复提示,保留原键对应的存储内容供排查,不能捕获异常后直接用空数组覆盖它。对于并发岗位的业务记录,Preferences仅保存本设备的待对账线索,不承担跨终端一致性职责;最终状态仍应由仓储服务回执决定。若清单数量增长,可分批清理已归档项,而不是在首屏读取上万条历史记录。

汇总数字必须有可解释的统计口径
页面顶部给出“已确认1、待确认1、冲突1”,下方显示每个箱位的原始命令身份与最新回执。用户点击“核对待确认回执”才触发查询,切换筛选项、屏幕宽度或返回页面只改变视图,不自动重复创建命令。对043,展示“已确认,修订版4”;对051,展示“回执待确认”;对063,展示“修订冲突,保留复核”。如果051得到明确已确认答复,数字应随状态变化成为“已确认2、待确认0、冲突1”。
import { ReviewReceipt, ReceiptState } from '../model/BatchReceipt';
@Entry
@Component
struct ReceiptPage {
@State private rows: ReviewReceipt[] = [];
@State private checking: boolean = false;
private countOf(state: ReceiptState): number {
return this.rows.filter((row: ReviewReceipt) => row.state === state).length;
}
build() {
Column({ space: 14 }) {
Text('港口箱位 · 批量回执').fontSize(24).fontWeight(FontWeight.Bold)
Text(`已确认 ${this.countOf(ReceiptState.APPLIED)} · 待确认 ${this.countOf(ReceiptState.UNKNOWN)} · 冲突 ${this.countOf(ReceiptState.CONFLICT)}`)
List() {
ForEach(this.rows, (row: ReviewReceipt) => {
ListItem() {
Column({ space: 6 }) {
Text(`箱体 ${row.containerNo} ${row.slotId}`).fontSize(18)
Text(row.state).fontColor('#55718D')
}.padding(12).width('100%')
}
}, (row: ReviewReceipt) => row.commandKey)
}.layoutWeight(1)
Button(this.checking ? '正在核对回执' : '核对待确认回执')
.enabled(!this.checking)
}.padding(16).width('100%').height('100%')
}
}
这里把回执条目作为唯一键,避免箱体号重复导致列表组件复用错误。业务查询函数由页面协调器注入,checking在请求开始与结束时分别设置,完成后一次性替换rows数组。异步结果还应带一个请求序号,防止用户快速切换批次时上一次的晚到答复覆盖新批次。若某行被业务人员主动重新审阅,应生成新的核对意图并保留旧回执历史,而不是在旧命令上更换期望版本。

调试从操作键和状态跃迁检查
问题定位可以按四条路径展开。第一条是正常确认:043的命令键查询得到明确APPLIED,本地记录的修订版从3映射到4。第二条是请求超时:051维持UNKNOWN,重复执行恢复操作时沿用同一个键,不能增加第二条确认命令。第三条是版本冲突:063持续停在冲突列表,只有用户阅读修订差异后才能产生新的核对意图。第四条是页面重建:关闭回执页再进入,从Preferences恢复待确认线索并重新核对仓储状态。
HiLog可记录slotId、操作键的短摘要、查询耗时、beforeState与afterState,不必打印完整操作令牌和内部人员信息。对较慢的网络连接,列表可以逐项显示最新查询时间,并允许用户取消后续查询;取消不会改变已经在后台完成的结果,待确认记录仍需在下一次恢复时核对。如果多次查询持续没有回执,应给出人工处理入口而不是永久旋转的进度提示。
这套设计把批量操作拆成“首次提交”和“后续对账”两个阶段。稳定箱位ID保证记录不会串行,操作键保证同一意图能够被追踪,明确回执才能推动最终状态。随着批量审阅增多,前面建立的修订版比较、跨页多选及回执查询就形成一条可解释的港口箱位核对链路,下一步可在其上整理审阅历史和异常原因分布。
参考资料:ArkUI List列表容器、https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-watch@Watch状态变量更改通知、Preferences键值持久化接口、ArkTS语言基础。
更多推荐


所有评论(0)