港口箱位核对经历跨页批量确认、网络中断和交接班后,班组负责人需要查看的不只是当前待办数,而是可以解释每个处理结果的归档视图。箱体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需要按新修订重新检查差异,形成明确的新操作后才能变更业务分类。这些动作都可能产生新事件,因此要与归档查看行为分开。对交接任务的接收、指派和结案也采用相同原则:历史页只展示确实存在的任务修订,不凭本地按钮点击生成“已结案”的事实。

DevEco Studio中的归档投影、分类代码和审阅页面

页面焦点和筛选偏好不参与证据判断

手机上可以按照“全部、已确认、待查、冲突、缺证据”切换列表;宽屏设备可在左侧保留列表、右侧显示证据详情。列表采用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语言概述。

Logo

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

更多推荐