Spring Boot进销存系统开发:从数据模型到事务权限的完整实践
简介面向计算机及相关专业毕业生的Spring Boot企业进销存系统毕业设计资料包以代码加论文的形式完整呈现进销存业务的后端实现与文档支撑解决选题设计、编码实现和论文撰写等痛点。系统围绕企业日常运营设计了九大功能模块包括公告发布、供应商与客户档案维护、商品信息及价格库存管理、商品出入库精准记录、销售订单全流程处理、退货审批退款以及销售人员和仓库人员的绩效与职责分配业务覆盖较为全面适合作为课程设计或毕业设计的直接参考与二次开发基础。资源包采用zip压缩格式整体大小约27.26MB内含项目源码和论文文档结构清晰便于本地导入运行与按章节查阅。目前已有54人学习适合具备一定Java基础、希望快速搭建进销存系统并完成毕业设计的学生使用。1. 基于 Spring Boot 的企业进销存系统不是一套 CRUD 那么简单企业进销存系统在 Java 毕业设计里的出现频率极高但大多数人把它的难点想小了。真正让这套系统区别于普通课设的不是商品表、供应商表和销售单的增删改查而是「库存怎么保证不错」「一张销售单提交后库存、流水、账单如何在同一个事务里同时完成」。如果只做到 Controller 调 Service、Service 调 Mapper 的程度那和学生管理系统没有本质区别。这个标题里的「企业进销存系统」需要覆盖采购、销售、库存、供应商、客户、报表几个模块还要考虑代码和论文的对应关系——ER 图画什么、测试用例表怎么写、答辩时被问事务和并发怎么回答这些都直接决定项目能不能过。适合的人群很明确正在做 Spring Boot 毕设的 Java 后端学生以及想快速把进销存业务模型理清的前端同学。接下来的内容会从骨架搭建、数据模型、核心业务事务到报表权限一步步拆开全部是能直接落到 IDEA 里的方案。2. 搭建 Spring Boot 进销存项目骨架版本选型、目录与最小配置2.1 Spring Boot 版本怎么选以及为什么毕设别追最新很多人在 IDEA 创建项目时看到 Spring Boot 3.x 就顺手选了最新版结果后续 MyBatis 适配、javax 换成 jakarta 包名、一些老教程的代码直接跑不起来。Spring Boot 3.0 之后把javax.*迁移到了jakarta.*如果手里参考的毕设源码是 2.x 的复制过来后 IDE 会大面积报红。这不是代码错了是包名体系变了。毕设选题这块没有一个统一标准答案但我一般建议优先选维护期内的成熟版本而不是最新版本。具体操作上JDK 8 配 Spring Boot 2.7.xJDK 17 配 Spring Boot 3.x这两个组合是社区里最常用的对。选 2.7.x 的额外好处是网上搜到的 MyBatis 配置、拦截器写法、分页插件用法都不用改动论文里的技术描述也更容易对上实际代码。需要特别明确的一点Spring Boot 版本号无所谓高低好坏进销存系统用到的东西无非是 Web、AOP、事务、JDBC、Thymeleaf 或 Vue 联调这些能力在 2.7 和 3.x 上没有本质差别。2.2 项目目录结构与 Maven 依赖清单前后端分离是当下毕设的常见形式我用一个 Maven 单模块结构来组织后端前端 Vue 项目独立放在另一个目录里。后端目录按职责分层不搞花活src/main/java/com/example/inventory ├── InventoryApplication.java 启动类 ├── controller 采购、销售、库存、报表等接口 ├── service 业务逻辑层 │ └── impl 实现类 ├── mapper MyBatis 数据访问层 ├── entity 数据库实体 │ ├── Product.java │ ├── Stock.java │ ├── StockFlow.java │ └── PurchaseOrder.java ├── dto 接收前端参数的对象 ├── common 统一返回结果、异常处理 └── config 拦截器、CORS 配置对应的pom.xml核心依赖如下用spring-boot-starter-web提供接口能力用mybatis-spring-boot-starter做 ORM用mysql-connector-j连接数据库lombok简化实体代码parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这套依赖的关键点在哪spring-boot-starter-parent统一管理版本号写scoperuntime的 MySQL 驱动不会被打进最终运行逻辑里mybatis-spring-boot-starter会自动读取数据源配置并创建 SqlSessionFactory。启动类上的SpringBootApplication里藏着自动装配的核心逻辑——它就是三个注解的组合EnableAutoConfiguration开启自动配置、ComponentScan扫描本包及子包的 Bean、SpringBootConfiguration标记配置类。面试时问 Spring Boot 自动装配原理答到这一步就已经比大多数人强。2.3 application.yml 里的关键配置与自动装配原理进销存系统用不到多数据源一份application.yml配好数据源、MyBatis 驼峰映射和日志即可。注意骆驼命名映射不打开的话数据库字段product_name映射到productName会全部为 nullserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/inventory_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpllog-impl设为 StdOutImpl 是调试期常用手段——SQL 会原样打印到控制台参数值、执行结果一目了然。但要注意它只打印 SQL 和参数业务异常堆栈还是走正常日志通道。time-zone不加的话日期字段经常差 8 小时这是因为 MySQL 连接器默认按服务器时区解析而 MySQL 8 的默认时区是 UTC。2.4 IDEA 创建 Spring Boot 进销存项目的最小步骤IDEA 里创建项目时选 Spring InitializrType 选 MavenJava 版本按前面定的 JDK 版本选。依赖选择界面只需要勾 Spring Web、MySQL Driver、Lombok 三个MyBatis 依赖后面手动加进pom.xml因为 IDEA 初始界面的版本列表不一定有你想要的 MyBatis starter 版本。创建完项目后第一件事不是写代码而是先跑一次空的 Spring Boot 启动类确认内嵌 Tomcat 能起来。这一步能排除 Java 版本不匹配、Maven 仓库下载失败等环境问题再开始建表、写实体、接业务。3. 进销存系统的核心数据模型从商品表到库存流水表的设计3.1 进销存系统最少需要哪几张表一张采购单、一张销售单、一张库存表加上商品和往来单位这是大多数人第一版设计。实际跑起来会发现没法回答「某个商品的库存是怎么变成现在这个数的」。要回答这个问题必须引入库存流水表每一笔入库、出库、盘点调整都写一条流水库存表只存当前余额。这个设计的核心用意是库存余额可以被流水重新推导流水是账本余额是快照。完整的最小表集合是九张用户表、角色表、菜单表、商品表、供应商表、客户表、采购单表、销售单表、库存流水表。其中采购单表和销售单表都需要一个明细表来存商品明细所以实际是十一张左右。角色和菜单表是为了做权限控制前端菜单根据当前用户的角色动态渲染后端接口再做一遍校验。3.2 商品表、库存表与库存流水表的建表 SQL直接给核心的三张表建表语句这是整套系统数据模型的骨架CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_no VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编号, product_name VARCHAR(64) NOT NULL COMMENT 商品名称, specification VARCHAR(64) COMMENT 规格型号, unit VARCHAR(16) COMMENT 单位, price DECIMAL(10,2) NOT NULL COMMENT 参考进价, sale_price DECIMAL(10,2) NOT NULL COMMENT 参考售价, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL UNIQUE, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, flow_type TINYINT NOT NULL COMMENT 1入库 2出库 3盘盈 4盘亏, quantity INT NOT NULL COMMENT 变动数量正数, before_quantity INT NOT NULL, after_quantity INT NOT NULL, ref_no VARCHAR(32) COMMENT 关联单据号, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_product_time (product_id, create_time) );三个设计点特别说明。第一个金额字段统一用DECIMAL(10,2)浮点数在累计计算时会丢失精度这在报表模块做月度汇总时是致命的。第二个stock_flow里同时记录变动前后库存这是为了能回溯每一笔操作对库存的影响论文里的数据分析图表也依赖它。第三个stock表的product_id设置 UNIQUE 约束保证一个商品只对应一条库存记录防止并发下插入重复库存行。3.3 外键、索引和 ER 图画法进销存系统的表我建议不建物理外键只建普通索引外键约束在代码事务里控制。原因很简单物理外键在删除关联数据时会报错或连带删除毕设项目的数据量根本不需要数据库级外键保护。删除供应商时代码里先检查有没有关联采购单再决定是否允许删除逻辑完全在 Service 层控制。ER 图是论文里必备的内容。绘制思路上以商品表和库存表为中心采购单表、销售单表通过明细表关联到商品表供应商表关联采购单客户表关联销售单用户表通过角色表关联菜单表。用 IDEA 的 Database 工具连接数据库后右键选择 Diagrams 里的 Show Diagram可以自动导入表关系生成 ER 图导出为图片后放进论文。4. 采购入库与销售出库Spring Boot 事务与库存防超卖的实现4.1 采购入库的 Service 层实现事务加库存流水采购入库的业务流程是前端提交采购单后端校验供应商存在、明细不为空然后做三件事——保存采购单主表、保存采购明细、批量更新库存并写入库流水。这三件事必须在同一个事务里完成任何一步失败都全部回滚否则会出现采购单保存了、库存没加上的脏数据。Service public class PurchaseServiceImpl implements PurchaseService { Autowired private PurchaseOrderMapper purchaseOrderMapper; Autowired private PurchaseOrderItemMapper itemMapper; Autowired private StockMapper stockMapper; Autowired private StockFlowMapper stockFlowMapper; Override Transactional(rollbackFor Exception.class) public void createPurchaseOrder(PurchaseOrderDTO dto) { // 1. 保存采购单主表状态为待入库 PurchaseOrder order new PurchaseOrder(); order.setSupplierId(dto.getSupplierId()); order.setTotalAmount(dto.getTotalAmount()); order.setStatus(0); purchaseOrderMapper.insert(order); // 2. 保存采购明细 for (PurchaseOrderItemDTO itemDTO : dto.getItems()) { PurchaseOrderItem item new PurchaseOrderItem(); item.setOrderId(order.getId()); item.setProductId(itemDTO.getProductId()); item.setQuantity(itemDTO.getQuantity()); item.setPrice(itemDTO.getPrice()); itemMapper.insert(item); // 3. 更新库存 写流水 Stock stock stockMapper.selectByProductId(itemDTO.getProductId()); if (stock null) { throw new RuntimeException(商品库存记录不存在); } int before stock.getQuantity(); int after before itemDTO.getQuantity(); stock.setQuantity(after); stockMapper.update(stock); StockFlow flow new StockFlow(); flow.setProductId(itemDTO.getProductId()); flow.setFlowType(1); flow.setQuantity(itemDTO.getQuantity()); flow.setBeforeQuantity(before); flow.setAfterQuantity(after); flow.setRefNo(order.getOrderNo()); stockFlowMapper.insert(flow); } } }这段代码最关键的是Transactional注解它让整个方法处于同一个数据库事务中。这里用了rollbackFor Exception.class而不是默认值是因为 Spring 默认只在抛出 RuntimeException 或 Error 时才回滚自定义的业务异常如果不继承 RuntimeException事务会静默提交。参数itemDTO.getQuantity()在入库场景没有负数校验的必要性但如果前端传了负数库存会被反向扣减所以实际项目中要在 DTO 校验层拦截非正数。4.2 销售出库与防超卖乐观锁和事务的配合销售出库比采购入库多一个并发问题——超卖。两个用户同时下单购买同一个商品两个请求同时读到库存 10第一个减到 9第二个也减到 9实际库存就错了。解决办法是给库存表的 UPDATE 语句加条件AND quantity ?这样只有一个请求能更新成功Mapper public interface StockMapper { // 乐观扣减库存只有当前库存足够时才更新 Update(UPDATE stock SET quantity quantity - #{quantity} WHERE product_id #{productId} AND quantity #{quantity}) int deductStock(Param(productId) Long productId, Param(quantity) Integer quantity); }Service 层调用deductStock时检查返回值如果返回 0 说明没有影响任何行也就是库存不足或商品不存在直接抛异常。这个写法的核心优势在于把并发控制下沉到了数据库层面不依赖 JVM 锁多个实例部署也能正确工作。但要注意普通Update注解方式下MyBatis 的useGeneratedKeys不生效所以库存更新后的流水记录里before_quantity需要先在 Service 层查一次当前库存这中间存在极小的窗口期不过在毕设场景里完全足够。4.3 事务不生效的几个坑自调用、异常被吞、数据源没配事务事务是进销存系统里最容易出 bug 的地方常见的坑有三个。第一个是自调用同类内部一个方法调用另一个带Transactional的方法事务不生效因为 Spring 事务是基于 AOP 动态代理实现的内部this调用不会经过代理对象。解决方法是把需要事务的方法拆到另一个 Service 类里或者注入自己的代理对象。第二个是异常被吞掉try-catch里捕获了异常却没有重新抛出事务不会回滚。比如采购入库时库存更新失败但代码里 catch 住后只打印了日志事务管理器看到的是正常返回于是提交了主表数据。规范做法是 catch 里记录日志后throw new RuntimeException(入库失败)。第三个坑更隐蔽自定义数据源时没有配置PlatformTransactionManagerTransactional就会静默失效。排查方法是启动日志里看有没有输出事务拦截器的初始化信息或者写个测试方法故意抛异常验证数据有没有回滚。5. 权限、报表与 Vue 前后端对接毕设进销存系统的完整闭环5.1 RBAC 权限模型与登录拦截器企业进销存系统天然分角色采购员只碰采购单销售员只碰销售单仓库管理员管库存管理员配角色。用 RBAC 模型实现权限控制三张表搞定用户表、角色表、用户角色关联表更细的做法是加菜单表和角色菜单关联表让不同角色看到不同的左侧菜单。登录状态校验我用拦截器实现不走 Spring Security 那套重量级方案毕设场景里完全够用且更容易讲清楚。用户登录成功后把 userId 存进 Session拦截器里检查 Session 是否为空Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object userId request.getSession().getAttribute(userId); if (userId null) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write( {\code\:401,\msg\:\未登录\} ); return false; } return true; } }配合一个 WebMvcConfigurer 注册拦截器并放行登录接口和静态资源。权限校验要区分两个层面拦截器只管是否登录按钮和接口级权限在需要时再通过角色编码判断。前端 Vue 路由加全局守卫检查 localStorage 里的 token 或用户信息这套前后端配合的做法在答辩时能直接说清「会话管理」是怎么闭环的。5.2 统一返回结构Vue 前端对接才不用到处写状态判断前后端分离下最容易产生混乱的地方是接口返回格式不统一有的接口直接返回 Map有的返回数组有的把成功标志放在不同字段里前端怎么猜都是错。进销存系统我通常在common包里定义一个统一返回类Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(success); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }Controller 每个接口都返回Result?配合全局异常处理器RestControllerAdvice业务异常、参数校验异常、系统异常都统一转成对应的 code 和 msg。Vue 前端的 axios 响应拦截器里只判断code 200这一个条件其他情况统一弹错误提示。这样做的好处是前端不需要为每个接口单独写错误处理接口层的行为一致排查问题时也能快速定位是后端抛错还是前端逻辑问题。5.3 报表统计 SQL 与库存周转率报表是进销存论文里最好的加分项。月度销售统计用GROUP BY对销售明细聚合然后和商品表关联拿到名称展示SELECT p.product_name, SUM(o.quantity) AS total_quantity, SUM(o.amount) AS total_amount FROM sale_order_item o LEFT JOIN product p ON p.id o.product_id WHERE o.create_time BETWEEN 2024-01-01 AND 2024-01-31 GROUP BY o.product_id ORDER BY total_amount DESC LIMIT 10;注意SUM(o.amount)里的 amount 字段在创建销售单时就要从数量和单价算好存进去不要在统计时用quantity * price现场算。原因有两个单价可能在促销或改价后变化历史单据的销售额必须按当时的价格商品表里改价不影响历史报表另一个原因是统计查询的性能现场计算意味着每一行都要参与运算数据量上来后会变慢。库存在报表里还有一个常用指标是库存周转率公式是出库总金额除以平均库存金额。论文里放这个公式的推导过程再配合一段 SQL 统计结果业务闭环就完整了。6. 运行验证、常见排错与论文答辩配套6.1 从零到跑通的最小验证清单拿到项目后不要一次性启动按下述顺序验证每一大步过了再做下一步启动 MySQL执行项目里的init.sql启动 Spring Boot看到 Tomcat started on port 8080浏览器直接访问登录接口确认数据库连接正常新增商品 → 新增供应商 → 创建采购单 → 采购入库 → 查库存确认增加新增客户 → 创建销售单 → 销售出库 → 查库存确认减少、查流水确认记录打开报表页面确认统计图能出数。其中第 4 步和第 5 步如果库存数字不对不要看界面直接查stock_flow表的before_quantity和after_quantity流水是对的说明业务代码正确流水是错的说明事务写入了错误的计算逻辑。6.2 高频排错点版本、时区、连接池一类常见问题集中在 Spring Boot 版本太高导致的配置失效。比如高版本下server.tomcat.uri-encoding不再生效中文字符乱码时要检查前端请求是否带了charsetUTF-8以及数据库连接 url 里的characterEncoding是否显式配置。另一类是时区问题MySQL 连接串建议写成jdbc:mysql://localhost:3306/inventory_db?serverTimezoneAsia/ShanghaicharacterEncodingutf8mb4这样应用和数据库的时区都对齐。还有看到连接池报Passwordless connections are not supported by MySQL的报错时检查驱动是不是 8.0 以上的版本配了 5.x 的连接方式。6.3 论文里 ER 图、流程图和测试用例对应的素材论文不需要贴全部代码但要有技术原理的递进关系。ER 图用数据库工具导出后按模块拆成两张图放论文一张是核心业务表的逻辑模型一张是权限表的简化模型。流程图画采购和销售两个主流程就够了用 Visio 的泳道图画三行——前端操作、后端逻辑、数据库操作答辩时照着图讲思路比纯代码更清晰。测试用例用 Word 里的三线表列号放「用例编号、操作步骤、预期结果、实际结果」把上面最小验证清单里的六步填进去每行配一个截图这块内容基本不会扣分。答辩追问的高频问题就三类事务为什么能回滚、库存超卖怎么解决、数据库表为什么没有外键。事务答 AOP 动态代理结合回滚策略超卖答乐观锁条件更新外键答通过代码保证一致性、逻辑外键便于扩展。这三问的答案在这篇文章里已经全部落在代码注释和参数说明中回头再看一遍重心是理解为什么这样设计而不是背诵代码。本文还有配套的精品资源点击获取