先说一段我自己折腾过的经历。去年有朋友公司仓库还是纯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 '备注' ) engine=innodb 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 '备注' ) engine=innodb 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 '备注' ) engine=innodb 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) ) engine=innodb 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 '备注' ) engine=innodb 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") List<WmsInventory> selectInventoryList(@Param("wmsInventory") WmsInventory wmsInventory); }SQL里要注意:数据权限是靠部门ID拼接条件的,所以查询库存表时必须关联出仓库的部门信息:
<select id="selectInventoryList" resultType="WmsInventory"> select i.* from wms_inventory i left join wms_warehouse w on i.warehouse_id = w.warehouse_id <where> <if test="wmsInventory.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.skip=true打包完成后在ruoyi-admin/target/下拿到jar包,直接扔服务器上启动就行:
nohup java -jar ruoyi-admin.jar --spring.profiles.active=prod > /log/ruoyi.log 2>&1 &前端构建稍微需要注意一下环境配置。先检查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做库存同步,那就是下一阶段的事了。