HarmonyOS港口箱位连载08:交接审计归档与证据链索引【共创稿事节】
港口箱位核对经历跨页批量确认、网络中断和交接班后,班组负责人需要查看的不只是当前待办数,而是可以解释每个处理结果的归档视图。箱体043在south-02-04取得明确回执,修订版升至4;箱体051位于south-03-01,原操作回执仍为UNKNOWN,接班人员需要继续核实;箱体063位于north-01-08,核对时遇到修订冲突,保留版本5与差异依据。若把这三条都归类为“本班已处理”,实际审核状态便会在汇总时被掩盖。
前面的工程已经建立稳定箱位ID、核对操作键、回执历史和交接任务。本篇在这些模型之上建立审计归档,要求每个结论能够回指来源事件、人员处置与对应修订版。页面展示可以随窗口宽度变化,档案却不能随着筛选条件或页面焦点变化而改写。解决这类问题,必须让业务记录、审计快照和界面偏好各自承担独立职责。

区分箱位、回执、待办和归档四种身份
slotId定位堆场中的一个业务位置,commandKey对应一次已经提交的核对操作,eventId标识回执或状态事件,taskId标识交接后的人员工作。一个箱位可能关联多次核对命令,同一个核对命令在网络恢复后可以查询多次,但不能据此制造多个已确认结果。待办状态为ACCEPTED只说明有人接手,回执仍可以是UNKNOWN;待办状态为CLOSED也不自动意味着仓储核对通过。只有明确保留这些关系,查询结果才具有可解释性。
工程在model/ReviewHistoryEvent.ets、model/HandoffTask.ets基础上新增model/AuditEntry.ets、service/AuditProjector.ets、service/ArchiveReader.ets与pages/AuditArchivePage.ets。审计条目保存箱位身份、业务回执类别、事件引用、仓储修订版、交接任务状态和证据引用;不在归档对象中复制完整个人身份信息,也不把业务服务的状态更新交给页面组件执行。归档生成时记录统一事件水位,使后来到达的事件产生新修订,而不是悄悄改写旧文档。
export type ReceiptKind = 'APPLIED' | 'UNKNOWN' | 'CONFLICT';
export type HandoffState = 'NONE' | 'OFFERED' | 'ACCEPTED' | 'INVESTIGATING' | 'CLOSED';
export interface AuditEntry {
slotId: string;
containerNo: string;
receipt: ReceiptKind;
receiptEventId: string;
slotRevision: number;
handoffTaskId: string;
handoffState: HandoffState;
evidenceEventIds: string[];
latestSequence: number;
}
export interface AuditSnapshot {
createdAt: number;
sourceSequence: number;
rows: AuditEntry[];
}
用业务回执和证据确定真实分类
报表应分别统计“业务已确认”“原回执待查”“修订冲突”“证据缺失”。箱体043的回执是APPLIED,只有该事件存在于可追溯的证据引用里才归为已确认。051即使经交接进入调查阶段,原回执没有更新前仍保持待查。063如果已经转交给另一位复核人员,却没有得到适用于最新修订的操作结果,也仍处于冲突类别。把任务进度当成业务状态,会使待办和仓储数据相互污染。
判定函数使用传入的不可变数据,不通过网络请求补充字段,也不从颜色或列表位置推断状态。对于没有回执事件ID、或者事件ID不在证据集合中的条目,结果直接标为证据待补,不能默认为成功。重复的证据ID先去重,避免同一网络消息因多次接收而在统计中计数多次。这样首页分类和导出结果共用一套判断规则,避免列表显示待查而下载的汇总却写成完成。
export type AuditClass = 'CONFIRMED' | 'WAITING_RECEIPT' | 'REVISION_REVIEW' | 'MISSING_EVIDENCE';
export interface AuditResult {
slotId: string;
containerNo: string;
category: AuditClass;
canCloseBusiness: boolean;
evidenceCount: number;
}
export function classifyAudit(row: AuditEntry): AuditResult {
const refs = new Set<string>(row.evidenceEventIds.filter((id: string) => id.length > 0));
let category: AuditClass = 'MISSING_EVIDENCE';
if (row.receiptEventId.length > 0 && refs.has(row.receiptEventId)) {
if (row.receipt === 'APPLIED') category = 'CONFIRMED';
if (row.receipt === 'UNKNOWN') category = 'WAITING_RECEIPT';
if (row.receipt === 'CONFLICT') category = 'REVISION_REVIEW';
}
return { slotId: row.slotId, containerNo: row.containerNo,
category, canCloseBusiness: category === 'CONFIRMED', evidenceCount: refs.size };
}
冻结档案时先确定统一事件水位
审计服务通常分页返回事件。假设读取第一页后,051突然取得新回执,第二页又混入最新数据,那么两页直接拼接会得到一个既不属于交接时刻、也不属于当前时刻的集合。归档接口应返回一致的sourceSequence,客户端只接受不超过这个水位的条目,按事件序号与稳定箱位ID排序。若水位失效,重新查询完整数据;若部分页失败,保留原归档并列出缺失范围,不以已经读取的部分直接覆盖完整档案。
每个箱位在一个归档修订中只出现一条投影记录。来源历史中可能有多个事件,但将它们聚合为一条状态时,必须基于已处理事件顺序和版本规则,而不是随意取数组最后一个元素。冻结阶段把证据引用去重、排序,再复制对象,避免共享活动列表上的数组引用。保存时间与水位由两种字段分别承担:前者说明何时产生记录,后者说明记录包含哪些事件。旧快照要保留,新回执应该生成新的档案版本供比对。
export function freezeArchive(rows: AuditEntry[], watermark: number, now: number): AuditSnapshot {
const accepted = rows.filter((row: AuditEntry) => row.latestSequence <= watermark);
const seen = new Set<string>();
const frozen: AuditEntry[] = accepted.map((row: AuditEntry) => {
if (seen.has(row.slotId)) throw new Error('箱位重复:' + row.slotId);
seen.add(row.slotId);
const evidence = Array.from(new Set<string>(row.evidenceEventIds));
evidence.sort((a: string, b: string) => a.localeCompare(b));
return { ...row, evidenceEventIds: evidence };
});
frozen.sort((a: AuditEntry, b: AuditEntry) =>
a.latestSequence - b.latestSequence || a.slotId.localeCompare(b.slotId));
return { createdAt: now, sourceSequence: watermark, rows: frozen };
}
审核归档时允许查阅原事件详情,但不允许在历史详情页面直接重试核对命令。051仍处于回执待查时,应由原操作键去查询服务器状态;063需要按新修订重新检查差异,形成明确的新操作后才能变更业务分类。这些动作都可能产生新事件,因此要与归档查看行为分开。对交接任务的接收、指派和结案也采用相同原则:历史页只展示确实存在的任务修订,不凭本地按钮点击生成“已结案”的事实。

页面焦点和筛选偏好不参与证据判断
手机上可以按照“全部、已确认、待查、冲突、缺证据”切换列表;宽屏设备可在左侧保留列表、右侧显示证据详情。列表采用List与ForEach,以slotId作稳定键。用户选中051后改变筛选条件,051即使暂时不在当前列表,也不能把焦点误移到数组同一索引位置上的063;应提示当前筛选不包含选中箱位,并保留访问历史的入口。若后台归档版本发生改变,先对比版本标识,再决定是否更新列表,不让晚到的查询覆盖已经切换到的新档案。
Preferences用于记住当前筛选及最近查看的箱位,适合轻量界面偏好,但不承担跨终端的交接一致性,也不保存权威的仓储核对状态。写入流程需要等待put和flush结束,保存失败时仅影响下次打开的筛选恢复,不影响业务档案。应用重新进入时先载入界面偏好,随后请求归档列表,再核对选中箱位是否存在;空列表、读取超时和已经失效的焦点应分别显示不同提示。
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
export async function rememberAuditView(context: common.UIAbilityContext,
category: AuditClass, selectedId: string): Promise<void> {
const store = await preferences.getPreferences(context, 'port_audit_view');
await store.put('category', category);
await store.put('selectedSlotId', selectedId);
await store.flush();
}
export function visibleAuditRows(rows: AuditResult[], category: string): AuditResult[] {
return category === 'ALL' ? rows.slice() : rows.filter((row: AuditResult) => row.category === category);
}
从异常用例检查归档是否可解释
排查可以从043、051、063三条固定数据开始:043应进入已确认类别,051进入回执待查类别,063进入修订复核类别。将051的交接状态修改为已接收但不改变回执,观察其业务分类应保持待查;删除051的回执事件引用,它应转入证据待补,而不是跳到已确认。再打乱事件输入顺序,检查排序及统计不受原数组顺序影响;将相同箱位重复输入一次,检查冻结阶段能否给出定位明确的错误信息。
还需要从生命周期观察异步竞争:连续切换归档修订时,为每次请求分配查询代次,旧请求即使稍后返回,也不应覆盖当前视图。切到后台再返回,界面可以恢复筛选,但应重新比较档案修订。若某一页读取失败,应保留旧版并提供重试;若偏好写入失败,应显示轻量提示,不把错误扩散到归档服务。HiLog记录归档水位、查询代次、分类计数、错误类别与缺失事件数量即可,避免写出完整操作键及人员备注。

审计归档真正需要回答的是“这个结论由哪些可追溯事件得到”,而不是“屏幕上显示了哪个颜色”。将业务核对、回执查询、人员交接和历史归档拆成相互关联又独立的层级,043的完成记录不会被重算,051的待确认不会被交接动作掩盖,063的冲突也能留下明确的复核路径。至此,港口箱位系列从稳定ID和响应式导航,逐步完成单条定位、批量核对、回执对账、异常交接和可复核归档,后续扩展按班次统计或宽屏审阅,都可以继续沿用这条事件证据链。
参考资料:ArkUI列表开发、Preferences接口、ArkTS语言概述。
更多推荐



所有评论(0)