关键词
AI预测 进销存 智能验收 自动化 供应商管理 溯源 四端协同 食堂数字化
去年帮一家三甲医院部署食堂管理系统的时候,踩过一个让我印象深刻的坑。当时系统已经跑了一个多月,财务对账的时候发现,某供应商连续三个月的结算金额和实际送货量对不上------差了将近8%。翻遍了入库记录才发现,仓库师傅每次手工登记重量的时候,经常漏记小数点,有时候忙起来甚至直接凭记忆"估个数"填上去。
这个问题让我意识到一件事:进销存系统的数据准确性,决定性因素不是后台逻辑写得有多严谨,而是前端数据采集环节有多可靠。人工录入天然有四个短板------延迟、遗漏、错误、主观。这四个短板加起来,基本等于"Garbage in, garbage out"。
后来我们团队基于好伙狮数字食堂的整体方案,重新设计了验收和出入库环节。核心思路就一条:把数据采集从"人录入"转变为"设备自动采集"。这篇文章把整个技术架构和实践经验整理出来,给有类似需求的同行做个参考。

好伙狮这套系统的架构本质上是一个四端协同的物联网+进销存平台。四端分别是:智能秤终端(数据采集层)、手持终端(移动操作层)、采购助手/供应商助手(业务协同层)、电脑管理后台(数据分析与管理层)。四端通过统一的服务端API进行数据交互,数据存储层负责持久化和查询。
架构分层
```text ┌─────────────────────────────────────────────────┐ │ 表现层 (Presentation) │ │ 管理后台(Web) 采购助手(Mobile) 供应商助手(Mobile) │ ├─────────────────────────────────────────────────┤ │ API网关 (API Gateway) │ │ RESTful + WebSocket 实时推送 │ ├─────────────────────────────────────────────────┤ │ 业务服务层 │ │ 订单服务 验收服务 库存服务 供应商服务 报表服务 │ ├─────────────────────────────────────────────────┤ │ 数据采集层 │ │ 智能秤SDK(称重+拍照+上传) 手持终端SDK(扫码+拍照) │ ├─────────────────────────────────────────────────┤ │ 基础设施层 │ │ MySQL(业务库) MongoDB(影像库) Redis(缓存) │ │ MinIO(图片存储) RabbitMQ(消息队列) │ └─────────────────────────────────────────────────┘ ```
数据采集层是整个系统的关键。智能秤通过串口通信读取称重传感器数据,同时触发摄像头拍照,两者数据打包后通过HTTP/HTTPS上传至服务端。手持终端基于Android系统,集成Zxing扫码库和CameraX拍照模块,扫码和拍照完成后通过统一的RestAPI提交出入库记录。
3.1 智能秤数据采集模块
智能秤终端采用ARM Linux系统,核心流程是:重量稳定检测→触发拍照→数据打包→上传服务端。重量稳定检测的阈值设置在±20g以内,避免运输过程中的震动触发误采集。大公斤量程的传感器选型需要注意温漂补偿,食堂后厨环境温差大,不做好补偿的话早晚误差能差出好几十克。
下面是一个简化版的重量采集和稳定判断逻辑(Python实现,运行在终端设备的边缘计算模块上):
```python # -*- coding: utf-8 -*- """ 智能秤数据采集模块 v2.3.1 功能:重量采集、稳定检测、触发拍照、数据打包上传 """ import time import threading from dataclasses import dataclass from typing import Callable, Optional # 稳定检测阈值,单位克 STABILITY_THRESHOLD = 20 # 稳定持续时间,单位秒 STABILITY_DURATION = 1.5 # 最小触发重量,低于此值忽略 MIN_TRIGGER_WEIGHT = 100 @dataclass class WeighingRecord: """称重记录数据结构""" weight_kg: float photo_path: Optionalstr timestamp: int device_id: str batch_no: str supplier_code: str order_id: str operator_id: str class WeighingController: """称重采集控制器""" def init(self, serial_port: str = "/dev/ttyS1", camera_device: int = 0): self.serial_port = serial_port self.camera_device = camera_device self.stable_count = 0 self.last_weight = 0.0 self.current_weight = 0.0 self.is_collecting = False def read_weight(self) -> float: """从串口读取重量传感器原始数据""" # 实际部署时通过pyserial读取 # ser = serial.Serial(self.serial_port, 9600, timeout=1) # raw = ser.readline() # return self._parse_weight(raw) return 0.0 # 伪代码占位 def stability_check(self, weight: float) -> bool: """检测重量是否已稳定""" if abs(weight - self.last_weight) <= STABILITY_THRESHOLD: self.stable_count += 1 else: self.stable_count = 0 self.last_weight = weight check_interval = 0.1 # 采样间隔100ms required_samples = int(STABILITY_DURATION / check_interval) return self.stable_count >= required_samples def capture_photo(self) -> str: """触发摄像头拍照,返回图片路径""" # 实际部署时使用OpenCV或设备SDK # cap = cv2.VideoCapture(self.camera_device) # ret, frame = cap.read() # path = f"/data/photos/{int(time.time())}.jpg" # cv2.imwrite(path, frame) return "" # 伪代码占位 def on_weight_stable(self, callback: Callable): """重量稳定后的回调,打包数据并上传""" if self.is_collecting or self.current_weight < MIN_TRIGGER_WEIGHT: return self.is_collecting = True photo_path = self.capture_photo() record = WeighingRecord( weight_kg=round(self.current_weight / 1000, 2), photo_path=photo_path, timestamp=int(time.time()), device_id="SCALE_DEVICE_ID", batch_no="", supplier_code="", order_id="", operator_id="" ) self.is_collecting = False callback(record) def loop(self): """主循环------轮询重量并检测稳定""" while True: self.current_weight = self.read_weight() if self.stability_check(self.current_weight): self.on_weight_stable(self._upload_record) time.sleep(0.1) def _upload_record(self, record: WeighingRecord): """上传称重记录到服务端""" import requests api_url = "https://api.haohuoshi.cn/v1/weighing/receive" try: files = {} if record.photo_path: files'photo' = open(record.photo_path, 'rb') data = { 'weight': record.weight_kg, 'timestamp': record.timestamp, 'device_id': record.device_id, 'batch_no': record.batch_no, 'supplier_code': record.supplier_code, 'order_id': record.order_id, 'operator_id': record.operator_id } requests.post(api_url, data=data, files=files, timeout=10) except Exception as e: # 网络异常时写入本地缓存队列,待恢复后重传 pass ```
3.2 手持终端扫码出库模块
手持终端运行的是定制Android系统,出库流程的核心是条形码扫描识别和出库记录提交流程。扫码模块基于Zxing库,支持EAN-13、CODE-128、QR码等常见编码格式。拍照功能使用CameraX API,在扫码成功后自动触发拍照记录物资状态。
实际部署中踩过的一个坑是连续扫码的速度问题。食堂仓库高峰期出库频次很高,如果每次扫码都要等拍照上传完成才能扫下一个,效率会大打折扣。我们的解法是把上传操作异步化------扫码识别后立即提交出库请求,拍照上传放到后台线程处理,不阻塞主流程。
```java /** * 手持终端出库控制器 v1.8.2 * 异步拍照上传 + 同步主流程,保证连续扫码效率 */ public class OutboundController { private static final String API_BASE = "https://api.haohuoshi.cn/v1"; private final ExecutorService photoExecutor = Executors.newSingleThreadExecutor(); /** * 处理扫码出库------主流程同步、拍照异步 */ public void handleScanResult(String barcode, String operatorId) { // Step 1: 同步------查询物资信息 MaterialInfo material = queryMaterialByCode(barcode); if (material == null) { showError("未找到该条码对应的物资信息"); return; } // Step 2: 同步------提交出库记录(核心业务) OutboundRecord record = new OutboundRecord(); record.barcode = barcode; record.materialId = material.id; record.materialName = material.name; record.operatorId = operatorId; record.outboundTime = System.currentTimeMillis(); record.warehouseId = getCurrentWarehouseId(); boolean success = submitOutbound(record); if (!success) { showError("出库提交失败,请重试"); return; } // Step 3: 异步------拍照留痕(不阻塞主流程) photoExecutor.submit(() -> { String photoPath = capturePhoto(); if (photoPath != null) { uploadOutboundPhoto(record.id, photoPath); } }); // Step 4: 播放成功提示音,准备下一次扫码 playSuccessBeep(); showBriefInfo(material.name + " 出库成功"); } private boolean submitOutbound(OutboundRecord record) { // 调用服务端API提交出库记录 // Retrofit POST 请求到 /outbound/submit return true; // 伪代码占位 } private String capturePhoto() { // 使用CameraX API拍照 // ImageCapture.takePicture() return null; // 伪代码占位 } private void uploadOutboundPhoto(String recordId, String photoPath) { // Multipart上传照片到 MinIO 对象存储 } } ```
3.3 核心数据库设计
验收记录表是整个进销存系统的核心表之一。设计上需要同时存储结构化重量数据和非结构化的图片路径,外键关联到采购订单和供应商。以下是简化后的表结构:
```sql -- 好伙狮进销存系统 v3.1.0 核心表结构 -- 数据库:MySQL 8.0 -- 验收记录表(核心表) CREATE TABLE `receive_record` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_id` BIGINT UNSIGNED NOT NULL COMMENT '关联采购订单ID', `supplier_id` INT UNSIGNED NOT NULL COMMENT '供应商ID', `material_id` INT UNSIGNED NOT NULL COMMENT '物资ID', `weight_kg` DECIMAL(10,3) NOT NULL COMMENT '称重重量(kg)', `photo_url` VARCHAR(512) DEFAULT '' COMMENT '验收照片URL(MinIO)', `photo_thumbnail_url` VARCHAR(512) DEFAULT '' COMMENT '缩略图URL', `batch_no` VARCHAR(64) DEFAULT '' COMMENT '批次号', `device_id` VARCHAR(64) NOT NULL COMMENT '称重设备ID', `operator_id` INT UNSIGNED NOT NULL COMMENT '操作员ID', `warehouse_id` INT UNSIGNED NOT NULL COMMENT '仓库ID', `quality_status` TINYINT DEFAULT 1 COMMENT '品质状态:1合格 2待确认 3不合格', `remark` VARCHAR(255) DEFAULT '' COMMENT '备注', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), INDEX `idx_order_id` (`order_id`), INDEX `idx_supplier_id` (`supplier_id`), INDEX `idx_created_at` (`created_at`), INDEX `idx_batch_no` (`batch_no`), INDEX `idx_device_date` (`device_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='验收记录表'; -- 出库记录表 CREATE TABLE `outbound_record` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `material_id` INT UNSIGNED NOT NULL COMMENT '物资ID', `barcode` VARCHAR(64) NOT NULL COMMENT '条码', `quantity` DECIMAL(10,2) NOT NULL COMMENT '出库数量', `unit` VARCHAR(16) NOT NULL COMMENT '单位', `photo_url` VARCHAR(512) DEFAULT '' COMMENT '出库照片URL', `operator_id` INT UNSIGNED NOT NULL COMMENT '操作员ID', `device_id` VARCHAR(64) NOT NULL COMMENT '手持终端ID', `warehouse_id` INT UNSIGNED NOT NULL COMMENT '仓库ID', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), INDEX `idx_material_date` (`material_id`, `created_at`), INDEX `idx_barcode` (`barcode`), INDEX `idx_device_date` (`device_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库记录表'; ```
影像数据(验收照片、出库照片)存储采用MinIO对象存储而非数据库直接存Binary,原因是食堂日均验收记录可能达到几十到上百条,每张照片按照2MB计算,一年下来TB级的存储量,放在MinIO里无论是扩容还是CDN分发都比直接放数据库里灵活得多。数据库只存URL和缩略图URL。
3.4 采购协同与消息推送
采购助手和供应商助手之间的订单协同采用的是一套基于消息队列的异步通知机制。采购方下单后,订单服务将订单写入数据库的同时,向RabbitMQ投递一条订单通知消息。消息包含订单ID、供应商ID等关键信息,供应商助手通过WebSocket长连接或FCM推送接收到通知后,引导供应商进入接单确认界面。
```yaml # 订单协同消息路由配置 v2.1.0 # RabbitMQ Exchange & Queue 绑定关系 exchanges: - name: order.exchange type: topic durable: true queues: - name: order.notify.supplier.{supplier_id} durable: true arguments: x-message-ttl: 3600000 # 消息过期时间1小时 x-dead-letter-exchange: order.dlx - name: order.notify.admin durable: true bindings: - exchange: order.exchange queue: order.notify.supplier.{supplier_id} routing_key: order.created.supplier.{supplier_id} - exchange: order.exchange queue: order.notify.admin routing_key: order.*.admin # 死信队列------超时未处理的订单自动标记 - exchange: order.dlx queue: order.timeout routing_key: order.timeout ```
供应商助手收到订单通知后,调用API完成订单确认,同时触发标签打印服务。标签打印使用标准的ZPL指令集,通过TCP/IP发送到供应商端的标签打印机:
```zpl ^XA ^FO50,50^A0N,32,32^FD订单号: {order_id}^FS ^FO50,90^A0N,28,28^FD品类: {category}^FS ^FO50,130^A0N,28,28^FD数量: {quantity}^FS ^FO50,170^A0N,24,24^FD送货日期: {delivery_date}^FS ^FO50,210^BY2,2,80^BCN,80,Y,N,N^FD{order_code}^FS ^FO50,300^A0N,24,24^FD好伙狮数字食堂^FS ^XZ ```

坑1:后厨网络环境比想象中差
食堂后厨的设备区往往是WiFi信号的死角,不锈钢设备又多,信号衰减严重。智能秤上传图片一旦失败,如果直接丢弃数据,验收记录就成了"无图"状态。解决方案是在终端侧加一个本地SQLite缓存队列,上传失败的数据先写本地,网络恢复后按时间顺序重传。重传机制加指数退避策略,避免恢复瞬间的请求风暴。
坑2:照片不是越多越好
最早版本设计是每次称重拍三张照片(全景、特写、俯拍),结果发现食堂日均几百次称重,存储成本和上传带宽压力都不小。跟实际使用的采购员聊了之后精简为拍一张------能看清货物全貌的高角度照片就够了。溯源的核心是"有图",不是"图多"。这个调整让存储和带宽成本降了六成,不影响实用性。
坑3:手持终端的续航
第一版手持终端频繁扫码拍照上传,电池撑不过一个完整的上午班次。后来做了两个优化:一是降低CameraX的拍照分辨率(出库照片用1080p就够了),二是增加了一个低功耗模式------屏幕熄灭期间暂停后台心跳,只有扫码才唤醒。优化后续航从不到4小时提升到接近8小时,基本覆盖一个全天班次。
好伙狮这套系统跑了一段时间之后,积攒了相当体量的历史采购数据------每天什么品类采购多少、什么时段消耗量大、什么季节哪些食材需求波动明显。这些数据为AI预测模型的训练提供了基础。
我们在v3.2版本中上线了一个实验性的AI采购建议功能,基于Prophet时间序列模型对历史采购数据进行拟合,结合节假日、天气、用餐人数等外生变量,输出未来一周的采购量预测。目前这个功能还处于持续优化阶段,预测准确率在常规工作日可以达到85%以上,节假日和特殊活动日还有待提升。
AI预测的上限取决于数据质量,而数据质量的上限取决于前端采集的准确性。这也是为什么我们在智能秤和手持终端的数据采集上花了大量精力------入口数据不准,后面的预测模型再复杂也是白搭。
好伙狮数字食堂这套以智能称重拍照溯源和四端协同为核心的进销存方案,本质上解决的是食堂采购管理中的两个根本问题:数据可信度和协同效率。
数据可信度靠的是"设备替代人工"------称重数据由传感器采集而不是人手写,照片由摄像头拍摄而不是人用手机拍,杜绝了人为偏差和主观造假的空间。协同效率靠的是"四端联动"------采购下单、供应商接单、仓库验收、后台管理四个环节在系统内完成信息流转,不再依赖电话和即时通讯。
技术上没有特别高深的东西,难的是把硬件设备、移动端应用、Web后台和消息系统有机地串在一起,让每一个终端的操作都服务于同一个业务闭环。这也是做toB系统最有挑战的地方------不是造轮子,是把轮子装到正确的轴上。
(版本信息:好伙狮进销存系统 v3.2.1,本文技术架构基于该版本撰写。文中涉及的代码示例为简化版伪代码,实际生产环境实现更为复杂。)