简介:基于SpringBoot与Vue构建的仓库货物管理系统源码,面向需要快速搭建仓库管理后台的Java开发者和企业信息化学习者。系统以MySQL为数据存储核心,围绕货物出入库、库存记录、用户操作审核等典型业务场景设计,覆盖从后端接口到前端页面的完整实现链路,便于理解前后端分离项目的分层与协作方式。压缩包内共794个文件,以Java源码、Vue组件和JavaScript脚本为主,同时包含SVG图标、XML配置、SQL脚本、Shell脚本及必要的环境配置,整体约4.72MB;其中Java源码占比最高,便于梳理核心业务逻辑,前端由Vue组件与JavaScript脚本共同支撑,目录结构清晰,便于按模块查阅。资源附带环境部署手册、启动脚本和多环境配置示例,可支撑本地环境搭建、功能扩展和二次开发实践。目前已有430人学习,适合具备一定Java与Vue基础、希望获取完整可运行工程并上手练习的开发者。
1. 仓库货物管理系统:不只是把增删改查拼在一起
我见过很多仓库管理项目,界面漂亮,但一到盘点就出问题——入库单录进去了,库存却对不上;出库时超卖;月底对账全靠人工。这套基于SpringBoot+Vue的仓库货物管理系统,核心不是“能录入”,而是把入库、出库、库存调整做成一条带事务和并发控制的业务链。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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里把库存的联合唯一键放在(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> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>MyBatis-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 Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(String message) { Result<T> 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 Result<IPage<StockVO>> 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是商品名称模糊查询。required=false表示可以不传。分页查询的动态SQL写在了Service里,在真正的项目中你只需要实现一个分页条件构造器,不要在前端拼接大量查询条件。代码结构清晰,后面接报表、接看板都方便。
3. Vue前端:路由、动态表格与货物出入库表单
3.1 创建工程与安装依赖,别被版本卡住
前端部分用Vue 3和Element Plus。创建工程用Vite,命令是npm create vite@latest warehouse-web,然后选择vue模板。进入目录后npm install。这里有个常见问题:网络慢或者node版本太低时,vue安装依赖会报ERR_OSSL_EVP_UNSUPPORTED。处理办法是升级node到18以上,或者用set NODE_OPTIONS=--openssl-legacy-provider临时绕过,但不推荐后者。
安装基础依赖:
npm install vue-router@4 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 :data="tableData" v-loading="loading"> <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :width="col.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 quantity=quantity-?。但高并发下两个请求同时读到库存为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.js:
export 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打包后布局异常,照着上面两点改就不会卡住太久。
本文还有配套的精品资源,点击获取