HarmonyOS港口箱位连载07:交接待办归属与异常复核闭环【共创稿事节】
港口交接班时,最难解释的往往不是已确认的箱位,而是上一班组留下的两种“悬而未决”:箱体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语言概述。
更多推荐



所有评论(0)