若依+Vue+Spring Boot实战:从零搭建轻量级仓库管理系统WMS
发布时间:2026/9/19 15:47:50 锦皓数字建站

先说一段我自己折腾过的经历。去年有朋友公司仓库还是纯Excel记账账面库存和实物永远对不上每次月盘都要翻两三天旧账。他们想上一套正经的WMS问了一圈商业软件报价起步就是大几万而且流程锁得很死想改个字段加个自定义状态都要单独收费。后来我用若依框架直接给他们搭了一套轻量级仓库管理系统技术栈就是标题里说的Vue加Spring Boot从建表到上线前后三周入库、出库、盘点、库存流水、多仓权限全部覆盖成本几乎只花了台云服务器钱。这篇文章就把这套实战思路完整拆开讲一遍适合有Java和Vue基础、想在若依基础上快速落地仓储业务的开发者也适合手里有若依项目、正打算往WMS方向扩展的团队参考。我不会只贴代码重点会放在三个地方为什么这么设计表结构、为什么入库出库要这么写事务、以及把若依的权限体系用到WMS多仓场景时有哪些坑。这些才是搭一套能真正跑起来的仓库系统时最花时间的地方。1. 选型复盘WMS的核心价值不在框架而在仓储业务逻辑1.1 若依到底帮你省了什么很多搞开发的朋友一听说用若依搭WMS第一反应是若依不就是个后台管理脚手架吗跟WMS有什么关系这个理解没错但恰恰说明了为什么选它是对的。WMS系统本质上是一个重度依赖权限、组织架构、操作日志、字典管理的企业级应用。你随便去翻任何一套商业WMS用户管理、角色权限、菜单导航、操作审计这些模块一个都少不了。如果用Spring Boot加Vue从零写光把这些通用能力做到能上线见人的程度至少得投入两到三周。若依把这一切都做好了用户、角色、菜单、部门、岗位、字典、参数配置、通知公告、文件上传、操作日志、登录认证开箱即用。我当时给朋友搭系统时若依本身就带了一套完整的用户体系和权限拦截我只需要专注写仓库业务模块剩下的大量通用代码一行都不用碰。这就是为什么我说选若依搭WMS是把力气花在刀刃上。1.2 为什么不直接用开源WMS或买商业产品有人会问GitHub上也有一些开源WMS为什么不直接用我调研过几个坦白讲多数开源WMS在技术栈上偏老有的还是jsp那套二次开发体验很差功能表面上看着全但真到了库位级库存管理波次拣货多仓权限隔离这些场景要么写死要么半成品改起来比从零写还痛苦。商业WMS确实功能成熟但除了前面说的价格问题还有一个很现实的点没有哪家商业WMS能100%贴合你公司的流程。不同行业的仓库流程差异极大电商要波次拣货制造业要按工单领料医药行业要批次追溯食品行业要效期管理。商业软件给你的是标准化流程你要么改变自己公司流程去迁就软件要么花大价钱做定制开发。而用若依搭WMS本质上是自己掌握了核心代码流程想怎么调就怎么调。1.3 什么情况下不建议用若依搭WMS当然不是所有场景都适合。如果你的仓库日订单量上万、需要对接大量自动化设备AGV、电子标签、自动分拣线或者业务体量大到必须上微服务架构持续高并发那还是老老实实选成熟的商业WMS或者专业团队定制开发。若依单体应用撑中小型仓库绰绰有余但硬要扛超大流量就是拿自行车上高速。选型这事适合才是最好的。另外提一句市面上也有几款同类开源后台框架有的文档收费有的社区活跃度高一些。选哪个不是关键关键是团队里有没有人真正熟这一套代码。框架本身的价值远不如团队对它的熟悉程度重要。2. 数据地基WMS的库存模型与表结构设计2.1 仓储管理的核心数据对象仓库、库区、库位、物料仓库系统第一个要建好的根基就是基础资料表。这些表决定了后面所有单据的操作粒度。仓库表wms_warehouse记录仓库基本信息多仓部署时就是权限隔离的顶层维度。库区表wms_storage_area仓库下的物理分区比如A区B区或收货区发货区退货区。库位表wms_storage_location库存存放的最小物理单位例如 A-01-03这种编码。物料表wms_material管的是什么货包括物料编码、名称、规格、单位、默认库位等。这里最关键的是理解库位的意义。很多仓库管理系统做着做着就退化成了进销存只记录总数不管货放在哪。但真正到了盘点、拣货、先进先出的时候没有库位信息根本玩不转。所以从一开始就要把仓库-库区-库位-物料这条层级链路建好。2.2 单据与库存流水账实相符的底层保障如果说基础资料是骨架那单据和库存流水就是血管。WMS里最重要的两张业务单据是入库单和出库单。入库单记录货从哪来、谁送的、送到哪个库位出库单记录货发到哪去、谁领的、从哪个库位拣的。但光有单据还不够还必须有库存流水表wms_inventory_log。库存流水的作用一句话就能说清每一次库存变化都留痕。不管是入库上架、出库扣减、盘点调整还是移库搬运都要往流水表里插一条记录记录哪个物料、哪个库位、变化前数量、变化后数量、变化类型、关联单据号、操作人、操作时间。这样一旦账面和实物对不上顺着流水就能把整个链路拉出来查。这是WMS跟普通Excel台账最本质的区别可追溯。2.3 建表SQL与技术要点下面是精简版的核心建表脚本实际项目中建议在此基础上增加审计字段create_by、create_time、update_by、update_time等-- 仓库表 create table wms_warehouse ( warehouse_id bigint auto_increment primary key comment 仓库ID, warehouse_code varchar(32) not null comment 仓库编码, warehouse_name varchar(64) not null comment 仓库名称, address varchar(255) default null comment 仓库地址, status char(1) default 0 comment 状态0正常 1停用, remark varchar(500) default null comment 备注 ) engineinnodb comment仓库表; -- 库位表 create table wms_storage_location ( location_id bigint auto_increment primary key comment 库位ID, warehouse_id bigint not null comment 所属仓库ID, location_code varchar(32) not null comment 库位编码, area_type varchar(32) default null comment 库区类型收货区/存储区/发货区, status char(1) default 0 comment 状态0正常 1停用, remark varchar(500) default null comment 备注 ) engineinnodb comment库位表; -- 物料表 create table wms_material ( material_id bigint auto_increment primary key comment 物料ID, material_code varchar(32) not null comment 物料编码, material_name varchar(128) not null comment 物料名称, spec varchar(64) default null comment 规格型号, unit varchar(16) default null comment 计量单位, default_location_id bigint default null comment 默认库位ID, status char(1) default 0 comment 状态0正常 1停用, remark varchar(500) default null comment 备注 ) engineinnodb comment物料表; -- 库存表 create table wms_inventory ( inventory_id bigint auto_increment primary key comment 库存ID, warehouse_id bigint not null comment 仓库ID, location_id bigint not null comment 库位ID, material_id bigint not null comment 物料ID, quantity decimal(18,3) not null default 0 comment 当前数量, frozen_quantity decimal(18,3) not null default 0 comment 冻结数量, version int not null default 0 comment 乐观锁版本号, unique key uk_inv_loc_mat (location_id, material_id) ) engineinnodb comment库存表; -- 库存流水表 create table wms_inventory_log ( log_id bigint auto_increment primary key comment 流水ID, warehouse_id bigint not null comment 仓库ID, location_id bigint not null comment 库位ID, material_id bigint not null comment 物料ID, change_type varchar(32) not null comment 变动类型inbound/outbound/check/adjust/move, before_qty decimal(18,3) not null comment 变动前数量, after_qty decimal(18,3) not null comment 变动后数量, change_qty decimal(18,3) not null comment 变动数量, ref_bill_no varchar(64) default null comment 关联单号, create_by varchar(64) default null comment 操作人, create_time datetime default null comment 操作时间, remark varchar(500) default null comment 备注 ) engineinnodb comment库存流水表;建表时有几个细节必须注意。第一库存表一定要加唯一约束防止同一库位同一物料出现两条库存记录这是数据一致性的底线。第二数量字段不要用int要用decimal很多物料存在小数单位的情况。第三预留冻结数量字段不少业务场景需要锁定库存比如订单已承诺但未出库后期再加这个字段要改一堆SQL不如一开始就设计进去。3. 从表到页面用若依代码生成器把基础资料模块半小时跑通3.1 代码生成器的使用前提表结构规整若依自带的代码生成器是搭这类系统的加速器。它的工作逻辑很简单数据库里建好表导入表结构配置生成规则直接生成后端Controller、Service、Mapper和前端Vue页面。但生成器能不能顺利跑通前提是表结构本身规整。我总结了几条硬性要求表必须有主键最好是自增ID生成器会用它做编辑和删除的定位依据。字段必须有注释生成器会直接拿注释生成前端表格列名和表单label。所有字段建议设置默认值尽量允许为空否则生成的新增表单会频繁报xxx不能为空。尽量用统一的字段风格比如所有时间字段叫create_time、update_time所有状态字段叫status。如果建表时不注意这些导入后就是无穷无尽的调整。黑马若依导入表失败这类问题绝大多数都是表里没有主键或者字段类型生成器不认识导致的。3.2 导入表结构的实际操作操作路径不复杂在若依管理后台进入系统工具-代码生成点击导入按钮选择要导入的表确认后就能看到表字段预览。导入后需要调整几个关键配置项生成模板选单表即可基础资料表不需要树表或主子表模型。基本信息确认类名、模块名、业务名。业务名会出现在请求路径里比如填了wms生成的接口就是/wms/material/list。字段信息这是最需要花时间的地方。每个字段都要检查是列表显示、查询条件、插入表单还是编辑表单。比如物料表里物料编码适合做模糊查询状态适合做下拉查询而备注只做列表显示不需要查询。字典类型状态字段关联若依的系统状态字典前端会自动渲染成下拉框不用写任何额外代码。配置完之后有两种方式拿代码一种是在生成器页面直接下载zip包手动解压放到项目对应目录另一种是配好生成路径后直接生成到项目里然后重启后端就能看到接口刷新菜单后就能看到页面。我个人更推荐下载代码手动放这样能看到每个文件被放在哪里出了问题也好排查。3.3 生成后的必要调整代码生成器能节省70%的基础CRUD工作但剩下30%的调整才真正决定这个模块好不好用。以物料管理页面举例生成之后我基本都要做这几件事查询条件调整物料编码、物料名称、状态这三个条件足够把多余的筛选条件删掉避免页面太臃肿。数据校验补充生成器会给必填字段自动加required规则但业务级的校验得自己写。比如物料编码一旦创建就不允许修改这种逻辑要在后端更新接口里加判断。联动行为物料表里默认库位字段更好的体验是弹窗选择库位而不是手填ID这就需要在Vue页面里改造成关联选择组件。代码生成器生成的页面是基于若依封装的通用CRUD组件改造起来并不难但前提是你得理解若依前端的增删改查封装逻辑list方法拉数据、add方法弹表单、update方法回填、del方法二次确认删除。搞懂这条链路改任何生成页面都不慌。4. 入库出库不算完库存流水与并发扣减的Spring Boot实现4.1 入库上架与入库审核基础资料跑通后真正进入业务核心入库和出库。这里我再强调一次入库单不是重点库存变动才是重点。入库流程按我们当时的设计分了三步仓库文员创建入库单填写来源、关联采购单号、物料明细。收货员实际点货后提交上架给明细里的每个物料指定库位和实际数量。主管审核通过系统真正增加库存并写流水。为什么要把创建和审核分开因为实际业务里单据创建时的信息和实物到达时的信息经常不一致。可能采购订了100件实际到货只有98件或者破损了2件。如果不分开账永远对不上。这一步设计到位后面盘点会省很多事。核心入库逻辑用Spring Boot实现大致如下Transactional(rollbackFor Exception.class) public void confirmInbound(InboundConfirmDTO dto) { // 1. 更新入库单状态 WmsInboundOrder order inboundOrderMapper.selectById(dto.getOrderId()); if (order null || !CREATED.equals(order.getStatus())) { throw new ServiceException(入库单不存在或状态不允许确认); } // 2. 遍历入库明细 for (InboundItemDTO item : dto.getItems()) { // 2.1 检查库位和物料是否存在 WmsInventory inventory inventoryMapper.selectByLocationAndMaterial( item.getLocationId(), item.getMaterialId()); if (inventory null) { // 首次入库创建新库存记录 inventory new WmsInventory(); inventory.setWarehouseId(item.getWarehouseId()); inventory.setLocationId(item.getLocationId()); inventory.setMaterialId(item.getMaterialId()); inventory.setQuantity(item.getQuantity()); inventoryMapper.insert(inventory); } else { // 2.2 已有库存用原子更新累加数量 inventoryMapper.increaseQuantity( item.getLocationId(), item.getMaterialId(), item.getQuantity()); } // 3. 写库存流水 InventoryLog log new InventoryLog(); log.setWarehouseId(item.getWarehouseId()); log.setLocationId(item.getLocationId()); log.setMaterialId(item.getMaterialId()); log.setChangeType(INBOUND); log.setBeforeQty(inventory.getQuantity()); log.setChangeQty(item.getQuantity()); log.setAfterQty(inventory.getQuantity().add(item.getQuantity())); log.setRefBillNo(order.getOrderNo()); inventoryLogMapper.insert(log); } // 4. 更新单据状态 order.setStatus(CONFIRMED); inboundOrderMapper.updateById(order); }这个方法加了Transactional所有库存变更和流水写入在同一个事务里要么全部成功要么全部回滚。这是WMS的底线不能出现库存增加了但流水没写或者单据状态变了但库存没动的情况。4.2 出库扣减库存时的并发安全入库相对简单因为数量是加出库就麻烦了因为数量是减而且减操作在高并发下容易出事。最典型的并发问题同一件商品库存剩100件两个出库单同时提交各扣80件。如果程序先查库存、判断够不够、再更新库存两个请求同时查到100件都判断够,然后各自扣成20件最终库存是20件而不是-60件这在业务上不合法但程序没有报错库存就这样悄悄错了。解决这个问题的核心是把判断扣减变成原子操作。生产环境我推荐直接用数据库条件更新一步完成判断和扣减update wms_inventory set quantity quantity - #{quantity}, version version 1 where location_id #{locationId} and material_id #{materialId} and quantity #{quantity}对应的Mapper方法返回影响行数如果返回0说明库存不足或记录不存在程序直接抛异常回滚。这样写比先查再更安全得多完全不用加分布式锁性能也好。Transactional(rollbackFor Exception.class) public void confirmOutbound(OutboundConfirmDTO dto) { for (OutboundItemDTO item : dto.getItems()) { int rows inventoryMapper.deductStock( item.getLocationId(), item.getMaterialId(), item.getQuantity()); if (rows 0) { throw new ServiceException(物料 item.getMaterialCode() 库存不足或库位不存在); } } // 更新出库单状态、写流水等 }这里有一个隐含细节扣减成功后再查一次最新库存用于写流水。因为条件更新不返回更新后的值流水表需要记录变动前后数量所以扣减后立刻select一次。流水里的beforeQty理论上应该是扣减前的值但在并发情况下select到的可能是扣减后的这时候不必太纠结——流水的核心价值是留痕和追溯用本次扣减数量操作后快照来记录已经能满足审计需求。如果业务要求极其严格可以在库存表上加一个变更前数量字段由数据库事务保证一致性。另外如果系统里引入了Redis缓存千万要小心先更新数据库还是先更新缓存的问题。我的习惯是数据库永远为准缓存只做读取加速。库存扣减先落库然后删除缓存key下次读取时拉数据库重新填充。绝不先更新缓存再落库否则一旦数据库失败缓存里就是错误数据。4.3 库存流水一切差异追溯的依据前面SQL里建了wms_inventory_log流水表单独说下这里的重要性。WMS上线三个月后账实不符几乎是一定会出现的事。原因五花八门有人入库时数量录错、出库时漏扫了一件、盘点时小数位四舍五入、甚至某个操作员私下调了库存。如果没有流水表账面和实物对不上时根本无处下手只能全仓盘点找差异。有流水表之后一切就简单了按物料和时间范围拉出所有流水找哪个环节的数量变化和实物对不上。我们实际排查过几起差异最后都是通过流水定位到某一天某个操作员的录入行为沟通成本骤降。所以哪怕业务逻辑再简单任何改变库存的入口都必须同时写流水。这应该是一条铁律写进代码评审规范里。入库写、出库写、盘点调整写、库位移动写一个都不能漏。同理修改库存的代码只要没写流水这个功能就不允许上线。5. 多仓权限互不可见把若依数据权限用到WMS业务里5.1 若依数据权限机制回顾若依的权限体系分两个层面菜单权限控制用户能不能看到某个功能入口数据权限控制用户能操作哪些数据行。菜单权限好理解比如入库单审核这个按钮只给主管角色普通操作员看不到。数据权限才是WMS多仓场景的关键。若依提供了一套基于部门的数据权限方案按数据范围分为全部数据、自定义数据、本部门数据、本部门及以下数据、仅本人数据等几种模式。实现的核心是DataScope注解和SQL里自动拼接的数据范围过滤条件。5.2 让仓管员只看得到自己仓库的数据WMS的典型场景是一个集团下有上海仓、广州仓、成都仓每个仓的仓管员只能看到自己仓库的单据和库存总部可以看全部。这个需求和若依的数据权限模型天然契合。实现方式很直接给仓库表加一个归属部门字段将仓库关联到若依的部门表。上海仓关联上海仓库部这个部门广州仓关联广州仓库部。然后业务Mapper上加上DataScope注解系统就会自动根据当前用户的数据范围拼接过滤条件把不属于该用户的数据过滤掉。Mapper public interface WmsInventoryMapper { DataScope(deptAlias w) ListWmsInventory selectInventoryList(Param(wmsInventory) WmsInventory wmsInventory); }SQL里要注意数据权限是靠部门ID拼接条件的所以查询库存表时必须关联出仓库的部门信息select idselectInventoryList resultTypeWmsInventory select i.* from wms_inventory i left join wms_warehouse w on i.warehouse_id w.warehouse_id where if testwmsInventory.materialId ! null and i.material_id #{wmsInventory.materialId} /if /where /select配置好之后用户登录系统只能看到自己有权限的仓库数据这就是多仓数据隔离。相比在每个查询里手动加warehouse_id 当前用户仓库的条件这套方案把所有过滤逻辑统一收敛到若依的注解机制里新增模块时不容易漏数据安全更有保障。不过有个坑必须提醒自定义数据范围需要关联中间表若依里每个用户、角色、部门都可以配置自定义数据范围维护成本随着数据量上升。我建议直接用本部门数据模式仓管员所在部门对应的仓库就是他有权限的数据范围简单直接不会出现配置混乱。5.3 按钮权限审核与操作分离除了数据权限WMS里业务上还必须做按钮级权限控制。入库单的创建和审核不能是同一个人操作否则就是自己审核自己录的单子风险很大。出库单的驳回和强制入库这种高风险操作必须单独授权给主管。若依的按钮权限实现很成熟后端用PreAuthorize(ss.hasPermi(wms:inbound:audit))控制接口权限前端Vue用v-hasPermi指令控制按钮显隐。菜单管理里针对每个按钮单独配置权限标识然后分配给对应角色就行。我见过不少团队用若依做系统但因为嫌麻烦一个页面只配新增、修改、删除三个按钮权限审核功能直接复用修改按钮。这在WMS里是致命的因为仓储业务的容错率非常低一个误操作可能导致整个库存数据混乱。审核、驳回、反审核这些操作一定要独立成按钮并且单独授权。6. 打包部署与线上优化Nginx、索引、常见报错一次说清6.1 前端构建与后端打包的具体步骤项目开发完成后就要部署上线若依项目的部署其实并不复杂但有几个步骤先后顺序搞错了排查起来很头疼。后端打包mvn clean package -Dmaven.test.skiptrue打包完成后在ruoyi-admin/target/下拿到jar包直接扔服务器上启动就行nohup java -jar ruoyi-admin.jar --spring.profiles.activeprod /log/ruoyi.log 21 前端构建稍微需要注意一下环境配置。先检查vue.config.js确认开发环境的代理指向正确生产环境构建前记得执行npm install把依赖装全然后构建npm run build:prod构建产物在dist/目录这个目录里的静态文件交给Nginx托管。上线前在本地先跑一遍npm run build:prod确认无报错能省去服务器上反复试错的时间。6.2 Nginx配置与413问题WMS系统经常涉及Excel导入导出、图片上传Nginx默认的client_max_body_size是1m一旦上传超过1M的附件就会报413 Request Entity Too Large。这个问题在搜索引擎上非常高频Spring Boot项目部署后上传文件报413绝大多数情况下不是后端代码的问题而是Nginx这一层就拦住了。解决方式很简单在Nginx的server块里加一行配置server { listen 80; server_name your-domain.com; client_max_body_size 50m; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /prod-api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里把接口请求通过/prod-api前缀代理到后端8080端口是若依前后端分离部署的标准姿势。同时Spring Boot自身的文件上传大小也别忘了配置spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB三层配置Nginx、Spring Boot、若依上传工具类都放行上传功能才算真正通畅。6.3 线上性能优化的几个方向WMS上线初期数据量不大性能问题不明显但跑到半年之后库存流水表可能已经有几十万甚至上百万条数据这时候查询变慢是必然的。有几个优化点值得提前做索引优化。流水表一定要按物料ID变动时间建联合索引按关联单号建普通索引。库存表按库位物料建唯一索引前面建表SQL里已经有了。分页查询。若依自带的PageHelper分页默认只适合中小数据量如果单表数据过了百万分页深翻页比如第500页会明显变慢。优化方向是后端加时间范围条件前端限制最大查询范围从根本上掐掉深翻页的查询。缓存热点数据。物料名称、库位编码、仓库名称这类基础资料在列表页会被反复关联查询。用Redis把这些字典类数据缓存起来能省下大量SQL查询。若依本身就整合了Redis直接用RedisCache工具类即可。注意缓存更新时机基础资料变更时主动删缓存不要等它自然过期。慢SQL日志。若依内置的Druid连接池支持慢SQL统计在application.yml里设置慢查询阈值比如超过1秒就打印日志。上线后定期翻慢SQL日志优化频率最高的几条比盲目加缓存有效得多。6.4 二次开发常见报错记录最后说几个若依二开过程中特别高频的报错都是我实际踩过或者帮人排查过的若依vue3版本TypeScript报错。若依官方在不断迭代Vue3版本的代码很多是基于TypeScript写的但模板里大量依赖运行时类型推断。最常见的报错是TS7053或TS2304原因是某个变量被隐式声明为any但strict模式下不允许。快速处理方式是在tsconfig.json里把strict改为false但要长期维护还是建议把类型声明补完整。代码生成器导入表失败。前面提过绝大多数原因是表没有主键少数情况是表名用了数据库关键字比如order或者字段里有不支持的字符集。建表时加上主键、字段加注释、避免关键字命名基本不会出问题。启动Spring Boot项目不显示端口号。这个是若依社区里的老问题原因通常是日志配置没加载或者端口被占用。先用lsof -i:8080查端口是否被占用再确认logback.xml路径是否正确90%的不显示端口号都是这两个原因。文件下载路径404。若依默认上传路径是/profile/upload/Nginx需要额外配置静态资源映射否则浏览器访问下载链接直接404。在Nginx的server里加上location /profile { alias /home/ruoyi/uploadPath/; }这个问题在本地IDE里跑通常不暴露一上生产环境就冒出来是部署阶段最容易被忽略的一环。回到文章开头那个实际项目。系统上线后第三周我第一次陪朋友仓库做月度盘点发现差异比想象中小很多而且每一笔差异都能通过流水表追溯到具体单据和操作人。那一刻我才觉得这套系统真正立住了它不只是把Excel换成了网页而是把仓库管理从靠人记变成了靠系统管。如果你正打算用若依搭WMS建议先把业务规则想清楚再动代码特别是审核流和库存变动的边界这些想透了写代码只是时间问题。至于更偏门的需求比如对接PDA手持终端、打印面单、对接ERP做库存同步那就是下一阶段的事了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。