港口交接班时,最难解释的往往不是已确认的箱位,而是上一班组留下的两种“悬而未决”:箱体051的批量核对指令已经发出,回执却停留在待确认;箱体063的提交条件与仓储记录修订版发生冲突,需要复核最新箱位内容。它们都不能靠一次列表刷新自动消失。若值班人员只收到“共有两条异常”的通知,却不知道谁来接收、依据哪条事件处理以及何时可以结案,前面建立的稳定箱位身份与回执时间线便无法真正服务交接。

这一阶段继续沿用PortSlotReview的箱位模型:south-02-04对应箱体043,已取得修订版4的确认回执,不再进入异常待办;south-03-01对应051,保留原命令键等待查证;north-01-08对应063,记录修订冲突时的期望版与最新版本。新的交接页面负责把异常事件转成有归属的待办,并在结案时保留证据引用,而不是重新发起此前的批量核对。

港口异常交接与待办归属

将业务回执和人员待办分层

核对回执表示仓储命令的处理结果,交接待办表示一位值班人员准备采取什么后续行动。两者生命周期不同:051即使已由新班组接收,原回执仍然是UNKNOWN;063即使被分派给复核人员,冲突也不会因此变成“已确认”。如果把taskState直接写回箱位status,界面很容易把“已接收处理”误显示成“箱位已通过核对”。

工程新增model/HandoffTask.ets、service/HandoffService.ets、pages/HandoffBoardPage.ets和view/HandoffRow.ets。待办通过taskId区分同一箱位的不同处置事项,通过slotId找到稳定箱位,通过originEventId回溯第六篇的审阅事件。commandKey仍交给原来的回执查询服务使用,普通待办卡片不显示其完整值。交接修订号由待办服务维护,与仓储箱位的修订号分别记录。

export type HandoffState = 'OFFERED' | 'ACCEPTED' |
  'INVESTIGATING' | 'CLOSED';
export type HandoffReason = 'UNKNOWN_ACK' | 'REVISION_CONFLICT';

export interface HandoffTask {
  taskId: string;
  slotId: string;
  containerNo: string;
  originEventId: string;
  reason: HandoffReason;
  state: HandoffState;
  ownerId: string;
  taskRevision: number;
  slotRevision: number;
  resolutionEventId: string;
  updatedAt: number;
}

export interface HandoffCommand {
  taskId: string;
  expectedTaskRevision: number;
  actorId: string;
  action: 'ACCEPT' | 'BEGIN' | 'CLOSE';
  resolutionEventId?: string;
}

这里的actorId是应用业务身份,并非操作系统自动赋予的交接凭据。人员选择动作前,服务侧还需基于已登录的业务会话核验权限;客户端传入的人员ID仅辅助表达意图,不能代替后台对身份和角色的判断。051与063各有不同的处置原因,后续证明材料也应采用不同的业务类型:前者关联同一操作键的新回执,后者关联重新读取箱位后的修订差异记录。

接收与结案应有明确的转换条件

交接状态从OFFERED进入ACCEPTED,再进入INVESTIGATING,具备确定结论后才能到CLOSED。处理中如果需要更换接收人,应走独立的转派流程并追加事件,不能把旧接收人直接覆盖。对于仍为未知回执的051,单凭“重新查询了一次”不足以结案;对于063,单凭重新进入详情页也不足以说明冲突得到处理。

把转换判断写成纯业务函数,能够让页面和数据服务共享规则。版本比较必须先于状态变更,结案还要收到符合当前待办原因的证据事件。下面返回新对象,不在函数里修改输入记录;后台持久化时需要用相同的期望修订版执行条件更新,防止两位人员同时接收时发生覆盖。

export type HandoffOutcome =
  { kind: 'APPLIED'; task: HandoffTask } |
  { kind: 'CONFLICT' | 'INVALID' };

export function transitionTask(
  task: HandoffTask, cmd: HandoffCommand,
  evidenceKind: string, now: number
): HandoffOutcome {
  if (task.taskId !== cmd.taskId ||
      task.taskRevision !== cmd.expectedTaskRevision) {
    return { kind: 'CONFLICT' };
  }
  if (!cmd.actorId.trim()) { return { kind: 'INVALID' }; }
  let next: HandoffState = task.state;
  if (cmd.action === 'ACCEPT' && task.state === 'OFFERED') {
    next = 'ACCEPTED';
  } else if (cmd.action === 'BEGIN' && task.state === 'ACCEPTED' &&
             task.ownerId === cmd.actorId) {
    next = 'INVESTIGATING';
  } else if (cmd.action === 'CLOSE' && task.state === 'INVESTIGATING' &&
             task.ownerId === cmd.actorId && cmd.resolutionEventId) {
    const allowed = task.reason === 'UNKNOWN_ACK' ?
      evidenceKind === 'RECEIPT_RESOLVED' :
      evidenceKind === 'REVISION_RECHECKED';
    if (allowed) { next = 'CLOSED'; }
  }
  if (next === task.state) { return { kind: 'INVALID' }; }
  return { kind: 'APPLIED', task: {
    ...task, state: next,
    ownerId: cmd.action === 'ACCEPT' ? cmd.actorId : task.ownerId,
    resolutionEventId: cmd.resolutionEventId ?? task.resolutionEventId,
    taskRevision: task.taskRevision + 1, updatedAt: now
  } };
}

这里的evidenceKind来源于业务服务查询到的可信事件记录,而不是让页面自己传字符串就认定结案。对于051,应先用原操作键检查仓储服务是否已确认该命令,再生成RECEIPT_RESOLVED业务事件;对于063,要比较当前箱位版本及冲突字段,形成REVISION_RECHECKED记录,并在必要时由人员重新发起核对。这些动作发生在原来的审阅域,交接服务只引用最终可追溯的事件ID。

交接快照和后端条件写入

待办在页面中可以呈现为列表,但多个设备同时处理交接时,@State数组只是当前视图,不能承担唯一归属判断。接收命令应提交taskId、期望待办修订版和处理意图;仓储业务后端在同一事务内检查当前状态与版号,匹配才写入接收人和新版本。发生冲突时返回最新的待办摘要,页面保留用户选择,再提供重新核对入口。对于网络超时,采用唯一操作键查询原指令处理结果,避免点击重试就产生新的接收历史。

客户端服务接口保留这些语义,不绑定未经定义的系统能力。HandoffGateway是应用自身的业务适配层,网络传输可由工程已有的接口客户端承接;若当前环境仅有本地仓储数据,仍应保持同样的返回类型,避免以后接入服务时把页面逻辑重写一遍。

export interface HandoffGateway {
  listOpen(): Promise<HandoffTask[]>;
  accept(taskId: string, expectedRevision: number,
    operationKey: string): Promise<HandoffOutcome>;
  queryOperation(operationKey: string): Promise<HandoffOutcome>;
}

export function createOperationKey(taskId: string, seq: number): string {
  return `${taskId}-accept-${seq}`;
}

export async function acceptWithRecovery(
  gateway: HandoffGateway, task: HandoffTask, key: string
): Promise<HandoffOutcome> {
  try {
    return await gateway.accept(task.taskId, task.taskRevision, key);
  } catch (error) {
    return gateway.queryOperation(key);
  }
}

操作键在首次发起接收前就应保存,查询时使用同一值。若查询本身也受阻,应保留“结果待查”,不把异常分支自动变成成功。纯粹使用taskId和页面点击次数拼出的键,需要保证点击序号在业务会话内持久而不重复;生产环境可使用服务端分配或项目内稳定生成的随机唯一值,关键不是字符格式,而是重试不得换键。出于安全考虑,日志记录键的摘要即可。

交接状态转换与待办列表的开发环境

页面重入和筛选不会转移任务归属

列表按状态分组:待接收、处理中、已结案。筛选仅改变显示集合,不改写ownerId。例如新班组接收051后,051从待接收页转到处理中页;063仍留在待接收,不会由于数组下标变动而被错误选中。043作为已确认历史可以在回执历史页查看,但不在异常交接清单里占一个待办名额。页面中的选择使用稳定taskId,每次刷新后重新判断该ID是否仍属于当前筛选。

@Entry
@Component
struct HandoffBoardPage {
  @State private rows: HandoffTask[] = [];
  @State private tab: HandoffState = 'OFFERED';
  @State private selectedTaskId: string = '';
  @State private message: string = '';
  private generation: number = 0;
  private gateway: HandoffGateway = createHandoffGateway();

  aboutToAppear(): void { this.refresh(); }
  aboutToDisappear(): void { this.generation++; }

  private async refresh(): Promise<void> {
    const token = ++this.generation;
    try {
      const fresh = await this.gateway.listOpen();
      if (token !== this.generation) { return; }
      this.rows = fresh;
      this.message = '';
    } catch (err) {
      if (token === this.generation) {
        this.message = '待办读取受阻,请刷新后继续';
      }
    }
  }

  build() {
    Column({ space: 12 }) {
      Text('港口待办交接').fontSize(24)
      Row({ space: 8 }) {
        Button('待接收').onClick(() => this.tab = 'OFFERED')
        Button('处理中').onClick(() => this.tab = 'INVESTIGATING')
        Button('已结案').onClick(() => this.tab = 'CLOSED')
      }
      if (this.message) { Text(this.message).fontColor(Color.Orange) }
      List() {
        ForEach(this.rows.filter((t: HandoffTask) => t.state === this.tab),
          (task: HandoffTask) => {
            ListItem() {
              Column({ space: 5 }) {
                Text(`箱体${task.containerNo} · ${task.slotId}`)
                Text(`${task.reason} · 交接版${task.taskRevision}`)
              }.padding(14).width('100%')
            }.onClick(() => { this.selectedTaskId = task.taskId; })
          }, (task: HandoffTask) => task.taskId)
      }.layoutWeight(1)
      Button('重新获取').onClick(() => this.refresh())
    }.padding(16).height('100%')
  }
}

createHandoffGateway()由工程中的service/HandoffService.ets实现,提供具体数据源,不属于HarmonyOS系统接口。页面加载用代次标识阻止旧请求覆盖新结果;销毁时递增代次,使已经离开页面的网络响应失效。List与ForEach按照官方ArkUI规则组织,键使用taskId,适合状态迁移后仍要保持组件关联的场景。实际交接按钮应在提交前弹出任务摘要,让人员看到原因与版号,再请求服务执行接收。

视图恢复和交接闭环的检查路径

交接界面只需要持久化最近使用的状态筛选及待办ID。可沿用此前Preferences的小型键值存储做界面恢复,但任务归属和结案证据仍以业务服务结果为准。页面重新进入时先读取业务待办,再应用视图线索;如果上次选中的待办已经由其他人员关闭,显示“记录已更新”,而不要用旧缓存覆盖新的归属。Preferences文档强调其适合单进程轻量配置,不能把它当作多端原子交接数据库。

调试时用051与063构造两条OFFERED待办。先由不同人员同时尝试接收051,检查期望修订版能否将其中一次引向冲突分支;再让接收请求返回超时,用原操作键查询以核实是否已经写入;接着让051仍为UNKNOWN却请求结案,检查证据类型判断是否拒绝;让063通过重新读取取得版本差异记录,再检查结案操作能否关联该事件。切换状态选项、快速离开页面与重新进入时,观察同一taskId的焦点是否被保存。HiLog记录待办数量、状态分类、期望版与查询代次即可,不展开处理人的隐私信息。

港口异常待办接收与处理状态的手机界面

异常交接形成闭环的关键不是页面上多一个“已办”标签,而是把稳定箱位、回执原因、人员归属、任务修订和结案证据组织为可追踪的链路。此时051的待确认回执仍由原命令键负责查询,063的修订冲突仍由箱位核对域处理,交接页面只承接归属与进度。接下来的统计与归档工作可以据此生成交接结果,而不必从颜色或当前列表位置反推历史。

参考资料:ArkUI列表创建、Preferences接口、ArkTS语言概述。

Logo

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

更多推荐