DemoProofBatch 页面AssetAuditPage 任务PB-2504一、五张素材并行检查不能让最慢的一张决定整份报告上架前整理素材时经常要同时做文件可读性、尺寸策略和自有命名规则的检查。单张图片出错可以给出理由最麻烦的反而是批量五张里面四张很快返回最后一张一直没有消息页面是等它还是把其他结果先交出来如果用一个总超时笼统打断既不能解释哪张失败也容易把已通过的记录一起抹掉。本轮换一个角度不去接某个具体系统图像分析服务而是研究 Promise 组合与批次报告的边界。工程名 ProofBatch演示页 AssetAuditPage任务 PB-2504。输入顺序固定为 IMG-01 至 IMG-05单项等待预算800毫秒。演示输出三条PASS、一条FAIL、一条TIMEOUT报告状态DONE_WITH_ERRORS。这些数值由本地可控的模拟检查器产生不代表任何商店审核平台的判定也不是设备文件系统扫描的实测结果。二、先把结果设计成表而不是把错误直接丢到页面我最先否定的方案是让某个Promise.reject直接穿透整条批次链。批量上传或检查的业务往往需要一份完整报告即便某项失败也必须保留另外四项的结果。更重要的是输入列表的顺序是审核人员理解报告的参照系不能因为IMG-04比IMG-02先返回就把它提前摆到第二行。数据模型因此只保留三个明确结果PASS表示演示检查返回真FAIL表示演示检查返回假或抛错TIMEOUT表示超过当前预算仍未得到结果。超时不等于文件已损坏更不等于商店拒审这只是应用层可解释的等待终止状态。每个结果必须携带原始ID与同一任务ID方便下一次复核只重试特定对象。下面是第一段关键代码给检查输入和结果定型。演示检查器故意让 IMG-05 在1200毫秒才返回以构造“800毫秒先超时迟到结果不能翻案”的场景IMG-03则明确返回假验证失败和超时不会混为一类。type AuditStatus PASS | FAIL | TIMEOUT; interface AuditItem { id: string; delayMs: number; passes: boolean; } interface AuditResult { taskId: string; id: string; status: AuditStatus; } const AUDIT_TASK: string PB-2504; const PER_ITEM_TIMEOUT_MS: number 800; const INPUTS: AuditItem[] [ { id: IMG-01, delayMs: 120, passes: true }, { id: IMG-02, delayMs: 260, passes: true }, { id: IMG-03, delayMs: 180, passes: false }, { id: IMG-04, delayMs: 340, passes: true }, { id: IMG-05, delayMs: 1200, passes: true } ]; function checkFixture(item: AuditItem): Promiseboolean { return new Promiseboolean((resolve: (ok: boolean) void): void { setTimeout((): void resolve(item.passes), item.delayMs); }); }这里没有读取图片像素、没有调用商店审核API也没有伪装服务端判定。checkFixture是确定性演示输入只有将来替换为实际文件检查器以后才需要补充文件句柄、编码器、图片解码对象等资源的释放流程。当前函数发出的短时setTimeout会自然到点结束不能把它误读成可取消的真实I/O任务。三、单项超时要有终态门闩而不是给Promise贴一个警告并行检查最容易漏掉的一件事是超时只说明调用方不再等待底层操作可能仍继续执行。比如 IMG-05 的模拟检查器在1200毫秒返回真但这张素材在800毫秒已经记为 TIMEOUT。如果后来的真值能把它改成PASS那么审核人员刚看到的失败报告就会悄悄变样下一轮补检也难以判断到底用了哪个版本。我把这个问题交给一个settled门闩处理。单项包装器在任意路径第一次完成后才生成结果第二次无论来自迟到成功、异常回调还是计时器都只记录忽略事实在成功或失败先发生时立即清理超时句柄避免无效定时器占着闭包。注意这个“完成一次”只约束结果对象不会反向停止真实网络传输生产环境要按实际服务另接取消接口。function guardOne(item: AuditItem, check: (input: AuditItem) Promiseboolean, timeoutMs: number): PromiseAuditResult { return new PromiseAuditResult((resolve: (r: AuditResult) void): void { let settled: boolean false; let timerId: number 0; const finish (status: AuditStatus): void { if (settled) { console.info([DEMO] ${AUDIT_TASK} ignored-late id${item.id}); return; } settled true; clearTimeout(timerId); resolve({ taskId: AUDIT_TASK, id: item.id, status: status }); }; timerId setTimeout((): void finish(TIMEOUT), timeoutMs); try { check(item).then((ok: boolean): void { finish(ok ? PASS : FAIL); }).catch((): void finish(FAIL)); } catch (_) { finish(FAIL); } }); }这个封装在示例层面保证每个输入都会变成一个可分类的结果不把单项拒绝抛给页面。clearTimeout的责任是清掉本包装器安装的倒计时不是释放检查器本身持有的外部资源。特别要注意若真实检查器内部存在永不返回且不可取消的操作仅靠 Promise 包装无法回收那份底层消耗需要业务超时协议和真实I/O的终止能力配合。四、并行要保持顺序还需要把异步报告当成不可变快照单项都能归档后批次层反而很简单按输入顺序创建五个 Promise再交给Promise.all。这里能够保持结果顺序的前提是每个包装器都把失败转成正常的AuditResult而不是继续向外抛出拒绝。完成快的项可以先内部归档但最终展示数组仍对应 IMG-01、02、03、04、05。真正上线时我不会让“本轮结果”直接覆盖“当前页面结果”。页面可能已经离开或者用户在上一个批次尚未结束时重新发起检查。下面示例把 Promise 合并本身写成一个独立函数页面调用者再使用epoch与alive双条件检查只有最新批次且页面仍可见才允许写入State。这不是系统自带事务是应用端显式建立的提交门禁。async function auditInInputOrder(items: AuditItem[]): PromiseAuditResult[] { const jobs: PromiseAuditResult[] items.map( (item: AuditItem): PromiseAuditResult guardOne(item, checkFixture, PER_ITEM_TIMEOUT_MS) ); return Promise.all(jobs); } function summarize(results: AuditResult[]): string { let passed: number 0; let failed: number 0; let timeout: number 0; results.forEach((r: AuditResult): void { if (r.status PASS) passed; if (r.status FAIL) failed; if (r.status TIMEOUT) timeout; }); return PASS${passed} FAIL${failed} TIMEOUT${timeout}; }这段代码不依赖真正的线程并行Promise是异步结果的组织方式不能据此宣称五项同时使用了五条系统线程。若改用TaskPool或者Worker做CPU密集的图像处理还要额外分析线程间对象传递、任务取消和背压不能仅改一个函数名。这里重点是报告顺序和每一项的终态而不是性能加速指标。五、页面只消费这一轮退场以后不接受任何迟到覆盖为了避免把方案停留在算法层最后补上 AssetAuditPage 的显示骨架。页面第一次出现时发起演示检查在退出时把alive置为假并递增代次。再次进入或重新运行时用新代次标识新的一轮。即便上一轮晚一点返回旧快照也不允许替换新结果。这里的phase在等待阶段为 RUNNING五项均归档后如果有FAIL或者TIMEOUT就显示DONE_WITH_ERRORS。页面中保留三种统计与每个ID的独立状态而不是只给出一个醒目的红叉。演示日志所示的PASS3 FAIL1 TIMEOUT1由上面的固定样本推导尚未经过鸿蒙设备或商店审核环境执行。Entry Component struct AssetAuditPage { State phase: string READY; State results: AuditResult[] []; State summary: string PASS0 FAIL0 TIMEOUT0; private epoch: number 0; private alive: boolean false; aboutToAppear(): void { this.alive true; this.startAudit(); } aboutToDisappear(): void { this.alive false; this.epoch; } private async startAudit(): Promisevoid { const current: number this.epoch; this.phase RUNNING; this.results []; const done: AuditResult[] await auditInInputOrder(INPUTS); if (!this.alive || current ! this.epoch) { return; } this.results done; this.summary summarize(done); this.phase done.some((r: AuditResult): boolean r.status ! PASS) ? DONE_WITH_ERRORS : DONE; console.info([DEMO] PB-2504 ${this.summary}); } build() { Column({ space: 14 }) { Text(ProofBatch · PB-2504).fontSize(22) Text(this.phase).fontSize(18) Text(this.summary).fontSize(16) ForEach(this.results, (item: AuditResult) { Text(${item.id} ${item.status}).fontSize(16) }, (item: AuditResult) item.id) }.width(100%).padding(24) } }这里aboutToDisappear做的是禁止迟到结果提交并不会中途杀死正在等待的 Promise。因为我们把单项预算限制为800毫秒示例会在合理的演示时间内自然收束真实检查器还必须额外实现取消和资源关闭。对页面长期可用的产品还要把startAudit的异常兜底上移一层把输入校验、重复点击防抖、取消按钮和无障碍文案纳入验收而不是仅靠最终文字状态。六、用五个确定性输入解释完整报告的价值我为这篇文章记录的模拟时间线是IMG-01在120毫秒返回PASSIMG-03在180毫秒返回FAILIMG-02在260毫秒返回PASSIMG-04在340毫秒返回PASSIMG-05在800毫秒先被本地预算标成TIMEOUT直到1200毫秒才返回真但只留下ignored-late日志。它们完成的顺序与输入序列不同最终报告的排列顺序却始终是01、02、03、04、05。最终状态为DONE_WITH_ERRORS统计严格是 PASS3、FAIL1、TIMEOUT1。这个统计是样本函数的确定性推论可以在独立JavaScript环境对纯逻辑进行复算但不能替代目标SDK编译、真实图片解码、真实审核接口或网络故障注入的证据。要把结果接进发布流程仍需定义每种FAIL的证据链接和能够复检的输入摘要。实践中还要区分两个概念TIMEOUT是本地等待契约而FAIL是检查规则给出的否定结论把超时误写成图片损坏会让后续同事沿着错误方向排查。尤其是文件读取、网络校验和云端鉴权混在一张清单里时建议让实际检查器再返回结构化原因码这篇演示只保留最小三态是为了准确展示控制流不是建议正式项目只显示三个英文词。七、这轮复盘留下的检查清单真正可用的批次报告应当对每个ID只给出一个终态完整保留输入顺序既能允许部分失败又不让迟到成功偷偷改变已经发布的结论。页面退出以后不再回写状态新批次必须换代次真实外部资源由各自检查器明确取消和释放。还要留出一个重要出口已经超时的检查在下一轮可以重新发起但必须有新的批次ID不能直接把旧批次记录改成PASS。这一套设计对上传前的图片预检、线下设备清单、课程素材质量门禁都有参考价值但它现在只是一份使用固定五条样本的控制流演示。文中的 DevEco 风格画面和纯手机画面都是人工制作的技术示意不是设备运行截图完整 ArkTS 仍需在实际目标SDK下编译并把接口实现、取消能力、文件权限以及异常处理逐项验收。参考资料华为官方 ArkTS 简介及定时器 API用于限定语言异步处理能力与定时器行为。见下列链接https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-overviewhttps://developer.huawei.com/consumer/cn/doc/harmonyos-references-V14/js-apis-timer-V14