简介基于SpringBoot与Vue构建的仓库货物管理系统源码面向需要快速搭建仓库管理后台的Java开发者和企业信息化学习者。系统以MySQL为数据存储核心围绕货物出入库、库存记录、用户操作审核等典型业务场景设计覆盖从后端接口到前端页面的完整实现链路便于理解前后端分离项目的分层与协作方式。压缩包内共794个文件以Java源码、Vue组件和JavaScript脚本为主同时包含SVG图标、XML配置、SQL脚本、Shell脚本及必要的环境配置整体约4.72MB其中Java源码占比最高便于梳理核心业务逻辑前端由Vue组件与JavaScript脚本共同支撑目录结构清晰便于按模块查阅。资源附带环境部署手册、启动脚本和多环境配置示例可支撑本地环境搭建、功能扩展和二次开发实践。目前已有430人学习适合具备一定Java与Vue基础、希望获取完整可运行工程并上手练习的开发者。1. 仓库货物管理系统不只是把增删改查拼在一起我见过很多仓库管理项目界面漂亮但一到盘点就出问题——入库单录进去了库存却对不上出库时超卖月底对账全靠人工。这套基于SpringBootVue的仓库货物管理系统核心不是“能录入”而是把入库、出库、库存调整做成一条带事务和并发控制的业务链。SpringBoot负责REST接口和事务边界Vue负责操作界面源码可以直接跑起来改。如果你正在做毕业设计、接手中小型仓库的后台或者想学前后端分离项目的完整写法这套代码值得你拆开看。它的价值在于表结构设计得规整业务分层清晰前后端接口约定统一改起来不费力。下面我会按“后端设计 → 前端页面 → 核心业务 → 打包排错”的顺序把实际落地时最容易踩的坑也一并讲清楚。2. SpringBoot后端表结构、MyBatis-Plus与统一返回结构2.1 先设计仓库域的表不要急着写代码仓库货物管理系统的核心数据模型围绕“货物在哪里、动过多少、谁动的”三个问题展开。常见做法是五张表货品表、仓库表、库存表、入库单和出库单。库存表不是简单的数量字段而要带上版本号和最近变动时间为后面的乐观锁做准备。CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, sku_code varchar(64) NOT NULL COMMENT 商品编码, name varchar(128) NOT NULL, spec varchar(64) DEFAULT COMMENT 规格, unit varchar(16) DEFAULT 件, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sku (sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL, warehouse_id bigint(20) NOT NULL, quantity int(11) NOT NULL DEFAULT 0, version int(11) NOT NULL DEFAULT 0, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_product_warehouse (product_id,warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里把库存的联合唯一键放在(product_id, warehouse_id)上避免同一商品在同一仓库出现两条记录。version字段是给乐观锁用的任何库存增减必须先带出这个版本号更新时再比较。表结构定了后面业务才稳。2.2 SpringBoot版本选择别一上来就追3.x很多人在创建SpringBoot项目时习惯选最新版结果引入MyBatis-Plus后报一堆错最典型的是springboot版本太高导致和druid、pagehelper的兼容性问题。我的建议是工作或毕业设计选稳定线比如2.7.x。这个版本同时兼容javax和大部分国产组件网上能搜到的资料也最多。如果是SpringBoot 3.x包名变成了jakarta很多老项目的代码直接编译不过。pom.xml里核心依赖就这么几个dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependencyMyBatis-Plus的版本要和SpringBoot匹配。SpringBoot 2.7对应MyBatis-Plus 3.5.x没问题。如果你的项目是SpringBoot 3.x就得用mybatis-plus-spring-boot3-starter这个细节很多人不知道。选版本时不要只看官方最新要看你的团队和生态组件跟不跟得上。2.3 统一返回结构前后端联调的第一份契约接口返回值如果每个方法自己拼HashMap前端拿到什么全凭运气。这套系统的源码里用一个Result类统一包裹Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }code统一含义200成功500业务失败401未登录403无权限。前端axios拦截器里只需要判断code不用每个接口单独处理错误。这样写的好处是出问题时你能从network里一眼看出是业务错还是框架错。2.4 Controller只做参数接收业务逻辑放Service很多新手把SQL写在Controller里这样也能跑但维护起来就是灾难。源码里的Controller很薄比如库存查询RestController RequestMapping(/api/stock) public class StockController { Autowired private StockService stockService; GetMapping(/list) public ResultIPageStockVO list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword) { return Result.ok(stockService.pageQuery(page, size, keyword)); } }这里IPage是MyBatis-Plus的分页对象参数page是页码size是每页条数keyword是商品名称模糊查询。requiredfalse表示可以不传。分页查询的动态SQL写在了Service里在真正的项目中你只需要实现一个分页条件构造器不要在前端拼接大量查询条件。代码结构清晰后面接报表、接看板都方便。3. Vue前端路由、动态表格与货物出入库表单3.1 创建工程与安装依赖别被版本卡住前端部分用Vue 3和Element Plus。创建工程用Vite命令是npm create vitelatest warehouse-web然后选择vue模板。进入目录后npm install。这里有个常见问题网络慢或者node版本太低时vue安装依赖会报ERR_OSSL_EVP_UNSUPPORTED。处理办法是升级node到18以上或者用set NODE_OPTIONS--openssl-legacy-provider临时绕过但不推荐后者。安装基础依赖npm install vue-router4 axios element-plus element-plus/icons-vuevue-router必须用4.x版本它对应Vue 3。Element Plus的图标要单独安装组件里按需引入。如果你的项目从Vue2迁移过来注意this.$router.push要改成useRouter()这是新手的重灾区。3.2 路由参数处理从列表页跳到详情页仓库管理里最常见的交互是在货物列表点击一行跳到该货物的出入库明细页。这时需要传递货物ID就是vue路由参数的核心用法。路由配置如下import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/Dashboard.vue) }, { path: /product/:id, name: ProductDetail, component: () import(../views/ProductDetail.vue), props: true } ] const router createRouter({ history: createWebHistory(), routes }) export default router在列表页跳转时用router.push({ path: /product/123 })在详情页取参时用useRoute().params.id。props:true能让你直接在组件里用defineProps([id])来接收省去$route的依赖。注意如果参数是中文或特殊字符路由参数要编码推荐用query方式传搜索条件params只传ID这类标识。3.3 Element Plus动态表格列配置和插槽用法货物列表页的表格不要写死每一列而是用一个列配置数组驱动渲染。这样后续加字段、调顺序只改配置不动结构。核心代码如下template el-table :datatableData v-loadingloading el-table-column v-forcol in columns :keycol.prop :propcol.prop :labelcol.label :widthcol.width / /el-table /template script setup import { ref, onMounted } from vue import { fetchProductList } from ../api/product const tableData ref([]) const loading ref(false) const columns ref([ { prop: skuCode, label: 商品编码, width: 120 }, { prop: name, label: 商品名称, width: 180 }, { prop: spec, label: 规格, width: 120 }, { prop: unit, label: 单位, width: 80 }, { prop: stockQty, label: 当前库存, width: 100 } ]) onMounted(async () { loading.value true const res await fetchProductList() tableData.value res.data.data.records loading.value false }) /script这里的loading状态由v-loading指令控制接口请求期间页面会显示遮罩。el-table-column的v-for循环渲染列时需要绑定:key否则Element Plus会警告。表格数据源要配合后端返回的records字段如果后端返回结构变了前端只需改fetchProductList里的返回值路径这是统一返回结构的收益。3.4 表单校验入库单和出库单不能靠手填货物出入库表单用el-form的rules规则来校验。比如出库数量必须大于0且不能超过当前库存。这里有个细节校验逻辑不要写在模板里而是写成函数const rules { quantity: [ { required: true, message: 请输入数量, trigger: blur }, { validator: (rule, value, callback) { if (value 0) { callback(new Error(数量必须大于0)) } else { callback() } }, trigger: blur } ] }自定义validator会在表单提交时触发如果你希望在输入过程中也校验就把trigger改成change。注意Element Plus的表单校验只能在el-form-item的prop对应字段存在于表单model中时生效别漏掉prop。4. 货物出入库核心业务与库存扣减的并发处理4.1 为什么不能先query再update最直观的库存扣减写法是先查库存如果数量够就update set quantityquantity-?。但高并发下两个请求同时读到库存为10都判断可以扣5最后库存变成5而不是0这样就超卖了。仓库系统虽然并发没有电商高但出库单批量导入、多终端同时操作时同样会撞车。源码中解决这个问题用的是乐观锁。在库存表里加version字段更新时带上版本号Update(UPDATE stock SET quantity quantity - #{quantity}, version version 1 WHERE id #{id} AND version #{version}) int deductStock(Param(id) Long id, Param(quantity) Integer quantity, Param(version) Integer version);如果更新返回0说明version对不上也就是别人已经改过这条数据业务就要重试或者报错。MyBatis-Plus提供了Version注解但手写SQL更直观方便你理解原理。实际生产里还可以用UPDATE stock SET quantity quantity - #{quantity} WHERE id #{id} AND quantity #{quantity}利用数据库行锁来防超卖。两个方法选一个就行源码里用了乐观锁代码更“Java”。4.2 事务边界一张入库单牵动三张表入库操作不是简单加库存还要写入库单、更新库存表、记录库存流水。任何一个步骤失败前面已经改的数据都要回滚。Service方法上加Transactional(rollbackFor Exception.class)Transactional(rollbackFor Exception.class) public void inbound(InboundOrderDTO dto) { // 1. 保存入库单主表 InboundOrder order new InboundOrder(); order.setOrderNo(generateOrderNo()); order.setProductId(dto.getProductId()); order.setWarehouseId(dto.getWarehouseId()); order.setQuantity(dto.getQuantity()); order.setOperator(dto.getOperator()); this.save(order); // 2. 更新库存不存在则插入 Stock stock stockMapper.selectByProductAndWarehouse(dto.getProductId(), dto.getWarehouseId()); if (stock null) { stock new Stock(); stock.setProductId(dto.getProductId()); stock.setWarehouseId(dto.getWarehouseId()); stock.setQuantity(dto.getQuantity()); stock.setVersion(0); stockMapper.insert(stock); } else { stockMapper.addQuantity(stock.getId(), dto.getQuantity(), stock.getVersion()); } // 3. 记录流水 StockFlow flow new StockFlow(); flow.setOrderNo(order.getOrderNo()); flow.setType(1); // 1入库 2出库 flow.setQuantity(dto.getQuantity()); this.saveFlow(flow); }注意rollbackFor要设成Exception.class因为Spring默认只回滚RuntimeException如果你抛的是自选异常不加这个就白放在那了。另外事务只对public方法生效在同一个类里调用内部方法不会走代理这也是个经典坑。如果你发现入库后库存变了但事务没回滚先检查是不是被同类方法调用了。4.3 生成单号时间戳加随机数别在并发下重号入库单号如果直接用System.currentTimeMillis()拼接随机数高并发时重号概率不高但不为零。更稳的方式是利用Redis的incr或者请求进来时取数据库当天的最大单号后缀1。源码里用的是简单方案private synchronized String generateOrderNo() { String date LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); int seq (int) (Math.random() * 9000) 1000; return IN date seq; }synchronized只锁了一个方法在单机部署下有效。如果你要集群部署建议改Redis自增。这个方法的random会有千分之几的碰撞可能但配合数据库唯一索引也能挡一批。仓库系统的单号不像电商那么严苛真撞了数据库抛异常事务回滚再生成一次就行。4.4 出库业务库存不足时的友好提示出库和入库的差别在于要校验库存是否足够。源码里直接在事务外先做一次预校验事务内再次校验public void outbound(OutboundOrderDTO dto) { Stock stock stockMapper.selectByProductAndWarehouse(dto.getProductId(), dto.getWarehouseId()); if (stock null || stock.getQuantity() dto.getQuantity()) { throw new BusinessException(库存不足); } doOutbound(dto, stock); }预校验是为了给用户及时的反馈事务内的二次校验才是防并发。BusinessException是自定义异常会被全局异常处理器捕获并转成Result.error(库存不足)。前端拿到这个message直接在页面上用ElMessage弹出提示干净利落。5. 打包部署、SpringBoot版本与Vue布局异常排查5.1 前端打包后塞进后端还是用Nginx如果你的仓库系统是给几十个人内部用的最简单的方式是把Vue构建后的dist目录直接copy到SpringBoot的static目录下。让后端同时提供接口和页面部署时只要启动一个jar包。具体操作在Vue项目下执行npm run build然后把dist下的文件复制到src/main/resources/static/重新打包SpringBoot项目。注意Vue的路由要用createWebHashHistory否则直接访问子路由会404。也可以用Nginx反向代理前端一个服务后端一个服务。Nginx配置里把/api开头的请求转发到SpringBoot的8080端口其他请求走Vue的静态文件。我一般推荐用Nginx因为调试时不用重新编译后端改前端只替换dist目录就好。5.2 Vue打包后布局异常八成是publicPath问题vue打包后布局异常这个情况很常见表现为页面样式丢失、图片路径404。原因是默认构建时资源引用的是绝对路径/assets/xxx.js如果你把文件放在服务器子目录比如/warehouse/下就会找不到。解决办法是修改Vite的vite.config.jsexport default defineConfig({ base: ./ })base设成相对路径后所有资源引用都会变成相对路径在任意子目录下都能加载。如果你用的是Vue CLI对应的是publicPath: ./。改完之后重新build样式就正常了。5.3 版本不对导致的低级错误检查清单最后给你一个排错顺序表遇到问题先按这个查能省很多时间现象检查点解决办法SpringBoot启动失败pom里的依赖版本把2.7.x系列换成3.x需同步升级MyBatis-Plus前端控制台报跨域后端是否配置CORS写一个WebMvcConfigurer配置addCorsMappings列表页数据不显示后端是否返回Result结构快照网检查code和data字段名表单提交后无反应Vue实例的model绑定确保v-model和el-form-item的prop一致其中跨域问题在前后端分离时几乎一定会遇到我在后端写一个配置类就能解决Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns用通配符而不是allowedOrigins(*)是因为allowCredentials(true)要求来源不能是裸的星号这也是SpringBoot的高版本安全限制。实际部署时建议把allowedOriginPatterns改成你前端的域名别什么都放行。到这里你已经能把这套源码完整跑起来也基本掌握仓库系统最核心的进出库链路。如果再遇到springboot版本太高或vue打包后布局异常照着上面两点改就不会卡住太久。本文还有配套的精品资源点击获取