1902 字
10 分钟
··
移动端扫码收料的防错状态机设计

在移动端现场操作系统里,扫码看起来只是一个输入动作,但真实链路往往不止“扫到码 -> 调接口 -> 展示结果”这么简单。

一个典型场景是:用户先选择或扫描目标容器,再连续扫描资源标签;如果目标容器是智能设备,还要等待设备按钮确认或异步回调;如果资源标签已经绑定过业务单据,还要给出上下文;如果扫描失败,也不能让用户误以为已经入账。

这类流程最容易出问题的地方,不是接口写不出来,而是状态边界不清楚:什么时候允许扫下一盘?什么时候应该阻止重复请求?什么时候可以切换目标容器?什么时候必须刷新统计?

背景:一个抽象场景#

假设有一个移动端收料页面,需要完成下面几件事:

  • 按业务单据筛选待处理任务。
  • 进入详情后绑定一个目标容器。
  • 扫描资源唯一标识,完成收料和发料动作。
  • 如果目标容器需要硬件确认,则等待设备事件。
  • 页面上展示本次扫描记录、成功失败结果和汇总数量。

流程中同时存在用户输入、接口响应、硬件回调和页面状态刷新。只靠一个 loading 很难表达完整状态,需要把流程拆成更明确的状态机。

flowchart LR
A["选择任务"] --> B["绑定目标容器"]
B --> C["扫描资源码"]
C --> D{"是否需要设备确认"}
D -->|否| E["直接完成入账"]
D -->|是| F["创建待确认会话"]
F --> G["等待设备按钮/回调"]
G --> E
E --> H["刷新统计和记录"]

问题拆解#

移动端扫码页面常见的问题有四类。

第一类是目标上下文缺失。资源扫进来之前,必须知道它要进入哪个容器、哪个位置、哪个处理节点。如果页面允许在未绑定目标时扫码,后端即使能拦截,也会给用户带来反复失败的体验。

第二类是连续扫码过快。扫描枪或硬件按键可能连续触发,网络请求也可能还没返回。如果没有请求中状态,用户可能把同一个资源扫两次,或者在上一个资源还等待设备确认时扫入下一个资源。

第三类是异步确认悬空。智能容器、货架按钮、外部设备回调并不一定和 HTTP 请求同步完成。前端必须保存“当前等待中的会话”,并在它完成前限制其他操作。

第四类是结果回显不足。现场用户不一定记得刚刚扫了什么,所以需要展示本次扫描记录、失败原因、数量、当前统计,而不是只弹一个很快消失的 toast。

方案设计#

移动端:把页面状态拆小#

一个可维护的扫码页面,至少需要区分这些状态:

  • targetId:当前是否已经绑定目标容器。
  • scanMode:当前扫的是目标容器,还是资源标签。
  • requesting:是否存在未完成的扫描请求。
  • activeSessionId:是否存在等待设备确认的会话。
  • scanRecords:本次扫描记录,用于现场回溯。
  • summary:已处理、待处理、异常等统计数据。

这些状态各自表达不同含义,不能互相替代。例如 requesting=false 不代表可以继续扫,因为 activeSessionId 可能仍然存在。

一个简化后的守卫逻辑可以写成这样:

function canScanMaterial(state) {
if (!state.targetId) return { ok: false, message: '请先绑定目标容器' }
if (state.requesting) return { ok: false, message: '上一笔正在处理中' }
if (state.activeSessionId) return { ok: false, message: '请先完成设备确认' }
return { ok: true }
}

这个例子看起来普通,但它解决了一个核心问题:把“能不能扫”的判断集中起来,而不是散落在按钮禁用、输入框事件、接口调用前后。

后端:响应要带足上下文#

移动端扫到唯一标识后,后端不能只返回“成功/失败”。更好的响应应该包含:

  • 资源是否已绑定。
  • 当前数量或累计数量。
  • 资源所属任务或来源单据的摘要信息。
  • 当前资源的关键展示字段。
  • 明确的失败原因。

这样前端才能在本次扫描记录里展示“为什么成功、为什么失败、属于哪个上下文”,避免用户只能靠口头确认。

设备协同:用会话表达异步等待#

如果扫描动作需要外部设备确认,前端可以把“等待设备确认”建模成会话:

async function handleScan(code) {
const guard = canScanMaterial(pageState)
if (!guard.ok) return showTip(guard.message)
pageState.requesting = true
try {
const result = await api.scan(code, pageState.targetId)
if (result.needDeviceConfirm) {
pageState.activeSessionId = result.sessionId
pageState.activePartNo = result.partNo
return
}
appendRecord(result)
refreshSummary()
} finally {
pageState.requesting = false
}
}

这里的关键不是具体字段,而是把异步等待变成页面的一等状态。只要 activeSessionId 存在,就不允许继续扫下一笔;设备回调完成后,再清空会话并刷新统计。

交互细节#

列表页和详情页的职责也要分开。

列表页负责筛选、分页、状态过滤和草稿拦截。比如未发布或未生效的任务,不应该进入扫码详情页,否则用户到了现场才发现不能操作。

详情页负责目标容器选择、扫码模式切换、扫描记录和统计展示。目标容器可以来自手动选择、扫描识别或快速创建,但最终都要落到同一个 targetId 上。

这样拆分之后,列表页只回答“这张单能不能进”,详情页只回答“这一盘能不能扫”。

常见坑#

第一,按钮禁用了,但扫码枪仍然能触发输入事件。移动端防错不能只依赖按钮状态,扫码事件入口也要调用同一套守卫逻辑。

第二,接口失败后忘记恢复状态。requesting、扫码模式、输入焦点、定时器都要在失败路径里处理,否则页面会进入“看起来没在加载,但怎么也扫不动”的状态。

第三,设备回调和页面卸载冲突。离开页面时要清理 MQTT 或事件监听,避免旧页面收到新页面的设备事件。

第四,只展示最后一条结果。现场连续扫码时,用户需要看到多条记录和失败原因,否则一旦漏扫或重复扫,很难定位。

可复用经验#

  • 扫码前置条件要集中成守卫函数。
  • 请求中状态和异步确认状态要分开。
  • 设备确认要有会话 ID,而不是靠页面文案猜测。
  • 后端响应要提供足够展示上下文。
  • 扫描记录应该包含成功、失败、数量和原因。
  • 页面卸载时清理监听器、定时器和输入焦点逻辑。

总结#

移动端扫码流程的难点,不在于“识别一个码”,而在于把现场操作、后端校验和设备回调放进同一个可控状态机。

当页面能明确回答“当前扫什么、扫到哪、是否等待、能否继续、失败原因是什么”时,扫码功能才真正具备现场可用性。

移动端扫码收料的防错状态机设计
https://blog.hiauto.me/posts/2026-07-03-mobile-scan-receiving-error-proof/
作者
Kris_Wen
发布于
2026-07-03
许可协议
CC BY-NC-SA 4.0

评论