ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot与微信小程序的物流仓储管理系统设计与实战

基于SpringBoot与微信小程序的物流仓储管理系统设计与实战

开头先说点实际的。上个月帮某小型物流公司梳理他们的仓储流程,发现还在用 Excel 记账、纸质单据流转,出入库靠人工记忆,月底盘点要翻一整天的聊天记录和本子。这种模式下库存不准是必然的,货物丢了都不知道是哪一环出的问题。当时我就意识到,这类中小仓储最缺的不是多智能的算法,而是一套能让现场人员用着顺手、管理者能实时看到数据的小系统。于是就有了这套基于 SpringBoot 和微信小程序的物流仓储管理系统,项目代号就叫它 w41h6a03。

这套系统的价值一句话能说清楚:仓库现场的扫码、入库、出库、盘点、查询全部搬到微信里,后端用 SpringBoot 把库存逻辑、权限控制和数据记录收得明明白白。管理员在手机上就能看库存、查操作记录,仓管员扫码就能完成货物收发,老板不用再到仓库里翻账本。对于正在做毕业设计、或者想给中小仓储做信息化的开发者来说,这篇文章不打算讲花哨架构,只讲我从需求拆解、表结构设计、接口实现到真机部署这一路走下来,哪些地方值得抄作业,哪些坑值得绕开。

1. 为什么"小程序 + SpringBoot"这套组合最适合这类项目:需求视角的取舍

1.1 仓库管理的真实痛点是什么

很多人一上来就把系统设计得很复杂,觉得仓储系统要有 WMS 级别的波次策略、库存周转分析、智能补货。但中小物流公司的实际情况根本不是这样。他们的痛点集中在三个地方:入库时货物和单据对不上,出库时找不到货或者拿错货,库存数据永远滞后于实物。

以这家公司为例,仓管员老张每天要做的事情是:货车到了,对照纸质送货单清点货物,把数量写在入库单上;客户来提货,凭单子进库房找货,出来再签字确认。听上去有流程,但一旦货量上来,单子多了就容易漏记、错记。月末盘点差异率长期在 3% 到 5% 徘徊。

所以我在梳理这块的时候,确定的第一原则是:系统要解决的是"录清楚"和"查得到",不是"算最优"。核心功能必须围绕货物的进出与库存现状展开,先把数据弄准,再谈分析和优化。这也是整个项目后面所有设计决策的出发点。

1.2 小程序端与后端的技术选型逻辑

为什么前端选微信小程序而不是 App 或者 Web 后台?我当时主要考虑了现场使用环境。仓库里信号不稳定,仓管员手上的终端千奇百怪,如果让每个仓管员都装一个 App,光适配和更新就是灾难。小程序扫码不用跳转 App,微信里直接扫,对一线操作工人几乎零学习成本,这是最关键的。

后端用 SpringBoot,理由更简单:生态成熟、上手快、招人容易。这套系统需要一个 Web 管理端(给管理员在电脑上操作),后端如果选 Node 或 Python 也能做,但 SpringBoot 加上 MyBatis Plus 之后,单表 CRUD 的代码量少得惊人,而且事务控制、连接池管理这些现成方案稳定。配合 MySQL 存业务数据,Redis 存登录态和分布式锁,这套组合足够撑起日均几千单的业务量。

1.3 功能清单要克制:哪些必须做,哪些第一版可以砍掉

项目一开始很容易膨胀,我通过和仓库主管反复访谈,把第一版功能收敛到下面这张表:

模块必须有的功能第一版先不做
登录微信静默登录、账号绑定、角色区分多租户、单点登录
入库扫码录入、手动录入、入库单生成、明细校验质检流程、上架策略推荐
出库扫码出库、库存校验、出库单生成拣货路径优化、波次拣货
库存实时查询、库存预警、操作日志库存周转率分析、ABC 分类
基础数据货物档案、仓库/库区维护多仓协同、调拨流程

砍掉这些功能不是能力问题,而是业务根本没有这个需求。第一版只要做到"每一件货从哪里来、到哪里去、系统说得清",就已经超过了 90% 的同行。

2. 从表结构开始把业务钉死:角色权限与核心数据模型

2.1 三类角色与权限边界

在动手写代码之前,我做的第一件事不是建 SpringBoot 工程,而是用一张图把角色理清楚。这个系统最终只有三类人:系统管理员、仓管员、普通用户(可能是客户或司机)。

管理员维护基础数据,看全局库存和操作日志,不发货不入库;仓管员执行出入库操作,扫码、核对、确认;普通用户只查自己的货。权限如果不分开,会产生很多笑话式的 Bug,比如客户把别人的库存单删了。

在 SpringBoot 里我用 Sa-Token 做登录鉴权,配合注解@SaCheckRole("admin")实现接口级角色控制。小程序端拿到登录凭证后,后端签发 token,后续请求带上这个 token,拦截器统一校验。这里有个细节:不要把小程序的 openid 直接当用户 ID 用,业务表里应该用自增user_id,openid 只存放在 user 表的关联字段里,否则将来做账号合并、多端登录会非常痛苦。

2.2 核心表的设计与关联关系

说一个我一开始没想透、后来返工过的点:货物表和库存表一定要分开。货物表(product)存的是商品的基础信息,比如名称、规格、条码;库存表(stock)存的是某个货物在某个库区的存量,比如 A 货在 1 号库区有 200 件。如果你把库存数量直接挂在货物表上,那么"同一个货放在两个库区"这个场景就无法表达,查询也只能一把抓。

核心表大概是这几张:

  • sys_user:用户表,字段包括账号、昵称、openid、角色标识(admin/keeper/user)
  • product:货物档案表,字段包括名称、规格、单位、条码(唯一索引)、图片 URL
  • warehouse:仓库表,这里可以做简化,只保留仓库名称和地址
  • stock:库存表,联合唯一索引(product_id, warehouse_id),字段包含quantity和locked_quantity
  • stock_in_order/stock_out_order:出入库单主表,字段包含单号、操作人、仓库 ID、状态、备注
  • stock_in_item/stock_out_item:出入库明细表,字段包含单号关联、货物 ID、数量、库区

这里有个很多人会忽略的设计:为什么要有 locked_quantity(锁定库存)这个字段?因为在出库流程中,订单先生成、仓库后拣货,中间有一段"已申请但未实际出库"的时间。如果没有锁定库存,两个客户同时下大单,系统会都提示有货,实际拣货时货不够,就会产生超卖。哪怕第一版流程简单,我也建议保留这个字段,后面扩展拣货流程会非常顺手。

2.3 库存字段类型和更新策略的两个关键决策

第一个决策是:库存数量用什么类型?答案是INT,不是DECIMAL。仓库里的货物只有整件整箱的概念,不存在 3.5 件,用 INT 省去一堆精度问题。如果业务有按重量计算的货物,那种场景建议单独建字段,不要和数量混在一个字段里。

第二个决策是:库存更新不能用"读出来改回去"的朴素写法,比如先SELECT quantity FROM stock WHERE product_id = ?,然后在 Java 里减掉 10,再UPDATE回去。这种写法在高并发下一定出问题。后面第 4 节我会专门讲我最终采用的方案,这里先提个醒。

3. 小程序端那些绕不开的实现细节:登录、扫码与出入库动线

3.1 微信登录怎么和自家用户体系打通

小程序端的登录流程是这类项目里最容易让新手卡住的地方。微信的wx.login()只能拿到一个临时 code,这个 code 要发给后端,由后端去微信的接口换 openid 和 session_key。注意:openid 换取的请求必须放在后端完成,小程序端直接请求微信接口会暴露密钥,这是安全红线。

我的做法是:小程序端调用wx.login()拿到 code,通过/api/auth/login传给后端;后端拿到 code 后调用微信接口换取 openid,然后查sys_user表:如果这个 openid 还没绑定,就自动创建一个新用户,默认角色是"普通用户";如果已绑定,就正常签发 token 返回。前端把 token 存到 storage 里,每次请求带上Authorization请求头。

这里要补充一个体验细节:自动注册的用户没有真实姓名和手机号,所以在小程序端个人中心加了一个"完善资料"的功能,用户至少绑一次手机号。仓管员角色由管理员在后台手动把某个用户设置为"keeper",这比小程序端自己选角色要安全得多。

3.2 扫码出入库的完整动线

扫码是这套系统里小程序端最核心的动作,它用到的 API 是wx.scanCode。这个 API 可以识别一维码(EAN-13、Code128 等)和二维码,扫描结果在result字段里带回。我踩过的一个典型问题是:货物条码贴着反光膜,扫不出来。这个属于硬件环境问题,代码里能做的优化是加上scanType: ['barCode', 'qrCode']参数,并提示用户调整光线角度。

入库动线我设计成四步,每一步都有独立界面:

  1. 仓管员点"扫码入库",调起wx.scanCode;
  2. 扫到条码后,后端根据条码查product表,返回货物名称和规格;
  3. 仓管员填写本次入库数量和目标库区;
  4. 提交入库单,后端扣减锁定库存(如果是采购入库则没有锁定概念)、增加实际库存、写操作日志。

出库动线则是反过来:扫货物条码、选关联出库单、填出库数量、提交。为了减少仓管员的操作次数,每个页面都支持"连续扫码"模式:扫一个货,填数量,自动进入下一个商品添加状态,不用再点一次"添加商品"按钮。这一个小细节让仓管员每天扫码的手速快了很多。

3.3 表单校验与空状态处理

小程序端最容易被吐槽的就是"点了没反应"。我在这套系统里做了一个统一请求封装,所有接口返回统一结构{ code: 0, msg: "success", data: ... }。前端封装里做了三件事:code 不为 0 时弹出 msg 提示;token 过期时跳转登录页;网络异常时显示重试按钮,而不是白屏。空状态也做了统一组件,"暂无数据"配上操作按钮,比干巴巴一行文字好用很多。

另外一个细节是表单校验。例如出库数量不能大于库存数量,这个校验不能只靠后端,前端也要提前判断,因为后端校验返回错误再弹窗,用户感知到的延迟非常明显,特别是仓库里网络不好时。前端先判断一次,后端再兜底校验一次,体验和安全都能兼顾。

4. SpringBoot 后端要扛住的关键逻辑:接口设计、并发扣减与事务

4.1 接口分层与统一返回结构

后端工程我按 controller -> service -> mapper 三层来组织。controller只负责接收参数和调用 service,不写业务逻辑;service层承载所有业务规则,比如出入库数量校验、库存扣减、日志记录;mapper层用 MyBatis Plus 的BaseMapper解决单表 CRUD。

统一返回结构我选了比较经典的 Result 类:

public class Result<T> { private Integer code; private String msg; private T data; }

这里的 code 我定义成:0 表示成功,非 0 表示业务失败。还有一类是 401,表示未登录或 token 过期。这个结构看起来简单,但它统一了前后端的契约,前端封装的request.js里只需要判断res.code === 0即可,不需要对每个接口写不同的错误处理逻辑。

4.2 并发出入库的库存扣减方案

这是我全项目最重视的地方。前面提到过"先查再改"的方式会出问题,那换成什么?我最终是这么做的:

用乐观锁 + 条件更新。每次更新库存时,不先去查询,而是直接在 SQL 里带条件执行更新操作,返回受影响行数来判断是否成功。比如出库扣减 10 件:

UPDATE stock SET quantity = quantity - 10, version = version + 1 WHERE product_id = ? AND warehouse_id = ? AND quantity >= 10

这行 SQL 的关键在于quantity >= 10这个条件。MySQL 在执行 UPDATE 时会锁住匹配的行,所以quantity - 10是原子操作,不会出现两个请求同时读到 100 再各自扣成 90 的问题。如果第二个请求进来时库存已经被扣到 5,不满足quantity >= 10,受影响行数就是 0,程序检测到后返回"库存不足"。

同时在stock表加了一个version字段做乐观锁,扣减成功的同时版本号自增,这样如果将来并发量更大,可以在这套逻辑之上再做版本冲突重试。第一版其实不强制要 version,但提前埋好这个点,后续演进不用改表结构。

4.3 事务与日志怎么配合

出入库一旦涉及多张表,就必须引入事务。入库操作要同时更新两张表:stock表的库存数和stock_in_item表的明细分录,中间任何一步失败,前面成功的更新也要回滚。我的实现是在 service 方法上标注@Transactional,同时把事务边界控制在 service 层,不要在 controller 层开事务。

日志方面,我单独建了一张operation_log表,记录操作人、操作类型(IN/OUT/UPDATE)、货物 ID、数量、操作时间。注意,日志表的写入必须在业务事务之外或者采用独立事务,否则很可能因为日志写入失败导致整个出库操作回滚,业务反而做不成了。实践上我用了一个@Transactional(propagation = Propagation.REQUIRES_NEW)的方法来写日志,保证日志和主业务互不拖累。

要补充的是,这类系统不需要引入消息队列。出入库单量如果到不了每秒几十上百的级别,Redis 锁和数据库事务足够。引入 MQ 只会增加运维成本,第一版千万别过度设计。

5. 真机联调与部署阶段最容易踩的坑

5.1 小程序域名与 HTTPS 要求

微信小程序正式版要求所有接口必须是 HTTPS,而且域名要备案。很多人把后端开发完,在小程序开发者工具里勾了"不校验合法域名",真机预览一切正常,一提交审核就被打回,说请求的域名不在合法列表中。

正确做法是:后端上线后,在微信公众平台的后台配置服务器域名 request 合法域名,然后在小程序开发工具里关闭"不校验合法域名"重新测试。域名必须是公网可访问的,不能用 IP 地址(本地调试除外)。备案周期通常要 1~2 周,这个时间要提前预留,别等系统开发完才开始申请。

5.2 扫码识别与条码库的坑

真机扫码时我发现,用wx.scanCode扫某些 EAN-13 条码,识别结果偶尔会多一个数字或者少一个数字。最初以为是 API 问题,排查后发现是货品条码本身印刷不规范,比如条码下面没有印数字、条码纸卷边。这种情况下,代码要做的是:扫到条码后先去product表精确查,查不到时提示"货物档案不存在",同时把条码原值展示给仓管员,让他在界面上手动修改后再查一次。

还有一类问题是条码前缀。有些供应商会在条码后面追加批次号,扫出来是"6901234567890A01"这种长字符串。我在product表里建了一个match_rule字段,存的是长度截取规则,比如"取前 13 位"。这个需求很小,但如果第一版不处理,现场扫码的成功率会直接影响仓管员对这个系统的信任感。

5.3 数据库时区与连接池配置

部署阶段最容易出的隐性 Bug 是时间差。服务器默认时区是 UTC,MySQL 连接串没指定serverTimezone=Asia/Shanghai,结果小程序端显示的时间和数据库存的时间差了 8 小时。这个查起来非常隐蔽,因为接口返回的数据看起来"正常",只是对不上。

我的连接串配置是:

url: jdbc:mysql://localhost:3306/wms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false

连接池我用的是 HikariCP(SpringBoot 默认),生产环境设置maximum-pool-size: 20就足够这套系统的量级。不要开太多连接,连接池过大反而会因为空闲连接被 MySQL 提前断开而报错。

5.4 小程序端图片上传的路径问题

货物档案一般会上传照片,小程序端用wx.uploadFile上传图片到后端。这里有个坑:wx.uploadFile不支持自定义请求头(虽然新版开始支持部分,但兼容性仍不稳定),所以 token 没法通过 header 传递。我最后的方案是:先用普通请求向后端申请一个上传凭证接口,返回带时效的临时上传 URL,再调用wx.uploadFile直接上传到这个 URL。或者更简单一点:上传接口在框架层面加一个@IgnoreAuth注解,允许匿名访问,靠 URL 里带一个随机 token 参数来做校验。这种方式够用,但要注意上传接口必须做文件类型和大小校验,防止垃圾文件占满磁盘。

6. 交付后我做的三件"加分"改造

6.1 操作日志回放功能

第一版上线跑了一周后,仓库主管提了一个需求:有一票货数量对不上,想知道是哪一天、哪个人、在哪个环节出了问题。虽然日志表里已经存了全量操作记录,但管理后台只能单调地列表展示。我把日志改造成了"回放"视图:选择一个时间段,系统按时间线把每一天的入库、出库、调整操作列出来,每条记录都带操作人、货物、数量和备注。这样一来,仓库主管查账的效率提升非常多,这种基于现有数据的二次加工,成本很低但价值感极强。

6.2 库存上下限预警

当时某个常用耗材因为没人注意库存见底,导致临时补货耽误了两天。我给product表加了low_stock_threshold和high_stock_threshold字段。每次出入库提交后,后端顺手检查库存数量是否低于下限或高于上限,把预警信息写入一张预警表。小程序端库存页面直接显示"库存不足"黄色标签,管理后台也有待处理预警列表。预警本身不需要很复杂,但它是让系统从"记录工具"变成"管理工具"的关键一步。

6.3 批量导入与初始化工具

这套系统上线时,最痛苦的是初始库存数据录入。仓库里现有 300 多种货,每一种都要填名称、规格、条码、期初库存,如果靠手工录入,仓管员会当场崩溃。我做一个 Excel 导入工具,用固定的模板表格,后端用 Easy Excel 解析,批量插入product和stock表。这里有个教训:导入模板一定要先给数据示例,而不是给空表头。第一次我给的是空表头模板,结果对方把"货物名称"那列填成了"货物名称"四个字本身的示例,导致导入后数据里混入脏数据,又花了一个小时清洗。

批量导入的一个实现要点是:逐行解析后先用事务包住整体,或者先做全量校验再做全量插入。我的做法是先读 Excel 到内存 List,逐行校验(条码是否重复、库存是否为负数),全部校验通过后,再开启事务批量插入。这样避免了"导入到一半发现第 150 行有错,前面 149 行已经写进数据库"的尴尬局面。

上线稳定之后,我又陆续加了些小功能,比如出库单打印、库存导出 Excel、首页仓库面积利用率看板。但核心的思路一直没变:先把每一次出入库记录清楚,把库存数值变准确,所有花哨的功能都是在这个地基上长出来的。如果你现在正准备动手做类似的系统,我建议你先抛开那些炫技的设想,去真实的仓库里待一天,看看仓管员是怎么干活、在哪一步最容易出错的。把这一步理顺了,系统的价值就已经成功了一大半。回头再看这套 SpringBoot 加微信小程序的组合,技术层面确实没什么高深的地方,但它解决了真实业务里最要命的问题——数据匹配和流程可控,这就够了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进