• 2026年7月15日
  • 5 分钟阅读
  • SystemDo

扫码业务 APP 应用架构如何设计?模块化、状态与网络层

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

扫码业务 APP 应用架构如何设计?模块化、状态与网络层
SystemDo
SystemDo

软件定制开发团队

"真正有价值的技术内容,应该能帮助客户更快判断方向、预算和落地路径。"

扫码业务的核心技术挑战

扫码 APP 的核心是摄像头实时捕获图像、解析条码/二维码、触发后续业务逻辑。这个流程看似简单,但在企业级场景中,架构设计面临几个独特挑战。

第一,扫码是实时且高频的操作。用户连续扫码时,每秒可能产生 15-30 帧图像数据,这些数据要经过预处理、解码、结果校验,每一步都不能阻塞 UI 线程。

第二,扫码结果需要与后端服务交互。例如物流场景中,扫码后要立即查询包裹状态、更新库存或触发支付。网络延迟和异常处理直接决定用户体验。

第三,扫码业务通常嵌入更大系统。一个 APP 可能同时支持扫码出入库、扫码盘点、扫码追溯等多类场景,模块间边界不清晰会导致代码耦合严重。

这些挑战决定了扫码 APP 的架构不能简单套用通用 MVVM 或 Clean Architecture。下面从模块边界、状态管理、网络层、缓存和可测试性五个角度展开。

模块边界:按业务能力而非技术层次划分

常见错误是把扫码 APP 拆成“数据层、业务层、UI 层”。这种分层在简单 CRUD 应用中可行,对扫码业务则不够。因为扫码涉及摄像头控制、图像处理、解码引擎、业务逻辑,这些模块的依赖关系是网状的,不是线性的。

**推荐做法是按业务能力(Business Capability)划分模块**。每个模块包含自己的 UI、领域逻辑、数据和基础设施。扫码业务至少需要四个核心模块:

  • **ScannerModule**:负责摄像头管理、帧捕获、解码引擎集成。对外暴露“扫码结果”接口,不关心结果怎么用。
  • **BusinessModule**:负责具体业务逻辑,如库存校验、订单匹配、追溯查询。它依赖 ScannerModule 的结果,但不知道扫码是来自摄像头还是图片文件。
  • **NetworkModule**:负责与后端 API 通信。不包含业务逻辑,只提供请求/响应契约。
  • **PersistenceModule**:负责本地缓存和离线数据。与 NetworkModule 配合,实现缓存优先策略。

模块间通过接口依赖,而不是直接引用实现。例如 ScannerModule 定义一个 `ScanResult` 实体和一个 `ScanResultProcessor` 接口,BusinessModule 实现这个接口来处理业务。这样替换解码引擎(比如从 ZXing 换成 ML Kit)时,BusinessModule 不用改一行代码。

边界划分的关键原则是:**一个模块发生变更时,不应强迫其他模块变更**。扫码场景中,摄像头权限变化、解码库升级、后端 API 调整,都应只影响对应模块。

状态管理:区分扫描状态与业务状态

扫码 APP 的状态管理容易混淆两类状态:扫描过程的状态和业务执行的状态。

**扫描状态**包括:

  • 摄像头是否就绪
  • 是否正在解码
  • 最近一次扫码是否成功
  • 扫码模式(连续扫码/单次扫码)

**业务状态**包括:

  • 当前扫码结果是否已提交
  • 提交是否成功
  • 是否需要用户确认
  • 是否有待处理的离线数据

这两类状态应分开管理。扫描状态由 ScannerModule 内部持有,通过流(如 Kotlin Flow 或 RxJava Observable)对外发布。业务状态由 BusinessModule 管理,通常采用状态机或 Redux 风格的单向数据流。

实践中,我们使用一个轻量级状态容器,不引入全家桶式的状态管理框架。原因有三:

  • 扫码业务的状态数量有限,通常不超过 10 个状态。
  • 状态变化频繁(每秒多次),框架的 diff 和重渲染开销可能成为瓶颈。
  • 团队维护成本:引入 MobX、Redux 或 Bloc 需要学习曲线,而扫码 APP 的核心复杂度不在状态管理。

一个可行的状态设计示例:

```
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 码)。动态二维码(如一次性令牌)不应缓存。

**业务数据缓存**:扫码后查询的后端数据(如商品详情、库存数量)应缓存到本地。缓存策略采用“缓存优先 + 后台刷新”:

  • 先读本地缓存,立即展示给用户。
  • 同时发起网络请求,获取最新数据。
  • 如果网络数据与缓存一致,不做更新;如果不同,更新缓存并刷新 UI。
  • 缓存有效期根据数据变化频率设定。例如商品信息一天刷新一次,库存数据每 5 分钟刷新一次。

数据一致性问题是缓存设计的难点。扫码业务中,数据不一致通常表现为:用户扫了同一个条码,两次看到不同的库存数量。这其实是合理的,因为库存是实时变化的。我们不追求强一致性,而是最终一致性。用户应该看到“当前查询时的库存”,而不是“扫码瞬间的库存”。

如果业务要求严格一致性(如追溯码的防伪验证),则不应使用缓存,每次必须请求后端。架构上通过配置控制:哪些数据可缓存,哪些必须实时。

可测试性:模块解耦与模拟依赖

扫码 APP 的可测试性常被忽视。摄像头硬件、解码引擎、网络请求在单元测试中都不易模拟。如果架构设计没有考虑测试,后期补测试成本极高。

**核心原则:依赖注入 + 接口抽象**。

  • ScannerModule 提供 `CameraController` 接口,测试时注入模拟摄像头,返回预设帧或错误。
  • NetworkModule 提供 `ApiClient` 接口,测试时注入 Mock 响应,验证请求参数和重试逻辑。
  • BusinessModule 不直接创建任何依赖,所有依赖通过构造函数注入。

具体做法:

  • 解码引擎封装成 `Decoder` 接口,测试时注入一个返回固定结果的 Mock Decoder。
  • 摄像头帧数据通过 `FrameProvider` 接口传入,测试时用图片文件或视频流模拟。
  • 网络层使用 `HttpClient` 的拦截器机制,在集成测试中拦截请求并返回模拟数据。

扫码业务特有的测试场景包括:

  • 连续扫码压力测试:模拟用户 1 秒内扫 10 次,验证状态管理和网络队列是否正常。
  • 弱网测试:模拟网络延迟 2 秒、丢包率 30%,验证重试和离线队列是否按预期工作。
  • 权限拒绝测试:模拟用户拒绝摄像头权限,验证 APP 是否给出明确提示而非崩溃。

测试覆盖率应重点覆盖 BusinessModule 和 NetworkModule 的边界。ScannerModule 的测试可以简化,因为解码引擎本身是第三方库,测试重点放在集成而非单元。

在 SystemDo 的项目经验中,一个典型的物流扫码 APP 在重构后,测试用例从 50 个增加到 400 个,回归测试时间从 2 天缩短到 2 小时。这得益于模块边界清晰和依赖注入的引入。

架构决策的权衡与落地建议

没有完美的架构,只有适合业务的权衡。扫码 APP 的架构设计需要在以下方面做取舍:

**模块化粒度**:模块拆得太细,接口数量膨胀,开发效率下降。拆得太粗,代码耦合增加。建议以“一个模块负责一个业务能力”为基准,初期 4-6 个模块,后续按需拆分。

**状态管理复杂度**:如果业务状态超过 20 个,考虑引入 Redux 或 Bloc。如果少于 10 个,保持简单。扫码业务通常属于后者。

**缓存一致性**:严格一致性需求会增加网络依赖和延迟。与产品经理明确哪些数据必须实时,哪些可以接受延迟。大多数扫码场景可以接受 5-10 秒的延迟。

**测试投入**:扫码 APP 的测试自动化成本较高,尤其是摄像头模拟。建议优先保证 BusinessModule 和 NetworkModule 的单元测试覆盖率达到 80% 以上,集成测试覆盖核心流程。

最后,架构设计文档应随代码一起维护。每次模块边界调整、缓存策略变更,都要更新架构决策记录(ADR)。这对团队协作和后期维护至关重要。