ArkTS + HarmonyOS 巡检交付:待办健康检查与恢复闭环(连载06/06)【共创稿事节】
巡检工具最难交付的部分,不是把三个任务勾成绿色,而是面对一台刚从无信号配电间拿回来的设备,能够回答三个问题:这条记录存在本机吗?接收端是否真的确认了?如果两者不一致,应该由谁做出下一步决定?ShiftCheck 从页面状态、草稿保存、版本冲突走到离线待办和人工裁决,这一篇不再添加新的同步噱头,而是给整个工程建立一套可以重复执行的启动检查、人工恢复和交付验收。只要存在任何没有证据的状态,就不把它包装成“已经同步”。

整个系列沿用三条 CheckTask { id, title, location, done }:“机柜温控”“交换机端口”“备用电源”;本地 Preferences 文件名 shiftcheck_draft_v1,其中 tasks_v1 保存草稿,pending_confirm_v1 保存待确认命令。待办仍是 PendingConfirm { requestId, taskId, wantedDone, createdAtMs },第04篇的 DemoCommandGateway 和第05篇的冲突裁决只针对单进程内存接收端说明。上一章虽然允许“保留我的意图”并替换旧命令,但一个长期运转的现场工具还必须考虑被中断的写入、损坏的队列、过期版本与人为误操作。这些都是本篇处理的对象。
一、交付前先画清楚一条不可跨越的边界
原有页面有两个容易混淆的数字:本机勾选数量,以及接收端确认数量。两者可以恰好相等,但相等不构成同一事实。已经 flush() 的草稿只能证明该设备准备了这份数据;已经写入待办的请求只能证明本机保留了意图;只有获得接收端明确成功的回执,才能进入“已确认”。因此把本机状态显示为本地草稿、待确认、需核查、已确认四种可解释类别,拒绝在尚未取回权威数据时使用统一的“完成”角标。
每次启动页面要先恢复持久数据,再判断能开放哪些按钮。检查次序安排为“打开 Preferences → 读取原始草稿与队列 → 校验结构 → 对照任务身份 → 呈现安全模式 → 由用户选择同步或修复”。不能边异步恢复边让值班员点确认,否则可能先生成一条新待办,再被迟到的 get() 结果覆盖。为了说明差异,我们不依赖外部接口,而是把检查逻辑抽成一个无 UI、无文件写入的纯函数,确保它对相同输入始终返回相同的健康报告。
// entry/src/main/ets/common/QueueHealth.ets
import { PendingConfirm } from './PendingConfirm';
import { CheckTask } from './RevisionGateway';
export interface HealthReport {
canReplay: boolean;
needsReview: boolean;
message: string;
pendingCount: number;
}
export function inspectQueue(tasks: CheckTask[],
pending: PendingConfirm[]): HealthReport {
const taskIds: Set<string> = new Set(tasks.map((t: CheckTask) => t.id));
const requestIds: Set<string> = new Set<string>();
if (pending.length > 1) {
return { canReplay: false, needsReview: true,
message: '示例版只允许一笔待确认操作', pendingCount: pending.length };
}
for (const item of pending) {
if (!taskIds.has(item.taskId) || item.requestId.length === 0 ||
requestIds.has(item.requestId) || !Number.isFinite(item.createdAtMs)) {
return { canReplay: false, needsReview: true,
message: '待办身份与任务清单无法对应', pendingCount: pending.length };
}
requestIds.add(item.requestId);
}
return { canReplay: pending.length === 1,
needsReview: false, message: pending.length ? '本机有待确认操作' : '本机无待办',
pendingCount: pending.length };
}
inspectQueue() 并没有检查网络在线与否。即便返回 canReplay=true,也只意味着结构上允许进入补交流程,不意味着马上调用 send():还须检查是否处于冲突裁决、是否正在写盘、是否存在上一笔结果未知的情况。尤其在应用重启后,第05篇保存在内存里的 lastConflict 不复存在,必须重新取得接收端权威状态才能展示裁决按钮,不能把旧本机版本直接当成最新版本。

二、不要用一个空数组“修复”损坏数据
本机恢复路径最需要防范的错误,是把 JSON.parse() 失败当作“没有待办”。例如磁盘中的 pending_confirm_v1 因旧版本兼容问题或写入异常无法读取时,catch 分支若返回 [],页面就会出现“无待确认操作”,下一次保存更可能覆盖原始材料。对于普通待办清单,这只是体验问题;对于现场确认记录,它可能造成不可逆的信息损失。
本篇把读取结果拆成三类:结构有效、首次安装尚无记录、数据异常需要人工检查。首次没有该键时,允许展示空队列;键存在但格式损坏时则冻结补交和裁决按钮。下列实现延续上一期已有的 readPending() 校验函数,负责读取而绝不在异常分支中写回。pendingWrite 串行写入链仍保留,恢复时需等待它结束或由页面初始化确保没有未完成写入。
// entry/src/main/ets/common/SafeQueueLoader.ets
import { preferences } from '@kit.ArkData';
import { PendingConfirm, readPending } from './PendingConfirm';
export interface QueueLoad {
ok: boolean;
items: PendingConfirm[];
tip: string;
}
export async function loadQueue(store: preferences.Preferences): Promise<QueueLoad> {
try {
const raw = await store.get('pending_confirm_v1', '');
if (typeof raw !== 'string') {
return { ok: false, items: [], tip: '待办类型异常,请人工核查' };
}
if (raw === '') {
return { ok: true, items: [], tip: '首次使用或尚无待办' };
}
return { ok: true, items: readPending(raw), tip: '待办已读取' };
} catch (_) {
return { ok: false, items: [], tip: '待办读取失败,已暂停补交' };
}
}
此函数返回 items: [] 并不代表在 ok=false 时可以继续执行空队列逻辑。调用方需要先判断 ok,失败时保留原 Preferences 键、禁止清空或重写,并让专业人员在可控环境内检查备份。对于跨应用卸载的存储丢失,本机同一 Preferences 文件也无法承担云端容灾;真正可追责的生产系统必须由后端保存请求回执、操作员和审计记录。官方 Preferences API 给出了 get()、put()、flush() 的语义,也明确其多进程安全限制,它不是可替代服务端事务表的分布式消息中间件。
在早期版本中,保存队列和保存草稿使用两个键,即使都由 Preferences 管理,也不意味着存在跨键业务事务。比如队列已经 flush 成功,而 queueDraft() 尚未落盘便退出,恢复时应先读取队列,并依据其中的 taskId 与 wantedDone 把本地展示校正到待确认意图;这不是改变接收端权威结果,也不能自动清除待办。业务动作如无法恢复应进入“需核查”,保留可追踪原始键值,绝不能静默覆盖。
三、现场的“恢复”应该是一张能操作的页面
交付页面不是堆一排红色错误码,而是告诉值班员下一步有什么选择。ShiftCheck 增加“交付核对”入口,显示任务总量三项、待确认数量、最近一次本机检查提示,并提供“重新检查”“尝试补交”“查看冲突”三类明确动作。当队列异常时,禁用“尝试补交”;当上一条请求结果不明时,提示“保留原请求号查询或重试”;当接收端已拒绝时,转到上一章的人工裁决卡片。页面无需宣称使用了真实联网服务,示例环境仍用受控的链路开关判断是否调用内存接收端。
// entry/src/main/ets/pages/DeliveryPage.ets(局部)
@State private pendingCount: number = 1;
@State private healthTip: string = '本机有待确认操作';
@State private canReplay: boolean = true;
@State private replayBusy: boolean = false;
@Builder DeliveryCard() {
Column({ space: 14 }) {
Text('交付核对').fontSize(26).fontWeight(FontWeight.Bold)
Text(`巡检任务 3 项 · 待确认 ${this.pendingCount} 条`)
Text(this.healthTip).fontColor('#BD6B1D')
Button(this.replayBusy ? '核对中' : '尝试补交')
.enabled(this.canReplay && !this.replayBusy)
.onClick(() => { this.replayPending(); })
}
.width('100%').padding(20).borderRadius(20)
.backgroundColor('#FFFFFF')
}
这段 DeliveryCard() 是已有页面新增的展示组件,replayPending() 继续沿用第04篇的实现,canReplay 应由 inspectQueue() 与页面运行态联合计算,而不是写死成示例初始化值。对于已经收到成功回执的队首,仍要等待本地 savePending(rest) 持久化成功再清除角标;若清理失败,保留同一 requestId 再向有幂等回执能力的接收端确认。仅在示例 Map 中记录回执的实现跨进程不会保留结果,因此生产验收必须检查真实服务端的原子记账能力。

需要强调一个设计细节:当用户选择“保留我的意图”后,系统会生成新的 requestId,这是第05篇明确拒绝后的新的确认操作;与之不同,超时且结果未知时必须携带原请求号继续查询或重试。两个场景不管按钮外观多么相近,执行函数都应该不同,否则看似漂亮的“恢复中心”会破坏业务上的幂等约束。
四、回归检查用故障矩阵,别凭一句“测试通过”结案
把验证分成可复现的动作,而不是写“所有状态正常”。准备好三项固定任务与一台可清理测试数据的设备,再分别构造首次安装、离线入队、响应超时、明确版本冲突、flush() 失败、旧 JSON 格式损坏及接收端已确认但本地未移除队首的场景。每个场景都写明预期消息与禁止动作,测试人员才知道该对照什么,而不是看见蓝色按钮就默认成功。
| 场景 | 本机应显示 | 不允许发生 |
|---|---|---|
| 首次安装无键 | 本机无待办 | 报业务确认成功 |
| 离线确认已落盘 | 待确认 1 条 | 自动清理请求号 |
| 请求响应未知 | 保留原请求号 | 生成新请求号盲目重发 |
| 版本冲突明确拒绝 | 进入人工裁决 | 用本机值覆盖权威值 |
flush() 异常 | 暂停修改并核查 | 宣称已成功落盘 |
| 待办 JSON 损坏 | 数据需检查 | 用空数组覆盖原键 |

这张矩阵不含“实测恢复耗时”之类的未经验证指标。验收时把每一条场景写成测试输入、实际观察、保存的证据与结论;数据量上限、时区、系统版本和设备型号可以在独立测试记录中补充,但不应把随机数字画入技术文章。程序内要记录技术上有帮助的异常类别,避免在 HiLog 输出设备账号、位置、请求正文和个人敏感内容。DevEco 对应用进行构建、运行、抓日志的工作需要开发者在相应 SDK 工程中实际完成,这里给出的是可复现的核查路径,不是编译成功声明。
五、六篇工程主线到这里如何真正收口
回看整个 ShiftCheck:第一篇将 CheckTask 的显示变更与任务身份区分;第二篇用 Preferences 保住本地草稿;第三篇用版本号暴露并发修改;第四篇用持久队列与稳定 requestId 支撑离线补交;第五篇把明确冲突交还给人工裁决;第六篇把这些能力变成可核查的交付状态。它们串成的不是一条“永远成功”的请求链,而是一条在失败情况下不轻易丢失意图、不错误冒认确认结果的恢复链。
实际项目上线前还必须补上真服务器幂等事务、稳定身份授权、跨进程及跨设备安全策略、数据保留期限、审计追踪与真实设备回归。示例代码适合验证小规模页面流程,不应被直接用于高风险生产巡检。真正的工程完成标准是:遇到失败能说清数据在哪里、哪个动作可以重试、谁有权决定覆盖,以及如何证明那次决定最终被接收端确认。
更多推荐

所有评论(0)