茶器艺科智造HarmonyOS应用实战-64-新送印会强制完成上一笔pending,为什么单槽调度容不下并发订单:把状态转换改成按订单管理

商城列表已经允许同时出现多笔订单,送印调度却仍只记一组 timer、订单 ID 和截止时间。第二笔送印到来时,代码先推进上一笔,再把这组字段整体覆盖;表面上是“继续处理”,实质上是用新订单改写旧订单的运行上下文。本文把问题收窄到一个核心:订单是多实例,调度状态也必须按订单区分。

第64篇封面

本文以 Gitee 仓库 aiding521/chaqi-app-giteemaster 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。

一、从三个单槽字段还原串单路径

静态源码显示,新送印会先调用一次强制推进,再写入唯一的待处理订单 ID,并把新句柄放进唯一 timer 字段。当前代码可以概括为:

this.applySlicePrintKickScheduled(true);
this.pendingSlicePrintOrderId = orderId;
this.printerOrderTransitionTimerId = setTimeout(...);

把操作顺序换成 A、B 两笔订单就容易看清风险:A 创建延迟回调,B 在 A 到期前进入;B 先推动 A,又把 pendingSlicePrintOrderId 与 timer 句柄换成自己的。此后“当前待处理订单”只能指向 B,A 的原始等待过程已经无法从这些字段独立追踪。这里确认的是源码结构与可发生的覆盖路径,不等于证明线上已经出现串单;设备调度、后台节流以及实际打印结果仍需运行证据。

二、并发能力必须与业务队列一致

解决方向不取决于 timer 写法,而取决于产品到底允许什么。若多笔订单能够各自推进,就需要让每个订单拥有独立调度记录;若一台打印机只能串行处理,也不该用“强制完成上一笔”模拟串行,而应显式增加 queued,由一个调度器按顺序选出下一笔。两种方案可以共用订单状态模型,却不能混用同一个“当前订单”字段。

第64篇流程图

流程图中的关键分叉应发生在调度前:先判断该订单是否已有任务,再依据并发或串行规则决定“独立启动、进入队列、拒绝重复”中的一种。页面负责发起动作和展示订单;订单状态转换负责判断合法迁移;timer Map 或扫描器只负责在正确时刻提出迁移请求。把这三层拆开后,B 的到来不再隐式改变 A 的终态。

三、让 timer 与 orderId 一一对应

如果选择每笔订单独立推进,最小变化不是再增加第二组字段,而是把运行句柄按 orderId 收进 Map。现有建议代码如下:

private transitionTimers: Map<string, number> = new Map();
private scheduleOrderTransition(orderId: string, delayMs: number): void {
  const id = setTimeout(() => this.updateShopOrderById(orderId, 'printing', 20), delayMs) as number;
  this.transitionTimers.set(orderId, id);
}

这段模型至少恢复了可寻址性:创建时用订单 ID 写入,回调时仍携带同一个订单 ID,取消时也能定位同一条记录。真正接入时还需要在回调执行后移除对应 Map 项,并规定同一订单重复调用 scheduleOrderTransition 时是覆盖、忽略还是先取消旧句柄。这个决定必须显式,不能依赖 Map 的最后一次赋值,因为 Map 更新并不会自动清除旧 timer。

Map 只保存当前进程资源,不能替代订单事实。订单状态、进度和可恢复的截止时间仍属于业务数据;timerId 离开进程就没有意义。把两者分开,才能既处理页面离开,也为后续进程恢复留下空间。

四、取消路径也要覆盖整个订单集合

单槽实现通常只会清理“最后一个句柄”。改成按订单持有后,统一清理就必须遍历集合,现有建议代码保留如下:

private cancelAllOrderTransitions(): void {
  this.transitionTimers.forEach((id: number) => clearTimeout(id));
  this.transitionTimers.clear();
}

这个方法适合组件销毁时释放其拥有的全部进程句柄;单笔取消则应根据 orderId 找到一个句柄,clearTimeout 后删除该键。两条路径不能互相替代:用户取消 A 时不应连带清掉 B,而页面整体销毁时也不能只清理当前选中的订单。

接回现有组件时,改动范围应集中在送印入口、订单状态更新、单笔取消和页面清理四个点。杯型参数、切片算法、商城筛选与档案页面并不参与这次身份覆盖,不应借机重构。这样评审时可以沿着一个 orderId 从创建一直追到终态。

五、把并发规则写成订单级转换表

当前情况新动作A 订单结果B 订单结果
无活动订单发起 AA 建立自己的调度记录不存在
A 为 pending再次发起 A保持原任务或明确重排,不能生成两个匿名回调不存在
A 为 pending发起 B(并行策略)A 继续保持 pendingB 建立独立记录
A 为 pending发起 B(串行策略)A 不被强制完成B 进入 queued
A 被取消取消 A仅 A 的句柄被清理B 不受影响
页面离开清理运行资源保存的业务事实不因清句柄而伪造完成B 同样按自身状态处理

这张表刻意不把“新订单到来”写成旧订单的完成条件。completed 应来自对应订单自己的完成事件或明确的恢复判断,而不是另一笔业务的副作用。若产品采用串行打印,还要补充 queued 到 printing 的选取规则;若采用独立模拟,则需要保证每个回调只更新其携带的 orderId。

六、测试重点是订单之间不互相污染

interface BoundaryCase<T> {
  name: string;
  input: T;
  expected: string;
}

const cases: BoundaryCase<string>[] = [
  { name: 'empty', input: '', expected: 'reject' },
  { name: 'normal', input: 'valid', expected: 'accept' },
  { name: 'repeat', input: 'valid', expected: 'idempotent' }
];

上面的通用边界用例可以保留,但在本篇应把 input 具体化为订单动作序列,例如 start(A)start(B)cancel(A),输出则记录每笔订单的状态与活动句柄数。最关键的断言不是“最后显示 printing”,而是 A 的每次变化都由 A 的事件触发,B 同理。

至少需要覆盖:同一订单重复送印、两笔订单紧邻送印、只取消第一笔、先完成第二笔、旧回调迟到以及页面离开后重新进入。纯逻辑可以验证迁移函数和 Map 管理约定,却不能替代定时器在设备前后台切换时的实际行为,因此源码级用例与设备级证据要分开记录。

七、区分页面寿命与订单寿命

private disposeOwnedResources(): void {
  // timer/listener只由创建它的组件清理
  // 异步结果写状态前核对taskId或pageGeneration
  // 可恢复任务先保存业务事实,再释放进程句柄
}

这段通用清理约束在本篇尤其重要:timer 属于创建它的页面或调度对象,订单却可能需要跨页面存在。aboutToDisappear 可以调用统一清理释放所有 timer,但不能因此把所有 pending 订单改成 completed;否则资源释放又变成了一次业务状态伪造。

如果页面重新出现,恢复逻辑应读取每笔订单保存的状态或截止时间,再逐笔决定立即补算、重新排程或等待用户处理。回到前台仅能补偿当前进程内的节流,不能证明进程被杀后的任务已经执行。运行句柄清零与业务任务终止是两件事,日志也应分别表达。

八、验收不能只看最后一张卡片

  1. 搜索送印路径,确认唯一 pendingSlicePrintOrderId 和唯一 transition timer 是否已被完整替代,而不是新旧机制并存。
  2. 先发 A、紧接着发 B,分别记录 A/B 的状态序列、回调携带的 orderId 和活动句柄数量。
  3. A pending 时取消 A,确认 B 的句柄与状态均未变化。
  4. 重复点击 A,确认约定的幂等或重排策略生效,没有遗留不可寻址的旧回调。
  5. 页面离开前保留两笔未完成订单,确认所有进程句柄被释放,但订单没有被批量伪造为完成。
  6. 返回页面或重启进程后,逐笔核对恢复决策,缺失字段不能自动走向危险终态。
  7. 构建、模拟器、真机和打印服务读回分别记录;未执行项继续写“未执行”。

九、四种看似解决并发的误修

误修问题正确方向
再加一个备用 timer 字段只能从一笔扩成两笔,容量仍写死用 orderId 寻址集合,或建立显式队列
B 来时先完成 A把新请求误当作 A 的完成证据A/B 各自由自身事件迁移
只把 timer 放进 Map同一订单重复调度仍可能遗留旧句柄规定覆盖、拒绝或先取消策略
离开页面就把 pending 全改完成混淆资源清理与业务终态释放句柄,保留可恢复事实
只观察 B 最后成功A 的错误终态被界面最新结果遮住记录并断言每笔订单完整序列

并发排障应先问“回调属于哪笔订单”,再问“谁拥有这个句柄”,最后问“什么事件有资格写终态”。延长等待时间或调整提示文案都不会回答这三个问题。

十、让模型、调度与页面各守边界

第64篇结构图

按本文静态基线,entry 是产品入口,libraryhsp 承担页面和交互,libraryhar 放置模型、算法、路由与通用服务。订单状态与合法迁移可以保持为纯模型;timerId 依赖当前进程和页面生命周期,适合由 HSP/entry 中明确的所有者持有。若以后抽成独立调度服务,也应继续区分可持久化字段与运行句柄。

entry / libraryhsp -> 页面、组件、生命周期
libraryhar        -> 纯类型、纯函数、可持久化模型
tests             -> 边界、幂等、恢复序列
runtime evidence  -> 日志、设备行为、服务读回

这份分层图不能自动决定并行还是串行,但能防止订单模型反向依赖页面 timer。测试层应输入一串带 orderId 的动作,验证每笔订单状态;运行证据层再核对设备上的回调顺序与服务结果。

十一、本文证据边界

baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending

本文能确认单槽字段、强制推进调用和按订单管理的风险边界,建议代码只展示最小收口方向。它没有证明 Map 方案已接入,也没有证明真实打印机支持并行;构建、模拟器、真机、账号、网络和性能结果均未执行。后续若产品规则明确只能串行,应把 queued 方案纳入设计,而不是把本文示例直接当成已验证实现。

十二、用 A/B 订单复现覆盖与隔离

复现时不要用“连续点几次,最后成功”作为步骤。先固定两笔身份明确的订单 A、B,再分别记录发起、排程、回调、取消和恢复事件。这样即使界面只显示最新 Toast,也能从轨迹判断旧订单是否被新订单强制改写。

场景操作序列必须观察的证据不足以作为结论的现象
单笔基线发 A→等待 A 到期A 的完整状态序列与句柄回收A 卡片最终显示完成
并行隔离发 A→A 未完成时发 BA/B 各有身份对应的迁移记录最后一条 Toast 正常
局部取消发 A、B→只取消 AA 句柄删除,B 继续保持原计划两张卡片仍在列表中
页面离开发 A、B→立即返回所有 owned timer 清零,业务状态未被伪造页面没有崩溃
进程恢复保存两笔事实→杀进程→重开每笔截止时间与恢复决策可对应仅做前后台切换

日志不要只打印 successfailed。至少带上 orderId、调度时间、回调时间、旧状态、新状态和终态来源;只有涉及页面级异步时才需要 pageGeneration。涉及账号或材料时只记录必要状态码,不记录令牌、完整输入或敏感内容。

interface OperationTrace {
  taskId: string;
  generation: number;
  startedAtMs: number;
  finishedAtMs?: number;
  terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}

function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
  return trace.generation === currentGeneration &&
    trace.finishedAtMs === undefined;
}

这段通用轨迹结构可以辅助拒绝重复结果,但按订单并发时,taskId 应能够关联到具体 orderId,不能只依赖当前页面 generation。评审还要追问:重复回调是否幂等、B 先结束是否会推动 A、取消 A 是否影响 B、旧版本订单缺少截止时间时采取什么保守策略。四个答案都应能在状态转换或测试中找到,而不是藏在某个按钮回调里。

真实验收应保留 A/B 两笔订单的截图、带身份的日志片段和可重放步骤。编辑器预览只能说明页面能展示,本地模拟状态也不是打印服务回执。本文基线没有执行这些运行步骤,因此结果栏仍保持“待验证”。

补充审查要求:示例里的状态名、订单字段和延迟值都要与变更时仓库核对;若源码、SDK 版本或“并行/串行”产品规则变化,应重新建立基线,不沿用本文快照。没有执行的构建、设备、网络和账号步骤继续标记为未执行。

Logo

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

更多推荐