去年我接了一个很典型的项目:某仓储企业想上一套基于 Python 和 Vue 的物联网仓储仓库进销存管理系统可视化。刚听完需求,我以为就是把进销存模块和几个大屏图表拼在一起,结果真正落地才发现,这里面有两个很难缠的问题——库存账目不可信,物联网数据噪声大。
客户仓库不算小,几千个 SKU,十几个货架区,每天出入库单量在几百单左右。旧系统不是没有,但库存准确率长期在 90% 上下,月底盘点靠人海战术。仓库里刚装了 RFID 门禁、温湿度传感器和一批智能扫码枪,设备数据每天在涨,却和业务系统完全是两座孤岛。他们要的大屏,不只是把几个饼图柱状图放上去,而是要让一线人员和老板同时都能从数据里看到问题。
这篇文章我打算从需求梳理、技术选型、业务模块设计、可视化大屏落地到上线后的踩坑记录,完整讲一遍。适合正在做仓储数字化改造、进销存系统开发,或者想了解物联网实时数据怎么和业务系统融合的开发者参考。代码不会整份贴出来,但关键表结构、数据流和典型代码片段会讲透。
1. 需求分析:账面库存、实物库存与可视化大屏的真实关系
1.1 旧系统真正的问题不是"不能用",而是数据不可信
旧进销存系统每天录入出入库单据,按理说账面库存应该是实时准确的。但真实情况是:仓库员工先拣货后补单,甚至隔天补单;收货时数量按整托估算,拆零部分差一点也没人管;盘点靠打印 Excel 表再人工圈数,数据根本对不上。当账面库存和实物库存长期不一致的时候,系统里的库存数字就变成了参考值,管理层真正依赖的仍然是 Excel 和微信群。
我后来总结出一个判断标准:进销存系统的核心不是能不能录单据,而是能不能让"账面库存约等于实物库存"。如果日常操作都要靠线下 Excel 辅助,那系统本身就已经失败了。所以需求阶段,我们把目标从"做一套可视化系统"改成了"先解决库存准确率,再谈可视化"。大屏上显示的数字再漂亮,如果底数是错的,那只是放大了错误。
1.2 物联网设备带来的不是数据,而是噪声
客户仓库里的物联网设备大致分成三类:
- RFID 门禁:托盘进出库时自动读取标签,一台门禁一天会产生几千条甚至上万条读取记录,重复读取非常常见。
- 温湿度传感器:每 30 秒上报一次数据,单个传感器一天 2880 条,几十个传感器就是每天十万级的数据量。
- 智能扫码枪:扫码枪本身是操作工具,但也通过 Wi-Fi 把每次扫描事件实时上传,数据一天也有几万条。
如果把这些原始数据直接塞进进销存业务表,表会很快膨胀,而且业务查询会被噪声干扰。比如一条 RFID 读取事件里包含托盘码、标签 EPC、读取时间、读写器编号、信号强度,真正对进销存有用的只有"哪个托盘、什么时间、经过哪个门",其余都是噪声。所以物联网接入层必须做清洗、去重、转换,并且和业务单据做关联,而不是把设备数据当成业务数据直接用。
1.3 可视化大屏要回答三个层级的问题
在设计大屏之前,我习惯先把使用角色拆开。不同角色想看的东西完全不同:
| 角色 | 核心关注 | 大屏内容 |
|---|---|---|
| 一线仓管 | 今天有哪些出库任务、货位在哪、哪个区域拥堵 | 任务队列、货位热力图、设备在线状态 |
| 仓库运营 | 出入库趋势、库存结构、缺货预警 | 出入库趋势图、品类库存占比、预警列表 |
| 管理层 | 库存准确率、周转率、资金占用 | 数字指标、趋势曲线、异常告警 |
如果你让一线仓管去看资金占用,他不会关心;如果你让老板去看货架热力图,他也不会有感觉。可视化不是把所有图表堆在一起,而是把同一个数据源按角色拆成不同的窗口。这也是我后来在需求文档里反复强调的一点。大屏本质上是一个管理闭环的出口,它把底层物联网数据和业务单据翻译成不同角色能快速决策的信号。
2. 技术选型:Python + Vue 这个组合是怎么定下来的
2.1 Python 后端:一套代码同时扛业务接口和物联网上报
后端选型时考虑过 Java、Go,但最终选择了 Python,主要原因是团队对 Python 最熟,而且项目里有一半工作量在物联网数据接入上。Python 生态里有 paho-mqtt 处理 MQTT 消息,pyserial 对接串口设备,pymodbus 读 PLC 和 Modbus 传感器,还有 pandas 做库存分析和报表统计,这些都是现成的轮子。
实际开发用的是 FastAPI,而不是 Django。原因有两点:FastAPI 的异步能力能扛住物联网设备高频率上报;自动生成的接口文档在对接前端和硬件设备调试时非常省事。项目的核心业务接口大概 40 多个,物联网上报接口 10 多个,FastAPI 维护起来很轻。如果你更熟悉 Django,用 Django REST Framework 也完全可以,只是异步处理上要多花点功夫。
2.2 Vue 前端:中后台生态成熟,大屏组件能快速集成
前端选择 Vue 3 而不是 React,不是谁比谁好,而是我们这边前端同事的日常主力技术栈就是 Vue。Vue 的组件化开发在复用表单、表格、图表组件时效率很高,生态里 Element Plus 已经把中后台常用的表单、弹窗、表格封装得很完整,开源图表库的 Vue 封装组件也很多,接入成本非常低。
此外,Vue 的单页结构很适合大屏项目。多页面跳转在视频拼接墙上体验很割裂,单页应用可以通过路由切换大屏、管理后台、设备监控等多个视图,状态统一管理,切换时不会丢失数据。
2.3 整体数据流:从传感器到浏览器要经过哪些环节
整个系统大致可以分成四层:
- 设备接入层:RFID 读写器、扫码枪、温湿度传感器各自通过私有协议或有线接口把数据送到边缘网关。
- 消息中间层:边缘网关把原始数据清洗成标准 JSON 后,发布到 MQTT Broker 或 Redis Stream,业务服务订阅消费。这一步非常关键,比让设备直连数据库要稳得多。
- 业务服务层:Python 服务一方面处理进销存业务接口,一方面消费物联网消息,落库并触发库存变更、预警判断。
- 展示层:Vue 大屏通过 WebSocket 订阅变更事件,从后端拉取聚合数据实时渲染。
用一句话概括就是:设备数据不直接进业务库,先到消息队列排队,业务服务按需消费,再通过 WebSocket 推给前端。这样即使某个传感器上报频率很高,也不会压垮业务数据库。
2.4 为什么不拆微服务
这里补充一点:项目一开始就给整个系统设计好了模块边界,但我们刻意没有拆微服务,而是用单体应用加模块化代码。原因很实际——几十个接口的单体服务,开发、部署、排错成本都最低。要处理大数据量时,可以用消息队列和聚合表先顶着,等真的到瓶颈了再拆也不迟。
3. 进销存业务模块:数据库设计、库存计算与物联网数据融合
3.1 基础档案:先把仓库、货位、物料和计量单位理清楚
进销存系统的第一步不是写代码,而是建档案。我这里列几张核心表的设计思路:
仓库表 warehouse:id、code、name、status、remark。code 必须唯一,作为外部系统对接的稳定标识。
货位表 location:id、warehouse_id、code、zone、aisle、shelf、level。货位编码尽量带有可读语义,比如 A-01-02-03 表示 A 区、01 巷道、02 货架、第 3 层,这样既方便打印标签,也方便前端做热力图的坐标解析。
物料表 material:id、sku_code、name、spec、unit、category_id、is_batch_managed、shelf_life_days。条码和 SKU 编号是必须的,条码用于扫码枪扫实物,SKU 编码用于系统内部逻辑。如果物料有批次和效期管理,需要单独建 batch 表,否则所有库存操作都会因为"同一 SKU 不同批次"而出错。
往来单位表 partner:id、partner_type(客户/供应商)、name、code、contact、phone。这张表用来关联采购入库单和销售出库单,逻辑比较简单。
3.2 单据与库存流水:真正的核心是流水表,而不是余额表
进销存系统最容易犯的设计错误是:每次出入库时直接修改 material 表的库存数量字段。刚开始没问题,数据量一大,历史追溯就非常痛苦。正确做法是设计成"单据 + 流水 + 余额快照"三层结构。
入库单(inbound_order):单头字段有单号、供应商、仓库、状态、单据日期;单明细字段有物料、数量、批次、货位。 出库单(outbound_order)结构类似,但是增加了预占逻辑。 库存流水(stock_transaction):每次出入库审核时,往这张表写入一条或多条记录,记录类型为 INBOUND 或 OUTBOUND,同时记录变更前数量和变更后数量。 库存余额(inventory):按仓库、物料、批次、货位保存当前可用数量,这是给查询和大屏用的快照表。
写库存变更的方法很简单,但必须注意事务:
async def audit_inbound_order(order_id: str): async with db.transaction(): order = await get_inbound_order(order_id, lock=True) if order.status != "DRAFT": raise BusinessError("单据状态不允许审核") for line in order.lines: await write_stock_transaction(order, line, txn_type="INBOUND") await update_inventory_balance( warehouse_id=line.warehouse_id, material_id=line.material_id, batch_no=line.batch_no, location_id=line.location_id, quantity_delta=line.quantity, ) await update_order_status(order_id, "APPROVED")这里共享锁 lock=True 和数据库事务是必须的,否则并发审核两张相同物料的入库单时,库存会被覆盖写丢。
库存预占是另一个容易忽略的点。创建出库单时不能直接减库存,而是先在 inventory 表的可用数量上预占;实际拣货出库确认后,再真正扣减。这样既保证其他订单不会超卖,也能支持拣货未完成时取消订单回滚预占。
3.3 RFID、扫码枪和温湿度传感器如何参与业务
RFID 门禁最常见的接入场景是收货。托盘经过收货口门禁时,读写器把一批 EPC 读取出来。我们做的不是让它直接生成入库单,而是生成一张"收货通知单"。原因很现实:RFID 只能证明"这托盘物理上进了仓库门",但不能证明货物已经质检合格、没有破损。所以流程上是:RFID 事件 -> 生成收货通知单 -> 质检员扫码确认 -> 生成正式入库单 -> 审核后增加库存。
这样设计还有一个好处:RFID 重复读取不会影响库存。比如同一个托盘在门禁通道口来回移动,系统会收到多次读取事件,但只要没有生成新的通知单,就不会重复加库存。我们在读取事件表上建了唯一约束(reader_id, epc, read_time),用于进一步去重。
扫码枪出库则更像一种防错机制:拣货员扫描货位码和物料条码,后端校验这个物料是否确实在这个货位、批次是否在有效期内、预占数量是否足够。校验通过后系统自动生成出库确认记录。这一步把流程从"事后补录"变成了"边做边录",库存准确率才真正有保障。
温湿度传感器和库存的关系是间接的。后端定时任务每 5 分钟读一次最新环境数据,一旦某区域温度超过阈值,就生成环境告警记录,并且与当时存放在该区域的物料批次建立关联。如果仓库有冷链要求,这个联动可以直接触发"禁止出库"或"先检后出"的状态。
3.4 多仓调拨与库存联动
当企业有多个仓库时,调拨业务是绕不开的。调拨单本质是"出库 + 入库"的组合,但我们用了一张调拨单(transfer_order)来同步处理两个仓库的库存变化,避免两个操作之间的时间差导致在途库存丢失。在途库存单独放在 transfer_item 表中,目的仓不能直接占用。
4. 可视化大屏落地:图表选型、实时刷新与指标口径
4.1 图表方案:为什么选择 ECharts 而不是商用拖拽大屏
市面上其实有两种大屏实现路线:一种是基于开源图表库自己写代码,另一种是商用拖拽平台,拖组件、配数据源、出 3D 效果。我们选了前者,核心原因是项目需要和进销存后台共用前端代码,而且大屏上很多交互是动态的,比如点击某个区域的热力图,要联动展示该区域的物料列表,拖拽平台很难做到这种自定义交互。
具体用 ECharts 还是其他开源库,我们对比过:ECharts 胜在图表类型全(折线、柱状、热力图、环形图都有),社区资料多,中文文档和现成 Demo 多,对国内开发更友好。定制一个 3D 货架效果时,我们用 ECharts 的 graphic 组件配合 CSS 3D 也能做,虽然效果不如专业可视化引擎,但对于进销存大屏已经足够。
4.2 实时数据刷新:WebSocket 推送还是定时轮询?
大屏数据实时性是有不同档位的。出入库趋势这类聚合指标,30 秒刷新一次和 1 秒刷新一次,对决策没有本质区别;但环境温湿度告警、设备离线提醒这类事件,必须秒级触达。
所以我们的做法是"轮询兜底 + WebSocket 推事件":
- 常规聚合数据由前端定时 30 秒调用 REST 接口刷新;
- 一旦有出入库审核、环境告警、设备离线事件,后端通过 WebSocket 推送"变更事件"(不是推送全量数据)。前端收到事件后,再针对性地刷新对应模块。
后端用 FastAPI 实现 WebSocket 很直接,伪代码大概这样:
@app.websocket("/ws/dashboard") async def dashboard_ws(websocket: WebSocket): await websocket.accept() client_id = str(uuid4()) dashboard_clients[client_id] = websocket try: while True: message = await websocket.receive_text() if message == "ping": await websocket.send_text("pong") except WebSocketDisconnect: dashboard_clients.pop(client_id, None)前端连接和重连的逻辑同样关键:
function connectDashboardWS() { const ws = new WebSocket(`ws://${location.host}/ws/dashboard`); ws.onmessage = (event) => { const evt = JSON.parse(event.data); dashboardStore.handleEvent(evt); }; ws.onclose = () => { setTimeout(connectDashboardWS, 3000); }; }注意 WebSocket 一定要处理心跳和断线重连。我们遇到过一两天运行后连接断开,大屏没有任何报错但数据不再更新的情况,加上前端心跳和自动重连后才稳定。
4.3 指标口径:先定义清楚再画图
大屏开发里最常见的冲突不是技术,而是同一个概念有多种理解。比如"库存总量",有人理解成所有物料数量之和,有人理解成 SKU 数量之和,还有人理解成库存金额。我们做的第一件事就是拉上运营、财务、仓库三方,把指标逐一定义清楚,然后写进字段说明文档。
我列一下这套系统里用到的几个核心指标:
| 指标名称 | 定义 |
|---|---|
| 账面库存 | 数据库 inventory 表中每个批次、货位的数量之和 |
| 可用库存 | 账面库存减去预占数量 |
| 在途库存 | 调拨单已审核但目的仓未收货的数量 |
| 库存准确率 | 最近一次盘点中账实一致的物料数 / 盘点物料总数 |
| 库存周转率 | 一段时间的出库成本 / 平均库存成本 |
指标定义清楚后,图表的选择才谈得上合理。我们最终的布局大致是:顶部一排数字翻牌器展示库存总额、今日出入库单量、缺货预警数、设备离线数;中间区域是出入库 14 天趋势折线图和 SKU 品类占比环形图;底部左侧是货位占用热力图,点击热力图格子可以弹出该货位的物料明细;底部右侧是温湿度曲线和预警滚动列表。
货位热力图是这套大屏里最有实用价值的一块。X 轴取货架巷道,Y 轴取层数,每个格子颜色代表库存占用比例,仓库主管一眼就能看到哪条巷道压力最大,哪些区域在闲置。这个图表的实现不算难,就是把 location 表的 zone、aisle、shelf、level 四个字段拆成坐标,再把 inventory 表按坐标聚合映射成矩阵。
5. 上线后踩过的坑:重复数据、大屏卡顿与数据权限
5.1 RFID 重复读取和延迟导致库存虚增
上线第一周,仓库主管反馈了一个严重问题:某托盘明明只入库了一次,库存流水里却出现了三条相同的入库记录。我们排查链路大概是这样的:
第一步,查 RFID 读取事件表,发现同一个 EPC 在门禁通道里的确被上报了多次; 第二步,查消费日志,发现采集服务和库存服务之间发生了一次网络超时,采集服务重试后,重复事件被再次消费; 第三步,确认根因:消费代码里没有做幂等,事件表和库存事务之间没有唯一约束。
修复办法是双管齐下。一是在 rfid_event 表上加唯一索引(reader_id, epc, read_time),数据库层面挡掉完全重复的读取;二是在消费逻辑里引入业务幂等键,每次生成收货通知单时用(epc, first_read_time)作为唯一标识,重复消费时直接跳过。自从这两层加上之后,RFID 虚增的问题再没有出现过。
这个坑也让我养成了一个习惯:凡是涉及设备上报的接口,一律默认"可能重复",消费前先做幂等检查。设备数据不像人录单据,它不会自动意识到自己重发了,可靠性必须靠系统保证。
5.2 大屏卡顿:明细表直接 join 是最大元凶
大屏开发初期,为了省事,后端接口直接 join 出入库明细表和库存流水表来算指标。测试环境数据量只有十几万条,感觉很快;上线一个月后,流水表到了几百万条,大屏接口一次查询要 8 到 10 秒,整个页面卡得没法看。
解决思路分三步:
第一,新建 dashboard_daily_summary 聚合表,按天、仓、物料分类预先汇总出入库数量、金额、库存变化量; 第二,用定时任务每天晚上对当天明细做一次增量聚合,大屏接口只查聚合表,不再查明细; 第三,对热力图这类固定维度查询,增加 Redis 缓存,缓存 5 分钟,大屏刷新时直接走缓存。
前端这边也有优化,不是图表越多越好。图表实例不要每次刷新都销毁重建,而是通过更新数据接口来刷新,否则页面会频繁重新渲染,肉眼可见地掉帧。大屏上能复用的组件尽量复用,尽量减少 DOM 数量。
聚合表看似多了一个步骤,但对系统整体性能的提升是决定性的。后来业务要增加新报表,我们基本都优先考虑"能不能通过聚合表实现",而不是临时去查明细。
5.3 多仓数据权限:一张大屏给不同人看不同内容
客户有总部和三个分仓。最开始我们只做了功能权限,没有做数据权限,结果某个分仓的主管登录后台,能看到其他分仓的库存和成本数据,差点造成数据泄露。这个问题暴露得晚,是因为测试阶段大家用的是管理员账号,没有模拟真实角色。
后期我们重新梳理了权限模型。用户表、角色表、功能权限表之外,增加了 user_warehouse 关联表,每个用户可以关联一个或多个仓库。所有列表和统计接口,在 SQL 查询层就强制带上 warehouse_id 过滤条件,不允许前端传入一个 warehouse_id 参数就以为能解决一切,后端必须从登录令牌里解析。
大屏也有两套处理方式:放在展厅的参观大屏不展示成本和利润信息,只展示库存总量、出入库趋势、设备在线率这些脱敏指标;各仓内部的运营大屏则展示全部业务数据。实现上其实就是在同一个大屏组件上开了一个"访客模式"开关,数据权限过滤在前端和后端同时起作用。
6. 回看整个项目:最有价值的不是代码而是库存口径
做完整套系统,我最大的体会是:技术选型、大屏图表、WebSocket 这些并不是最难的,最难的是和业务方把"库存"这件事的定义对齐。系统上线后,库存准确率从 90% 逐步提到 99% 以上,不是因为代码写得多花哨,而是因为把入库必审、出库必扫、盘点闭环这些规范真正落到了流程里,物联网设备只是让这些流程的执行成本更低。
另外,我在这个项目里还体会到一个很微妙的地方:和仓库老师傅沟通系统怎么改,比写代码更耗耐心。他们最怀疑的就是"系统又给我加工作量"。后来我们把新建收货通知单的操作做到移动端扫码一步完成,老师傅才愿意用。任何进销存项目,如果一线人员觉得系统是来"管他们"的,上线就很容易失败。
如果以后你自己也要做类似系统,我有几个比较实际的建议:
- 设备接入一定要先做协议转换和消息队列,再进业务库,不要让设备直连数据库;
- 任何设备上报的接口都要做幂等,默认设备会重复上报;
- 大屏指标先和业务方逐条对齐,写进口径文档,再开始画图;
- 权限问题在需求阶段就按真实角色模拟一遍,不要等上线后补;
- 库存操作必须在事务里锁行,避免并发审核把数量写丢。
最后再分享一个排查问题的小技巧:在 RFID 读取事件表和温湿度上报表里,同时记录设备端时间和服务端接收时间。判断数据延迟时,这两个时间戳一对比就能看出是设备上报慢,还是消费链路阻塞,省去了很多猜来猜去的时间。如果你只存一个接收时间,遇到问题就只能靠猜了。
这套系统的可扩展方向也不少,比如加入需求预测模型做自动补货建议,或者把盘点功能搬到移动端,用扫码枪配合生成盘点差异单。核心的进销存底座只要稳,上层应用随时可以长出来。