本文从软件架构角度讨论扫码业务 APP 的模块边界划分、状态管理策略、网络层设计、缓存机制与可测试性实践,面向有经验的开发者和技术决策者。

软件定制开发团队
"真正有价值的技术内容,应该能帮助客户更快判断方向、预算和落地路径。"
扫码 APP 的核心是摄像头实时捕获图像、解析条码/二维码、触发后续业务逻辑。这个流程看似简单,但在企业级场景中,架构设计面临几个独特挑战。
第一,扫码是实时且高频的操作。用户连续扫码时,每秒可能产生 15-30 帧图像数据,这些数据要经过预处理、解码、结果校验,每一步都不能阻塞 UI 线程。
第二,扫码结果需要与后端服务交互。例如物流场景中,扫码后要立即查询包裹状态、更新库存或触发支付。网络延迟和异常处理直接决定用户体验。
第三,扫码业务通常嵌入更大系统。一个 APP 可能同时支持扫码出入库、扫码盘点、扫码追溯等多类场景,模块间边界不清晰会导致代码耦合严重。
这些挑战决定了扫码 APP 的架构不能简单套用通用 MVVM 或 Clean Architecture。下面从模块边界、状态管理、网络层、缓存和可测试性五个角度展开。
常见错误是把扫码 APP 拆成“数据层、业务层、UI 层”。这种分层在简单 CRUD 应用中可行,对扫码业务则不够。因为扫码涉及摄像头控制、图像处理、解码引擎、业务逻辑,这些模块的依赖关系是网状的,不是线性的。
**推荐做法是按业务能力(Business Capability)划分模块**。每个模块包含自己的 UI、领域逻辑、数据和基础设施。扫码业务至少需要四个核心模块:
模块间通过接口依赖,而不是直接引用实现。例如 ScannerModule 定义一个 `ScanResult` 实体和一个 `ScanResultProcessor` 接口,BusinessModule 实现这个接口来处理业务。这样替换解码引擎(比如从 ZXing 换成 ML Kit)时,BusinessModule 不用改一行代码。
边界划分的关键原则是:**一个模块发生变更时,不应强迫其他模块变更**。扫码场景中,摄像头权限变化、解码库升级、后端 API 调整,都应只影响对应模块。
扫码 APP 的状态管理容易混淆两类状态:扫描过程的状态和业务执行的状态。
**扫描状态**包括:
**业务状态**包括:
这两类状态应分开管理。扫描状态由 ScannerModule 内部持有,通过流(如 Kotlin Flow 或 RxJava Observable)对外发布。业务状态由 BusinessModule 管理,通常采用状态机或 Redux 风格的单向数据流。
实践中,我们使用一个轻量级状态容器,不引入全家桶式的状态管理框架。原因有三:
一个可行的状态设计示例:
```
scanningState: {
cameraReady: boolean,
decoding: boolean,
lastResult: ScanResult | null,
error: ScanError | null
}
businessState: {
currentSession: Session,
submittedResults: List<ScanResult>,
pendingConfirm: List<ScanResult>,
offlineQueue: List<ScanResult>
}
```
扫描状态变化时,BusinessModule 通过观察者模式或事件总线接收通知,但不能反向控制扫描状态。例如用户连续扫码时,BusinessModule 只消费结果,不干预摄像头是否继续扫描。
企业扫码场景中,网络不可靠是常态。仓库、车间、户外,Wi-Fi 可能不稳定,移动网络可能覆盖不足。网络层设计必须支持离线优先(Offline-First)。
**核心设计要点**:
1. **请求队列**:所有业务请求(扫码提交、查询等)先写入本地队列,再尝试发送。队列使用 SQLite 或 Room 持久化,APP 崩溃或重启后不丢失。
2. **幂等性**:每个请求携带唯一 ID(UUID),后端对相同 ID 的请求做去重。这样重试不会产生重复数据。
3. **指数退避重试**:网络失败后,重试间隔从 1 秒、2 秒、4 秒递增,最长不超过 60 秒。重试次数上限设为 5 次,超过后标记为“待人工处理”。
4. **优先级队列**:高优先级请求(如支付扫码)优先发送。低优先级请求(如批量盘点)在后台批量处理。
网络层还应提供清晰的错误码和错误分类。不把 HTTP 状态码直接暴露给业务模块。例如 401 统一映射为“认证过期”,502 映射为“服务暂时不可用”。业务模块根据错误类型决定是否自动重试、提示用户或进入离线模式。
注意,网络层不包含业务逻辑。它只做一件事:把请求发出去,把响应带回来。业务逻辑在 BusinessModule 中处理响应结果。
扫码业务的数据缓存需要分两层:解码引擎缓存和业务数据缓存。
**解码引擎缓存**:条码/二维码的解码结果可以缓存一段时间。例如同一商品条码在 5 分钟内被扫多次,直接返回缓存结果,避免重复解码。缓存键是条码字符串,值包含解码时间戳和元数据。缓存大小限制在 1000 条以内,使用 LRU 淘汰策略。
需要说明的是,这种缓存只适用于静态条码(如商品 UPC 码)。动态二维码(如一次性令牌)不应缓存。
**业务数据缓存**:扫码后查询的后端数据(如商品详情、库存数量)应缓存到本地。缓存策略采用“缓存优先 + 后台刷新”:
数据一致性问题是缓存设计的难点。扫码业务中,数据不一致通常表现为:用户扫了同一个条码,两次看到不同的库存数量。这其实是合理的,因为库存是实时变化的。我们不追求强一致性,而是最终一致性。用户应该看到“当前查询时的库存”,而不是“扫码瞬间的库存”。
如果业务要求严格一致性(如追溯码的防伪验证),则不应使用缓存,每次必须请求后端。架构上通过配置控制:哪些数据可缓存,哪些必须实时。
扫码 APP 的可测试性常被忽视。摄像头硬件、解码引擎、网络请求在单元测试中都不易模拟。如果架构设计没有考虑测试,后期补测试成本极高。
**核心原则:依赖注入 + 接口抽象**。
具体做法:
扫码业务特有的测试场景包括:
测试覆盖率应重点覆盖 BusinessModule 和 NetworkModule 的边界。ScannerModule 的测试可以简化,因为解码引擎本身是第三方库,测试重点放在集成而非单元。
在 SystemDo 的项目经验中,一个典型的物流扫码 APP 在重构后,测试用例从 50 个增加到 400 个,回归测试时间从 2 天缩短到 2 小时。这得益于模块边界清晰和依赖注入的引入。
没有完美的架构,只有适合业务的权衡。扫码 APP 的架构设计需要在以下方面做取舍:
**模块化粒度**:模块拆得太细,接口数量膨胀,开发效率下降。拆得太粗,代码耦合增加。建议以“一个模块负责一个业务能力”为基准,初期 4-6 个模块,后续按需拆分。
**状态管理复杂度**:如果业务状态超过 20 个,考虑引入 Redux 或 Bloc。如果少于 10 个,保持简单。扫码业务通常属于后者。
**缓存一致性**:严格一致性需求会增加网络依赖和延迟。与产品经理明确哪些数据必须实时,哪些可以接受延迟。大多数扫码场景可以接受 5-10 秒的延迟。
**测试投入**:扫码 APP 的测试自动化成本较高,尤其是摄像头模拟。建议优先保证 BusinessModule 和 NetworkModule 的单元测试覆盖率达到 80% 以上,集成测试覆盖核心流程。
最后,架构设计文档应随代码一起维护。每次模块边界调整、缓存策略变更,都要更新架构决策记录(ADR)。这对团队协作和后期维护至关重要。
继续了解企业数字化、SEO / GEO、AI 自动化和软件定制开发中的常见问题。