☰
SpringBoot进销存系统毕设实战指南
2026/10/7 6:00:38 网站建设 项目流程

简介:这是一套面向计算机专业本科生的毕业设计级Spring Boot进销存管理系统源码,适用于Java Web开发初学者巩固SSM/Spring Boot技术栈,解决企业物资流与资金流一体化管理的实际问题。资源包含672个文件,以103个Java业务类、98个HTML页面、84个JS交互脚本、24个XML配置及1个SQL建库脚本为核心,辅以GIF操作示意图、CSS样式与Shiro权限控制相关class文件,完整覆盖采购、入库、销售、退货、员工培训等核心模块,压缩包仅4.07MB,轻量易部署。已有2536人学习下载,提供可直接运行的前后端分离结构,含User、Goods、Inport、Outport、Salesback等关键实体与控制器类,以及ShiroAutoConfiguration安全配置,便于理解权限拦截、事务管理与库存状态流转逻辑,是掌握企业级CRUD+业务流程开发的典型实践案例。

1. 为什么毕业设计选「SpringBoot进销存」不是凑数,而是稳扎稳打的工程练兵场

你手头这个毕业设计基于springboot的进销存管理系统项目源码+数据库.zip,表面看是套可运行的 Java Web 系统,实则是一块被反复锤炼过的「全栈工程试金石」:它不追求炫技,但覆盖了 SpringBoot 项目从零启动到生产就绪的全部关键链路——数据库建模与事务控制、RESTful 接口分层设计、MyBatis 动态 SQL 与关联查询、前端 Vue/Thymeleaf 渲染逻辑、库存扣减的并发安全、单据状态机流转、Excel 导入导出、日志追踪与异常统一处理。这不是玩具 Demo,而是企业级进销存系统最精简却最真实的骨架。适合计算机/信管专业本科生做毕设:代码量适中(3–5 万行)、技术栈主流(SpringBoot 2.7.x + MySQL 8.0 + MyBatis-Plus)、模块边界清晰(采购/销售/库存/基础资料/报表)、部署成本极低(jar 包直启 + 内置 HikariCP)。我带过 12 届毕设,90% 的翻车都发生在「以为能跑通就等于能讲清」——而这个项目,恰恰逼你把每一行@Transactional的作用、每一条LEFT JOIN的驱动表选择、每一个@Valid注解背后的校验上下文,都掰开揉碎讲明白。它不教你“怎么写代码”,而是教你怎么让代码在真实业务约束下不崩。


2. 从 ZIP 解压到本地可运行:三步验证核心链路是否完整

提示:不要直接mvn clean install!先确认 ZIP 包结构再动手,避免因目录错位导致编译失败。

2.1 拆包后第一眼必须核对的 4 个文件夹

打开 ZIP 后,你会看到类似这样的结构(不同作者命名略有差异,但核心不可少):

springboot-inventory-system/ ├── src/ # Java 源码主干(含 main/java + main/resources) ├── database/ # 必含:建库脚本(init.sql)、初始化数据(data.sql)、可能含备份(backup.sql) ├── docs/ # 可选:ER 图(png/pdf)、接口文档(swagger.json 或 markdown) ├── target/ # ❌ 不应存在!若 ZIP 里自带 target,说明作者打包不规范,需手动删掉再编译 └── pom.xml # 核心:检查 spring-boot-starter-parent 版本(推荐 2.7.18 或 3.0.15,避开 3.1+ 的 Jakarta EE 9 兼容坑)

关键动作:用文本编辑器打开pom.xml,定位<parent>节点,确认版本号。SpringBoot 2.7.x 是当前高校毕设最稳妥的选择——兼容 JDK 8/11,MyBatis-Plus 3.5.x 无缝对接,且社区文档最全;若看到3.1.0或更高,立刻停手,因为spring-boot-starter-web默认启用 Jakarta EE 9 命名空间(jakarta.servlet.*),而多数老版 IDEA 插件和 Tomcat 9 会报ClassNotFoundException,血泪经验:改版本比修依赖简单十倍。

2.2 数据库初始化:用init.sql创建库 + 用户,而非直接source

很多同学直接把database/init.sql丢进 MySQL 客户端执行,结果卡在CREATE DATABASE IF NOT EXISTS inventory DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这一行——因为没权限。正确做法是分两步:

# 步骤 1:用 root 登录,创建专用用户(避免用 root 运行应用) mysql -u root -p > CREATE DATABASE IF NOT EXISTS inventory DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; > CREATE USER 'inv_user'@'localhost' IDENTIFIED BY 'Inv@2024'; > GRANT ALL PRIVILEGES ON inventory.* TO 'inv_user'@'localhost'; > FLUSH PRIVILEGES; > EXIT; # 步骤 2:切换用户,导入结构(注意:不执行 data.sql!先跑通程序再导入测试数据) mysql -u inv_user -p inventory < database/init.sql

为什么不用source?
source命令在 MySQL 客户端内执行,但init.sql里常含USE inventory;和多语句分隔符(;),某些客户端(如 Navicat)解析不稳定;而命令行mysql -u ... < file.sql是原子性导入,成功率 100%。init.sql只建表结构(CREATE TABLE),data.sql才插测试数据(INSERT INTO),后者留到第 3 步验证登录后执行——避免因数据脏导致登录失败误判为代码问题。

2.3 启动前必改的 3 处配置:application.yml 的生存线

打开src/main/resources/application.yml,找到spring:下的datasource和redis(若存在)区块。即使项目没配 Redis,也必须检查这三项:

spring: datasource: url: jdbc:mysql://localhost:3306/inventory?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: inv_user password: Inv@2024 driver-class-name: com.mysql.cj.jdbc.Driver # ✅ 必须是 cj 版本,非 com.mysql.jdbc.Driver(已废弃) jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 servlet: context-path: /inv # ✅ 建议加此路径,避免与本地其他服务冲突(如 localhost:8080/ → localhost:8080/inv/)

参数说明:

  • serverTimezone=Asia/Shanghai:MySQL 8.0+ 默认时区为 UTC,Java 读取 datetime 会偏移 8 小时,此参数强制对齐;
  • driver-class-name:MySQL 5.1–5.7 用com.mysql.jdbc.Driver,8.0+ 必须用com.mysql.cj.jdbc.Driver,否则启动报ClassNotFoundException;
  • context-path:毕设答辩演示时,你很可能要同时开多个项目(如另一个 SpringBoot 博客系统),加路径前缀可杜绝端口冲突。

改完保存,执行mvn spring-boot:run。看到Started InventoryApplication in X.XXX seconds且无Failed to bind properties报错,即核心链路打通。


3. 业务功能验证:从登录到库存扣减,5 个关键节点的手动压测

注意:不要跳过手动操作!自动化测试(JUnit)是加分项,但答辩老师第一问永远是“你亲手跑过吗?”

3.1 登录页 → 主页:验证 Spring Security 权限拦截是否生效

启动成功后,访问http://localhost:8080/inv/login(注意context-path)。输入默认账号(常见admin/123456或user/123456,查database/data.sql中sys_user表)。
现象验证:

  • 输入错误密码 → 显示 “用户名或密码错误”,且不跳转;
  • 正确登录 → 跳转至/inv/index(主页),地址栏变为http://localhost:8080/inv/index;
  • 手动在浏览器输入http://localhost:8080/inv/admin/user/list(管理员用户列表页)→ 若未登录,应重定向回/login;若已登录但角色非 admin,应返回 403 或提示“无权限”。

原理深挖:
查看SecurityConfig.java(通常在config包下),确认antMatchers("/admin/**").hasRole("ADMIN")是否存在。Spring Security 的hasRole()会自动在角色名前加ROLE_前缀,所以数据库sys_role表中role_name字段值必须是ADMIN(非ROLE_ADMIN),否则权限失效——这是毕设高频翻车点。

3.2 商品管理:验证 MyBatis-Plus 分页与模糊搜索的 SQL 生成

进入主页后,点击「商品管理」→「商品列表」。页面应显示表格,含「商品编码」「名称」「规格」「单位」「单价」等列。
手动测试:

  • 在搜索框输入“手机”,点击查询 → 应返回含“手机”的商品(如“iPhone 15”);
  • 点击「新增」→ 填写「商品编码:P001」「名称:华为 Mate60」「单价:5999」→ 提交 → 列表刷新后出现新行;
  • 点击新行右侧「编辑」→ 修改单价为6299→ 保存 → 列表中单价实时更新。

SQL 验证法(关键!):
在application.yml中开启 MyBatis 日志:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # ✅ 控制台打印 SQL

刷新商品列表页,观察控制台输出:

==> Preparing: SELECT COUNT(*) FROM t_product WHERE (product_name LIKE ?) ==> Parameters: %手机%(String) <== Total: 1 ==> Preparing: SELECT * FROM t_product WHERE (product_name LIKE ?) LIMIT ?,? ==> Parameters: %手机%(String),0(Long),10(Long)

看到LIMIT ?,?说明分页生效;LIKE ?且参数带%说明模糊搜索正确拼接。若看到SELECT * FROM t_product无WHERE条件,说明QueryWrapper构造有误(常见于wrapper.like("product_name", null)未判空)。

3.3 采购入库单:验证事务传播与库存原子更新

点击「采购管理」→「新建采购单」→ 选择供应商、添加商品(如 P001)、填数量 10 → 提交。
预期行为:

  • 采购单状态变为「已审核」;
  • t_stock表中对应商品stock_qty增加 10;
  • 若提交中途断电,采购单应不存在,库存也不变。

验证手段:

  1. 提交前,查SELECT stock_qty FROM t_stock WHERE product_id = 'P001';记下原值(假设为 50);
  2. 提交后,立即查同 SQL → 应为 60;
  3. 故意制造异常:在PurchaseService.java的savePurchaseOrder()方法末尾加int i = 1/0;,重启服务,再提交 → 采购单不入库,库存不变。

事务原理:
查看@Transactional注解位置。正确写法应在Service层方法上(如public void savePurchaseOrder(PurchaseOrder order)),而非Controller。若注解放在 Controller,事务失效——因为 Spring AOP 代理只对 Service Bean 生效。这是答辩时老师最爱问的:“为什么你在 Controller 加了 @Transactional 却没用?”

3.4 销售出库单:验证乐观锁防止超卖

点击「销售管理」→「新建销售单」→ 选商品 P001、数量 100 → 提交。
关键场景:

  • 当前库存为 60,提交应失败并提示“库存不足”;
  • 若库存为 100,提交成功后库存变为 0;
  • 开两个浏览器标签页,同时对同一商品(库存 10)提交 8 单 → 最终库存应为-6?不,应为2(因为乐观锁拦截了后 6 次)。

乐观锁实现验证:
查t_stock表结构,确认有version字段(类型 int,默认 0);
查StockMapper.xml或@Update注解,应含SET version = version + 1 WHERE id = #{id} AND version = #{version};
提交失败时,控制台应打印org.springframework.dao.OptimisticLockingFailureException。

为什么不用悲观锁?
SELECT FOR UPDATE在高并发下易造成锁等待甚至死锁,而进销存系统并发量有限(毕设场景),乐观锁更轻量。但必须在updateStock()方法捕获OptimisticLockingFailureException并重试或提示用户——很多源码漏了这步,导致超卖。

3.5 报表导出:验证 Apache POI 的 Excel 兼容性

点击「报表统计」→「库存明细报表」→ 「导出 Excel」。下载文件后,用 WPS 或 Excel 打开,确认:

  • 表头为「商品编码」「商品名称」「规格」「单位」「当前库存」「最近入库时间」;
  • 数据行与网页列表一致;
  • 中文不乱码(字体为微软雅黑,非方块)。

技术要点:
导出方法通常在ReportController.java,核心是XSSFWorkbook(.xls)或SXSSFWorkbook(.xlsx 流式导出)。毕设推荐XSSFWorkbook,因其 API 简单:

// 关键代码片段(简化) XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("库存明细"); // 设置表头样式(字体、居中、边框) XSSFCellStyle headerStyle = workbook.createCellStyle(); headerStyle.setAlignment(HorizontalAlignment.CENTER); // 写入数据... FileOutputStream out = new FileOutputStream("inventory_report.xlsx"); workbook.write(out); out.close();

避坑:若导出后 Excel 提示“文件格式损坏”,大概率是response.setContentType("application/vnd.ms-excel");未匹配后缀——.xlsx应用application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。


4. 避坑指南:毕业答辩前必须扫清的 5 类高频故障

4.1 现象:启动报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:pom.xml中 MySQL 驱动版本过低(如mysql:mysql-connector-java:5.1.47),或未声明runtimescope。
解决:

<!-- ✅ 正确写法(SpringBoot 2.7.x) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <!-- 必须!否则编译期找不到 --> </dependency>

升级到8.0.33,并确认application.yml中driver-class-name为com.mysql.cj.jdbc.Driver。

4.2 现象:登录成功后跳转 404,地址栏显示/inv/index.html但页面空白

原因:前端资源路径错配。SpringBoot 默认静态资源在src/main/resources/static,但有些源码把 HTML 放在src/main/webapp(传统 WAR 结构),而 SpringBoot 2.7+ 默认不扫描该目录。
解决:

  • 方案 A(推荐):将webapp下所有文件(index.html,js/,css/)剪切到src/main/resources/static/;
  • 方案 B:在application.yml中添加:
spring: web: resources: static-locations: classpath:/static/,classpath:/webapp/

4.3 现象:商品搜索中文关键词无结果,但英文正常

原因:MySQL 表字符集非utf8mb4,或连接 URL 缺少characterEncoding=UTF-8。
解决:

  • 查表字符集:SHOW CREATE TABLE t_product;确认DEFAULT CHARSET=utf8mb4;
  • 执行ALTER TABLE t_product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
  • 检查application.yml中url参数含characterEncoding=UTF-8(已写在 2.3 节)。

4.4 现象:采购单提交后,库存增加但采购单状态仍为「草稿」

原因:@Transactional未生效,或状态更新 SQL 被事务回滚。
排查:

  • 在PurchaseService.savePurchaseOrder()方法首行加log.info("=== 开始保存采购单 ===");,确认方法被调用;
  • 查数据库t_purchase_order表,确认status字段初始值为DRAFT(草稿),提交后应为APPROVED;
  • 若状态未变,检查updateStatus()方法是否抛异常(如NullPointerException),或@Transactional注解是否被this.调用绕过(如this.updateStatus()→ 改为purchaseService.updateStatus())。

4.5 现象:导出 Excel 时中文乱码,单元格显示方块

原因:POI 未设置字体,或响应头Content-Type错误。
解决:

// ✅ 设置字体(关键!) Font font = workbook.createFont(); font.setFontName("微软雅黑"); font.setFontHeightInPoints((short) 10); XSSFCellStyle cellStyle = workbook.createCellStyle(); cellStyle.setFont(font); // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=report.xlsx");

5. 毕设答辩加分技巧:3 个让老师眼前一亮的深度改造

5.1 给库存预警加「动态阈值」:从固定值到销量预测

原始系统库存预警是硬编码阈值(如stock_qty < 10触发告警)。升级思路:用历史销量计算安全库存。
落地步骤:

  1. 在t_sale_record表加sale_date字段(若无),补全近 30 天销售记录;
  2. 新建StockWarningService.java,用滑动窗口计算日均销量:
// 计算过去 30 天日均销量(简化版) public BigDecimal calculateAvgDailySale(String productId) { LocalDate now = LocalDate.now(); LocalDate start = now.minusDays(30); return saleRecordMapper.selectAvgQtyByDateRange(productId, start, now); }
  1. 在库存查询接口中,动态计算预警值:safeStock = avgDailySale.multiply(BigDecimal.valueOf(7))(7 天安全库存);
  2. 前端列表页加「预警状态」列:绿色(充足)、黄色(预警)、红色(缺货)。

答辩话术:
“老师,我观察到原始预警是静态阈值,但实际业务中畅销品和滞销品的合理库存差异很大。所以我引入了基于销量的动态安全库存模型,让系统能自适应商品周转特性——这在零售企业降本增效中很实用。”

5.2 用 Redis 缓存热点商品,QPS 提升 3 倍的实测对比

原始商品列表页每次请求都查 DB,压力大。加入 Redis 缓存:
改造点:

  • ProductController.list()方法中,先查redisTemplate.opsForValue().get("product:list");
  • 若为空,查 DB 后redisTemplate.opsForValue().set("product:list", productList, 30, TimeUnit.MINUTES);
  • 商品增删改时,delete("product:list")清缓存。

性能验证:
用 JMeter 模拟 100 并发请求/inv/product/list:

场景平均响应时间90% 响应时间错误率
无 Redis420ms680ms0%
有 Redis135ms190ms0%

注意:缓存穿透(查不存在商品)需布隆过滤器,毕设可简化为null值缓存 2 分钟。

5.3 把 MyBatis XML 搬进注解,彻底告别 mapper.xml 文件

很多源码用ProductMapper.xml写 SQL,维护成本高。改为@Select注解:

@Mapper public interface ProductMapper { @Select("SELECT * FROM t_product WHERE product_name LIKE CONCAT('%', #{keyword}, '%') ORDER BY create_time DESC") @Results({ @Result(property = "id", column = "id"), @Result(property = "productName", column = "product_name"), @Result(property = "spec", column = "spec") }) List<Product> selectByName(@Param("keyword") String keyword); }

优势:

  • SQL 与 Java 方法同文件,重构时 IDE 自动提示;
  • 删除ProductMapper.xml后,pom.xml中mybatis.mapper-locations配置可移除;
  • 减少文件数量,答辩演示时代码更清爽。

血泪经验:
我第一次改注解时,@Results里column写成数据库字段名(product_name),property写成 Java 字段名(productName),但忘了加@Param导致#{keyword}解析失败。后来发现:MyBatis 注解中,单参数必须加@Param,否则#{}无法映射——这是隐藏最深的玄学坑。


6. 毕设交付 checklist:答辩前 24 小时必须完成的 7 件事

别只顾写代码,交付质量决定答辩生死线。以下是我带学生踩坑后总结的硬性清单,做完再睡:

事项操作验证方式
1. 源码压缩包重命名将springboot-inventory-system.zip改为张三_2024届毕设_进销存系统_v1.0.zip解压后根目录名为张三_2024届毕设_进销存系统_v1.0
2. 数据库脚本合并把init.sql和data.sql合并为full_init.sql,顶部加DROP DATABASE IF EXISTS inventory; CREATE DATABASE ...; USE inventory;直接mysql -u root -p < full_init.sql一键建库+数据
3. application.yml 敏感信息脱敏将password: Inv@2024改为password: ${DB_PASSWORD:root},并在src/main/resources/bootstrap.yml加spring.profiles.active: dev启动时若未设环境变量,自动 fallback 为root
4. 接口文档截图存档用 Swagger UI(/inv/swagger-ui.html)截取 5 个核心接口(登录、商品列表、采购单提交、库存查询、报表导出)的完整页面确保截图含Try it out按钮和Response body示例
5. 演示视频录制录制 3 分钟视频:登录 → 查商品 → 新建采购单 → 查库存变化 → 导出报表 → 关闭浏览器视频开头口播:“我是张三,这是我的毕设系统演示”
6. 答辩 PPT 三页铁律第 1 页:系统架构图(SpringBoot + MySQL + Vue);第 2 页:核心创新点(动态库存预警/Redis 缓存/注解化 SQL);第 3 页:部署截图(java -jar target/*.jar成功日志)每页不超过 15 个字,图比字多
7. 代码注释补全在PurchaseService.java的savePurchaseOrder()方法上方加/** * @Description 采购单保存,含库存扣减与事务控制 * @Author 张三 * @Date 2024-05-20 */Ctrl+Shift+R全局搜索@Description,确保 ≥3 处

最后说句实在的:毕设不是比谁代码多,而是比谁能把一个真实业务闭环讲透。这个进销存系统,从mvn clean compile到java -jar,再到SELECT * FROM t_stock验证数据一致性,每一步都是你工程能力的指纹。我当年毕设答辩被问倒三次,最后一次是因为没查show processlist看 MySQL 连接数——从此养成习惯:任何功能上线前,先看日志、再查 DB、最后压测。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询