手持终端盘点App设计:任务模型、离线优先与扫描交互

简介: "端侧是偶尔联网的独立系统";任务包四要素 + 基线裁剪;流水与结果分离;InventorySession 断点续盘代码(先写流水再更新结果);扫描三重反馈 + 去重窗口;待盘清单按期望位置排序,收尾时间压缩一半

盘点这件事,最终是人在现场用一台手持终端(PDA)一步步走出来的。后台架构做得再漂亮,端上卡顿、断网丢数据、扫了半天不知道盘到哪了,一线就会用脚投票——重新拿回纸质表。

本文聚焦手持端这一个常被低估的环节,按"任务模型 → 离线优先 → 扫描交互 → 端侧比对 → 回传"五个部分展开。

一、端侧不是"简化版网页"

很多团队的第一版盘点 App 是把 Web 页面塞进 WebView,能登录、能查列表、能提交。上线后通常会在三个场景下崩掉:

场景 Web 思维的坑 端侧应有的设计
地下室、库房深处没信号 页面白屏、提交失败、数据丢失 离线优先,无网可完整作业
连续扫描几百件资产 每次扫描等接口返回,卡顿明显 端侧即时处理,不等网络
盘到一半被打断(开会、下班) 进度丢失,从头再盘 断点续盘,进度本地持久化

一句话:手持端不是"连着网的瘦客户端",而是"偶尔联网的独立系统"。这个认知决定了后面所有的技术选型。

二、任务模型:下发"任务包"而不是"全量数据"

2.1 任务包里装什么

盘点任务下发到终端时,应打包成一个自包含的任务包:

InventoryTaskPackage
├── taskMeta      任务元信息(任务ID、范围、截止、口径说明)
├── baseline      基线数据(应盘资产清单:资产编号、名称、EPC/条码、期望位置)
├── rules         规则(是否校验位置、是否允许盘盈、差异阈值)
└── orgSnapshot   组织与责任人快照(端侧展示与归属判断用)

关键在于基线数据要在端侧落本地库。这样每一次扫描都能立刻判断"这是应盘的 / 盘盈的 / 已经盘过的",不需要联网查询。

2.2 基线要裁剪,别全量下发

一个常见错误是把资产全表(几十个字段)下发到终端。正确做法是按盘点需要裁剪字段:

保留字段 用途
asset_id、asset_code 唯一标识与展示
epc / barcode 扫码比对
asset_name、model 现场核对
expect_location 位置校验
status 判断是否已报废/封存

大字段(图片、长文本备注、附件)一律不下发,只在需要时按需拉取。一个 3 万条资产的盘点任务,裁剪后基线通常在 5~10MB,端侧 SQLite 完全吃得下。

三、离线优先:本地库是唯一真相源

3.1 端侧数据分层

本地库(SQLite / Room)
├── task_baseline    基线(只读,服务端下发)
├── scan_record      扫描流水(每次读到标签的原始记录,含时间戳、RSSI)
├── result_item      盘点结果(按资产聚合后的状态:已盘/盘盈/待盘)
└── upload_queue     上传队列(待回传批次,含失败重试状态)

设计要点:扫描流水与盘点结果分离。流水是原始事实(一次读取一条,不去重),结果是聚合视图(一个资产一条状态)。这样即便聚合逻辑后面要改,历史流水还在,可以重算。

3.2 断点续盘的实现

每次扫描后立即写入本地库并更新内存状态,App 被杀掉、没电关机都不影响。重新打开时按任务 ID 从本地库恢复:

/**
 * 盘点会话恢复:从本地库重建内存状态
 *
 * 模块:手持端盘点引擎
 * 节选自 首码信息 移动端盘点模块 InventorySession
 */
public class InventorySession {
   

    private final long taskId;
    private final ScanRecordDao recordDao;
    private final ResultItemDao resultDao;

    /** 内存态:资产编号 -> 结果项,避免每次扫描查库 */
    private final Map<String, ResultItem> cache = new ConcurrentHashMap<>();

    public InventorySession(long taskId, ScanRecordDao recordDao, ResultItemDao resultDao) {
   
        this.taskId = taskId;
        this.recordDao = recordDao;
        this.resultDao = resultDao;
    }

    /** App 冷启动或任务重新打开时调用 */
    public void restore() {
   
        cache.clear();
        for (ResultItem item : resultDao.findByTask(taskId)) {
   
            cache.put(item.getAssetCode(), item);
        }
        Log.i("首码信息-盘点", String.format(
                "任务[%d] 会话已恢复:已盘 %d 条", taskId, cache.size()));
    }

    /**
     * 处理一次扫描结果。
     * 纪律:先写流水,再更新结果,最后刷新界面。顺序不能颠倒——
     * 只更新结果不写流水,事后无法追溯"当时到底扫到了什么"。
     */
    public ScanFeedback onScan(String epc, String barcode, float rssi) {
   
        // 1. 落原始流水(即便这条标签不在基线里,也要记)
        recordDao.insert(ScanRecord.builder()
                .taskId(taskId).epc(epc).barcode(barcode)
                .rssi(rssi).scanAt(System.currentTimeMillis())
                .uploaded(false)
                .build());

        // 2. 匹配基线资产
        AssetRef ref = resolveAsset(epc, barcode);
        ResultItem item = cache.get(ref.getAssetCode());
        if (item == null) {
   
            item = ResultItem.builder()
                    .taskId(taskId)
                    .assetCode(ref.getAssetCode())
                    .status(ref.isInBaseline() ? ItemStatus.FOUND : ItemStatus.SURPLUS)
                    .firstScanAt(System.currentTimeMillis())
                    .scanCount(1)
                    .build();
            cache.put(ref.getAssetCode(), item);
        } else {
   
            item.setScanCount(item.getScanCount() + 1);
        }
        item.setLastScanAt(System.currentTimeMillis());
        item.setDirty(true);
        resultDao.upsert(item);

        // 3. 生成现场反馈
        return buildFeedback(item, ref);
    }

    /** 盘点到一半退出时,把脏数据刷盘 */
    public void flush() {
   
        resultDao.upsertAll(new ArrayList<>(cache.values()));
    }
}

几个容易被忽略的细节:

  • 先写流水再更新结果。只更新结果不落流水,事后无法解释"为什么这台设备判盘盈"——是没扫到,还是扫到了但被去重逻辑吞了。
  • 扫描次数要记录。某一台设备被扫了 40 次,往往说明它贴在一个信号特别好的位置(比如正对天线),现场容易反复触发,这个信息对后续分析误读有价值。
  • flush 时机:除了每次扫描写库,建议在 onPause、切换任务、电量低于阈值时强制刷盘一次。

四、扫描交互:让一线"不用想就能用"

4.1 反馈要三重:声音、震动、颜色

现场环境嘈杂(机房风机、车间设备),单一提示必然失效。一次扫描的反馈应当同步给出:

反馈通道 正常(首次扫到应盘资产) 重复扫描 盘盈(不在清单) 位置不符
声音 短促"滴" 无或低沉"嗒" 双声提示 长音警告
震动 轻震 无 双震 连续震
界面 绿色,条目置顶 灰色,仅更新次数 橙色,单独分组 红色,展开差异

重复扫描不给强反馈是刻意的——否则扫到已盘资产时响个不停,操作员会误以为出错。

4.2 连续扫描与去重窗口

RFID 手持支持"按住扳机连续读",一秒能读几十次。端侧要做去重窗口:同一 EPC 在 N 秒(通常 2~3 秒)内的重复读取只更新计数,不重复刷新界面、不重复触发动画。否则界面会疯狂闪烁,CPU 也会飙升。

4.3 盘盈不要"直接入库"

扫到不在清单里的标签,正确做法是进入待确认列表,而不是自动记为新增资产。原因很实际:

  • 可能是邻柜/邻架的资产串读进来了(尤其功率开大时);
  • 可能是已报废但未销账的资产还挂着标签;
  • 可能是本次盘点范围外的资产(比如兄弟单位寄放的)。

现场让操作员看一眼名称再确认,比回到办公室从几百条盘盈里捞要省事得多。

五、端侧实时比对:把"盘完才知道差多少"变成"盘到哪知道到哪"

传统流程是:全盘完 → 回传 → 后台比对 → 出差异表 → 再去现场复核。一轮下来至少两周。

端侧有基线数据,完全可以实时算:

已盘(FOUND)  = 基线里存在且已扫到
待盘(PENDING)= 基线里存在但未扫到   ← 界面实时显示剩余数量
盘盈(SURPLUS)= 不在基线但扫到了
位置异常(MISPLACED)= 扫到了但位置与基线不符

界面上常驻一行概览:"应盘 320 / 已盘 187 / 待盘 133 / 盘盈 4 / 位置异常 2"。操作员随时知道还剩多少,也知道哪些区域还没走到。

最有价值的是"待盘清单的现场定位":临近结束时把待盘资产按期望位置排序展示,操作员直接照着清单去补扫,不用整个区域重新走一遍。这一个功能通常能把收尾时间压缩一半以上。

六、回传:增量、幂等、可断点

6.1 回传的是"流水 + 结果"两份

  • 扫描流水:全量回传,作为审计凭证(谁在什么时候扫到了什么)
  • 盘点结果:按资产聚合后的最终状态,作为业务数据

流水体积大但价值高——差异复核时能回答"这台设备是全程没扫到,还是扫到过但被判为无效",这在责任认定上是天壤之别。

6.2 幂等与重试

弱网环境下,回传超时但服务端其实已收到的情况很常见。端侧必须支持安全重发:

批次号 batchId = taskId + 终端号 + 序号
服务端按 batchId 幂等:重复批次直接返回上次成功结果,不重复入库
端侧队列状态:PENDING → UPLOADING → SUCCESS / FAILED(保留重试次数)

重试策略建议:指数退避 + 上限 5 次 + 手动重试入口。不要无限自动重试——那会在弱网时把电量和带宽耗光。5 次失败后停在队列里,界面上明确提示"有 3 批数据未上传",让操作员回到有网的地方手动点一次。

6.3 回传完成不等于任务结束

服务端收到后还要做全局比对(多个终端各自回传的结果要合并、跨终端重复盘点要归并)。端侧状态应区分:

LOCAL_DONE(本机完成) → UPLOADED(已上传) → SERVER_MERGED(服务端已合并) → CLOSED(任务关闭)

一线最常问的问题是"我这台机器的数据传上去没有",把这四个状态在界面上显式展示,能省掉大量沟通。

七、性能与体验的几条硬约束

问题 表现 对策
大列表渲染卡顿 3000 条资产列表滑动掉帧 分页加载 + RecyclerView 复用 + 禁止在 bind 里做 IO
连续扫描耗电发热 一小时后烫手、掉电快 扫描功率按场景分档;不扫描时释放射频模块
界面刷新风暴 一秒几十次读取导致 UI 重绘卡死 去重窗口 + 节流,界面刷新限频(如 200ms 一次)
手套/强光下看不清 现场抱怨 字号不小于 16sp,关键按钮高度 ≥ 48dp,高对比配色
误触扳机 放口袋里一阵狂扫 支持锁定屏幕时自动停止扫描

八、上线前检查清单

  • [ ] 无网络环境下能否完整走完一次盘点(含扫描、比对、暂存)
  • [ ] App 被杀掉后重新打开,进度是否完整恢复
  • [ ] 扫描流水与盘点结果是否分离存储
  • [ ] 重复扫描是否有去重窗口,界面是否限频刷新
  • [ ] 盘盈资产是否进入待确认列表而非自动入库
  • [ ] 是否实时展示"应盘/已盘/待盘/盘盈/位置异常"概览
  • [ ] 待盘清单是否支持按期望位置排序定位
  • [ ] 回传批次是否有幂等键,失败重试是否有上限与手动入口
  • [ ] 端侧状态是否区分"本机完成/已上传/服务端已合并/任务关闭"
  • [ ] 连续扫描 30 分钟后的发热、耗电、内存表现是否实测过
  • [ ] 戴手套、强光环境下的可读性是否现场验证

写在最后

手持端的设计有一个简单的判断标准:让一个第一次上手的人,在没有培训、没有网络、被打断三次的情况下,依然能把盘点做完。

做到这一点,靠的不是多加几个功能,而是把"离线优先、流水留存、进度可恢复、状态可解释"这四件事做扎实。这四件事背后是同一个原则——把不确定性留在端上,把确定性交给系统:现场的情况千变万化,系统能做的是如实记录、准确反馈、随时可恢复,而不是假设一切按理想流程走。

相关文章
|
28天前
|
缓存 物联网 Java
RFID资产管理系统的高可用设计:从设备掉线到平台降级
分层可用性定义、设备心跳超时检测(带影响面的告警)、四类设备降级兜底表、MQ三机制、盘点洪峰错峰下发代码、“采集永不停、数据永不丢、交互可延迟”原则、RPO=0/RTO≤15分钟
|
8天前
|
监控 算法 物联网
标签打印与写码服务设计:RFID项目里最"不起眼"也最容易翻车的环节
RFID标签打印写码是数据源头,却常被轻视。本文揭示:读不到多因标签“本就没写对”。提出三大核心设计——打印与写码原子化(先写码、回读校验、再打印)、模板与指令解耦、作业幂等可追溯,并强调耗材管理、打印质量、设备监控与废标处置,确保源头数据零差错。
|
1月前
|
存储 物联网 测试技术
RFID资产盘点系统的性能压测与容量规划
本文直击RFID项目上线后性能衰减痛点,系统阐述如何以工程化思维构建高可用系统:科学定义SLO、分层定位瓶颈(空口/设备/平台)、基于资产规模精准估算容量,并实施场景化分层压测。强调“去重聚合下沉边侧”等关键实践,助项目摆脱“POC惊艳、量产崩盘”困局。
|
14天前
|
SQL 物联网
条码与RFID混合共存:存量系统的渐进式迁移设计
本文介绍RFID替代条码的渐进式迁移方法:拒绝“一刀切”,主张条码与RFID共存过渡。通过统一资产编号为业务主键、统一扫描入口自动识别、双轨盘点比对验证,实现平滑切换;分批换标、严守三条纪律,兼顾系统稳定、员工习惯与数据连续性。
|
17天前
|
运维 物联网 芯片
RFID标签全生命周期管理:发码、绑定、换标、坏标与退役
RFID标签全生命周期管理涵盖发码、绑定、换标、坏标处置与退役,强调标签与资产实体分离建模,通过六状态(ISSUED至RETIRED)和原子化事务保障数据准确,杜绝“一物多码”“幽灵标签”,夯实RFID数据治理根基。(239字)
|
1月前
|
存储 传感器 缓存
RFID智能管控柜设计与实现
格口级独立识别的天线设计(独立天线 vs 相控分区)、在位状态机+关门复核防误读、格口即权限的开门模型、告警三级分级、8项选型验收清单
140 1
|
1月前
|
存储 监控 安全
RFID系统数据安全与权限管控设计:从标签到审计的纵深防御
标签TID防克隆、写保护、区域白名单、“三权分立”权限模型、事前/事中/事后审计闭环 + 10项安全自检清单
163 2
|
2月前
|
存储 算法 物联网
RFID标签选型与编码体系设计:资产识别的"第一公里"
频段对比(LF/HF/UHF)、无源vs有源、抗金属标签、TID/EPC/User三区存储模型、分段编码规则、发码中心防重码、写码流程 + 选型Checklist
326 0
|
9月前
|
Linux Android开发 iOS开发
新一代的抓包神器——Reqable
本文介绍全平台抓包工具Reqable,支持Windows、Mac、Linux及Android、iOS,操作简单,无需复杂配置,可轻松抓取HTTP/HTTPS和WebSocket请求,适合开发者快速分析数据,提升调试效率。
5012 0
|
弹性计算 安全 数据中心
阿里云香港服务器提供高性能、安全、CN2高速网络、BGP多线精品
阿里云香港服务器提供高性能、安全、CN2高速网络、BGP多线精品
2094 0