简介:这是一套面向高校计算机专业学生的Java期末大作业实战项目,基于SpringBoot+Vue+MySQL实现的完整仓库管理系统,适用于课程设计、毕业设计前期原型开发与全栈技术整合练习。资源包含109个文件,涵盖41个Java后端业务与控制器类、29个Vue组件及页面、10个JS工具脚本、5个XML配置与5个VM模板文件,辅以CSS、JSON、YML等配套资源,整体压缩包仅380KB,轻量易部署。已有6358人学习下载,反映出其在教学实践场景中的广泛认可度。读者可直接导入IDEA与Vue CLI运行,获得前后端分离架构下的完整CRUD功能演示、用户权限管理模块、商品/库存/出入库核心业务逻辑及基础数据可视化界面,代码结构清晰、注释规范,适合作为SpringBoot与Vue协同开发的入门级参考范例。
1. 一个能跑通、能改、能交的仓库管理系统到底长什么样?
很多同学拿到“基于SpringBoot+Vue+MySQL仓库管理系统”这个期末大作业题目时,第一反应是:网上源码一搜一大把,解压、导入、启动——结果报错连环跳:Failed to configure a DataSource、Cannot resolve symbol 'Vue'、npm run serve卡在98% after emitting……不是数据库连不上,就是前端路由白屏,再或者后端接口返回 404。这根本不是“系统”,是拼凑失败的三件套。真正能交、能演示、能讲清楚的仓库管理系统,必须满足三个硬条件:后端接口有完整CRUD逻辑且带基础权限校验(如登录态拦截)、前端页面覆盖入库/出库/库存查询/报表导出等核心业务流、MySQL表结构支持多级分类与操作日志追溯。它不追求高并发或微服务,但必须让老师在5分钟内看到“商品扫码入库→库存实时更新→导出Excel报表”这一闭环。本文就从零还原这套组合如何对齐——不依赖任何“已配置好”的压缩包,只用最主流、文档最全、IDE提示最友好的 SpringBoot 2.7.x + Vue 2.6 + MySQL 8.0 组合,手把手补全那些被压缩包悄悄删掉的关键配置和衔接细节。
2. 后端搭建:用SpringBoot 2.7.x搭出可验证的仓库API骨架
2.1 为什么选SpringBoot 2.7.x而不是3.x?版本兼容性是第一道坎
当前(2024年)高校Java教学环境仍以JDK 8/11为主,而SpringBoot 3.x强制要求JDK 17+,若强行升级会导致MyBatis-Plus、Druid等常用组件报UnsupportedClassVersionError。SpringBoot 2.7.x是最后一个支持JDK 8的长期维护版本,且与Vue 2.x的跨域调试、MySQL 8.0的caching_sha2_password认证机制兼容性最佳。新建项目时,在 start.spring.io 中明确选择:
- Project: Maven
- Language: Java
- Spring Boot:2.7.18(最新2.7.x补丁版)
- Dependencies: Spring Web, Spring Data JDBC, MyBatis Framework, MySQL Driver, Lombok, Validation
提示:不要勾选Spring Security Starter——期末作业中“登录校验”只需用拦截器实现,引入Security会增加XML配置、Filter链、密码编码等冗余复杂度,反而掩盖业务逻辑。
2.2 数据库建模:避开新手最常踩的3个ER设计坑
仓库管理系统的表结构不能简单照搬“商品表+库存表”二表模型。实际业务中,同一商品在不同仓库位置(如A区货架1-1、B区托盘2-3)需独立记录库存量,且每次出入库操作必须留痕。因此必须包含以下5张表:
| 表名 | 关键字段 | 设计要点 |
|---|---|---|
wms_user | id, username, password, role (enum: 'admin','staff') | password字段用char(60)存BCrypt加密后密文,非明文 |
wms_goods | id, code, name, category_id, unit (e.g. '件','箱') | code设为唯一索引,用于扫码快速定位 |
wms_warehouse | id, name, location, capacity | location存物理坐标(如"华东仓-3F-A区"),非经纬度 |
wms_stock | id, goods_id, warehouse_id, quantity, updated_at | 联合唯一索引(goods_id, warehouse_id),防重复插入 |
wms_operation_log | id, type ('in','out'), goods_id, warehouse_id, quantity, operator_id, created_at | type用枚举值,便于SQL聚合统计 |
执行建表SQL前,先确认MySQL 8.0已关闭严格模式(避免Invalid default value for 'created_at'错误):
-- 在MySQL命令行执行 SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));2.3 接口开发:用MyBatis-Plus写清库存增减的原子性逻辑
库存变更(入库/出库)必须保证数据库层面的原子性,不能靠Java代码做“先查再改”——高并发下必然超卖。正确做法是用MyBatis-Plus的update方法配合条件更新,并检查影响行数:
// StockService.java @Transactional public Result updateStock(Long goodsId, Long warehouseId, Integer deltaQuantity) { // deltaQuantity > 0 表示入库,< 0 表示出库 LambdaUpdateWrapper<WmsStock> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(WmsStock::getGoodsId, goodsId) .eq(WmsStock::getWarehouseId, warehouseId); // 出库时校验库存是否充足 if (deltaQuantity < 0) { WmsStock current = stockMapper.selectOne(wrapper); if (current == null || current.getQuantity() + deltaQuantity < 0) { return Result.fail("库存不足,无法出库"); } } // 原子更新:quantity = quantity + deltaQuantity wrapper.setSql("quantity = quantity + " + deltaQuantity); boolean success = stockMapper.update(null, wrapper) > 0; if (!success) { // 记录日志并抛异常触发事务回滚 log.error("库存更新失败:goodsId={}, warehouseId={}, delta={}", goodsId, warehouseId, deltaQuantity); throw new RuntimeException("库存更新失败,请重试"); } // 写入操作日志 WmsOperationLog log = new WmsOperationLog(); log.setType(deltaQuantity > 0 ? "in" : "out"); log.setGoodsId(goodsId); log.setWarehouseId(warehouseId); log.setQuantity(Math.abs(deltaQuantity)); log.setOperatorId(SecurityContext.getUserId()); // 从拦截器获取当前用户ID operationLogMapper.insert(log); return Result.success(); }注意:
setSql("quantity = quantity + ...")是关键。它将计算逻辑下推到数据库,避免Java层读-改-写(Read-Modify-Write)导致的竞态条件。MyBatis-Plus 3.4.3+ 支持此写法,低于该版本需改用@SelectKey或原生XML。
3. 前端集成:用Vue 2.6+axios打通跨域请求与路由守卫
3.1 Vue项目初始化避坑:别用Vue CLI 5.x创建Vue 2项目
Vue CLI 5.x默认创建Vue 3项目,若强行指定--preset vue2会生成过时的vue-template-compiler依赖,导致npm run serve报Cannot find module 'vue-template-compiler'。正确做法是全局安装Vue CLI 4.x:
npm uninstall -g @vue/cli npm install -g @vue/cli@4.5.15 vue create wms-vue --preset vue2创建后进入项目,安装核心依赖:
cd wms-vue npm install axios element-ui moment --save npm install less less-loader --save-dev3.2 跨域代理配置:用vue.config.js解决开发期CORS问题
SpringBoot后端默认运行在http://localhost:8080,Vue前端在http://localhost:8081,浏览器直接请求会触发CORS拦截。不能在后端加@CrossOrigin注解——那只是临时方案,且无法处理Cookie传递。必须在Vue开发服务器层做反向代理:
// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' // 把 /api/users → /users } } } } }配置后,前端所有请求写成axios.get('/api/users'),开发时自动代理到http://localhost:8080/users,构建生产环境时则需Nginx配置真实反向代理。
3.3 路由守卫实现简易权限控制:拦截未登录访问
仓库系统至少需区分管理员与普通员工视图(如管理员可见用户管理页,员工不可见)。Vue Router 3.x的beforeEach守卫可读取localStorage中的token判断登录态:
// router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('wms_token') const requiresAuth = to.matched.some(record => record.meta.requiresAuth) if (requiresAuth && !token) { // 未登录且目标页需要认证,跳转登录页 next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.path === '/login' && token) { // 已登录却访问登录页,跳转首页 next({ path: '/' }) } else { next() } })对应地,后端登录接口返回的token需存入localStorage:
// api/auth.js export function login(username, password) { return axios.post('/api/auth/login', { username, password }) .then(res => { if (res.data.code === 200) { localStorage.setItem('wms_token', res.data.data.token) // 存token localStorage.setItem('wms_user', JSON.stringify(res.data.data.user)) } return res }) }提示:此处token仅为字符串标识(如
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."),无需JWT解析。后端拦截器通过request.getHeader("Authorization")获取并校验即可,降低前端复杂度。
4. 前后端联调:用Postman验证接口 + Chrome DevTools定位白屏
4.1 用Postman逐个验证5个核心接口的请求/响应格式
光看控制台没报错不等于接口可用。必须用Postman手动发送请求,确认返回JSON结构符合前端预期。重点验证以下5个接口(URL均以/api开头):
| 接口路径 | 请求方式 | 典型请求体 | 预期成功响应 |
|---|---|---|---|
/auth/login | POST | { "username": "admin", "password": "123456" } | { "code": 200, "data": { "token": "xxx", "user": { "id": 1, "role": "admin" } } } |
/goods/list | GET | 无 | { "code": 200, "data": [ { "id": 1, "code": "G001", "name": "螺丝刀" } ] } |
/stock/detail?goodsId=1&warehouseId=1 | GET | URL参数 | { "code": 200, "data": { "quantity": 150 } } |
/stock/update | POST | { "goodsId": 1, "warehouseId": 1, "deltaQuantity": 10 } | { "code": 200, "msg": "操作成功" } |
/operation/log?dateFrom=2024-01-01&dateTo=2024-01-31 | GET | URL参数 | { "code": 200, "data": [ { "type": "in", "quantity": 50 } ] } |
注意:若
/auth/login返回401,检查SpringBoot中WebMvcConfigurer是否放行了/api/auth/**路径;若/goods/list返回空数组,确认MySQL中wms_goods表是否有测试数据(用INSERT INTO wms_goods VALUES (1,'G001','螺丝刀',1,'件');插入)。
4.2 Chrome DevTools定位Vue白屏的3个关键面板
当访问http://localhost:8081只显示空白页,按F12打开DevTools,依次检查:
- Console面板:查看是否有
[Vue warn]: Failed to mount component: template or render function not defined.——说明main.js中new Vue()挂载的DOM节点#app不存在,检查public/index.html是否漏写<div id="app"></div>; - Network面板:筛选
XHR,看/api/goods/list等请求是否返回200。若显示Failed to load resource: the server responded with a status of 404 (Not Found),说明Vue代理未生效,检查vue.config.js是否在项目根目录且已重启npm run serve; - Application面板 → Local Storage:确认
wms_token是否存在。若为空,说明登录接口未被调用或调用失败,回到Console看axios请求是否被拦截。
4.3 Element UI表格动态渲染:用v-for绑定库存列表并添加操作列
前端展示库存的核心是el-table组件。其data属性必须是数组,且每项需含id、goodsName、warehouseName、quantity等字段。后端/stock/list接口返回的数据往往为关联表JOIN结果,但字段名可能不匹配Element UI要求,需在Vue组件中做映射:
<!-- views/StockList.vue --> <template> <el-table :data="stockList" style="width: 100%"> <el-table-column prop="goodsCode" label="商品编码" width="120"></el-table-column> <el-table-column prop="goodsName" label="商品名称" width="180"></el-table-column> <el-table-column prop="warehouseName" label="仓库位置" width="180"></el-table-column> <el-table-column prop="quantity" label="当前库存" width="100"></el-table-column> <el-table-column label="操作" width="180"> <template slot-scope="scope"> <el-button size="mini" @click="handleIn(scope.row)">入库</el-button> <el-button size="mini" type="danger" @click="handleOut(scope.row)">出库</el-button> </template> </el-table-column> </el-table> </template> <script> import { getStockList } from '@/api/stock' export default { data() { return { stockList: [] // 初始化为空数组,避免v-for报错 } }, created() { this.loadStockList() }, methods: { async loadStockList() { try { const res = await getStockList() // 后端返回的字段名为goods_code, goods_name, warehouse_name,需转为驼峰 this.stockList = res.data.map(item => ({ ...item, goodsCode: item.goods_code, goodsName: item.goods_name, warehouseName: item.warehouse_name })) } catch (err) { this.$message.error('加载库存列表失败:' + err.message) } }, handleIn(row) { this.$prompt('请输入入库数量', '入库', { inputPattern: /^\d+$/, inputErrorMessage: '请输入正整数' }).then(({ value }) => { // 调用入库API updateStock(row.goods_id, row.warehouse_id, parseInt(value)) }) } } } </script>5. 生产部署与答辩技巧:打包上线+3分钟讲清技术亮点
5.1 后端Jar包部署:用nohup启动并配置日志滚动
SpringBoot项目打包为jar后,不能直接java -jar app.jar——终端关闭进程即终止。需用nohup后台运行,并配置日志按天滚动,避免磁盘占满:
# 打包(在SpringBoot项目根目录执行) mvn clean package -Dmaven.test.skip=true # 启动,日志输出到logs/app.log,按天切割 nohup java -jar target/wms-backend-0.0.1-SNAPSHOT.jar \ --logging.file.name=logs/app.log \ --logging.logback.rollingpolicy.max-history=30 \ --server.port=8080 \ > /dev/null 2>&1 &验证是否启动成功:
# 查看进程 ps -ef | grep wms-backend # 查看最后10行日志 tail -10 logs/app.log # 测试接口(返回{"code":200,"msg":"OK"}即成功) curl http://localhost:8080/api/actuator/health5.2 前端静态资源部署:Nginx配置反向代理与history模式支持
Vue Router使用history模式时,直接访问http://your-domain.com/stock会返回404(Nginx找不到该路径的HTML文件)。需在Nginx配置中添加try_files指令:
# /etc/nginx/conf.d/wms.conf server { listen 80; server_name wms.your-domain.com; location / { root /var/www/wms-frontend; # 前端dist目录绝对路径 index index.html; try_files $uri $uri/ /index.html; # 关键:所有路径都 fallback 到 index.html } # 将/api请求代理到后端 location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }配置后重载Nginx:sudo nginx -s reload,此时访问域名即可看到系统首页。
5.3 答辩话术设计:用“问题-解法-效果”三句话讲透一个技术点
老师最常问:“你这个系统里,哪个地方体现了你的技术能力?” 不要回答“我用了SpringBoot和Vue”,而要聚焦一个具体问题:
“比如库存扣减的准确性问题。如果用先查再改的方式,多人同时出库同一商品,可能出现超卖。我的解法是在MyBatis-Plus中用
setSql('quantity = quantity + ?')执行原子更新,并在SQL层面校验quantity + delta >= 0。效果是,即使100人并发点击‘出库50件’,系统也只会成功执行一次,其余99次返回‘库存不足’提示,完全避免了数据错乱。”
这种表述把技术名词(原子更新)、实现手段(setSql)、业务价值(防超卖)全部串起来,比罗列框架名有力得多。同理,可准备2个类似话术:一个是Vue路由守卫如何拦截未登录访问,另一个是Nginx如何解决history模式404——每个都控制在3句话内,答辩时随机选一个展开。
本文还有配套的精品资源,点击获取