news 2026/10/9 3:18:47

SSM进销存系统实战:库存扣减与事务管理核心解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM进销存系统实战:库存扣减与事务管理核心解析

简介:这是一套基于Java SSM框架开发的美特超市进销存管理系统完整源码,面向计算机专业学生用于毕业设计或课程设计,也可作为Java Web入门者练习SSM整合与前后端分离开发的实战项目。压缩包共717个文件,约20.28MB,其中184个java文件承载后端业务逻辑,130个vue文件与44个js、17个css构成前端页面,另有29个xml、2个sql及properties、yml等配置与数据库脚本,辅以svg、jpg、png等静态资源,结构完整、层次清晰。系统采用JDK1.8、Tomcat7、MySQL5.7与Maven3.3.9,管理员账号密码均为admin,前后台入口路径明确,便于快速部署运行。目前已有98人学习关注。读者可获得一套可直接运行的进销存管理方案,涵盖商品、库存、订单等核心模块,并附环境配置安装包与数据库脚本,帮助理解SSM分层架构、Vue前端交互及项目打包部署流程,为毕设答辩与课设提交提供可参考的完整实现。

1. 从一份 SSM 进销存源码说起:美特超市管理系统到底解决了什么问题

很多做 Java 课程设计或毕业设计的同学,第一次拿到「基于 Java 美特超市进销存管理系统 ssm-91crh」这类标题时,第一反应是去搜现成源码,第二反应是发现跑不起来。进销存这三个字看着简单——进货、销售、库存,但真正落到代码里,它是一套围绕商品流转的账目系统:采购单进来库存要加,销售单出去库存要减,退货要能反向冲账,盘点要能对得上数。美特超市这个场景的价值在于,它把「多商品、多供应商、多收银员」的日常经营压缩成一个可以单机跑通的管理后台,适合拿来练 SSM(Spring + SpringMVC + MyBatis)三层架构的完整闭环。

这篇文章面向三类人:正在做课程设计、需要一套能讲清楚业务逻辑的 Java Web 项目;刚学完 SSM 想找个真实业务练手;以及接手了类似进销存系统、需要快速理清表结构和库存扣减逻辑的初级工程师。我不会假装看过那份 zip 里的每一行代码,而是按这类系统最常见的可靠做法,把技术选型、库表设计、核心业务实现和踩坑点讲透,让你拿到任何一份同类源码都能自己跑通、改得动。

2. SSM 三层架构在进销存里的分工:谁管库存,谁管账

2.1 为什么进销存系统适合用 SSM 而不是 Spring Boot 一把梭

先回答一个高频疑问:都 2025 年了,为什么这类课程设计还在用 SSM?原因很实际——SSM 把 Spring 的 IoC/AOP、SpringMVC 的请求分发、MyBatis 的 SQL 映射拆得清清楚楚,面试时问「java 八股文」里的 Bean 生命周期、事务传播、动态 SQL,这套架构每一层都能对应上具体代码。而 Spring Boot 虽然开箱即用,但自动配置把很多细节藏进了黑匣子,对刚入门的人反而不利于理解请求是怎么从 Controller 走到 Mapper 的。

进销存的业务特点决定了它必须强事务。一次销售出库,要同时写销售单主表、销售单明细、扣减库存、可能还要记一条库存流水。这四步要么全成功要么全回滚,靠的就是 Spring 的声明式事务。SSM 里通常把@Transactional加在 Service 层方法上,而不是 Controller 或 Mapper,这是第一个要记住的边界。

选型上还有几个现实约束。数据库一般用 MySQL 5.7 或 8.0,JDK 用 1.8 最稳,Tomcat 用 8.5 或 9.0。别一上来就 JDK 17 配老版本 Spring,java 启动失败怎么解决这类问题十有八九是版本对不上。Maven 依赖里 spring-webmvc、mybatis-spring、druid 连接池、mysql-connector-java 是核心,jsp 或 thymeleaf 看源码用的是哪个。

2.2 库表设计:进销存的六张核心表怎么定

进销存的表不用多,但字段要想清楚。下面这套结构是我在多个同类项目里验证过的最小可用集,字段名按常见命名习惯给,你对照源码时按实际调整。

表名作用关键字段
goods商品档案id, goods_name, category_id, purchase_price, sale_price, stock, warn_stock
supplier供应商id, supplier_name, contact, phone, address
purchase_order采购单主表id, order_no, supplier_id, total_amount, status, create_time
purchase_item采购单明细id, order_id, goods_id, quantity, price
sale_order销售单主表id, order_no, user_id, total_amount, pay_type, create_time
sale_item销售单明细id, order_id, goods_id, quantity, price

库存字段stock放在 goods 表里,这是最直接的做法,但要注意并发。warn_stock是预警库存,低于它就在首页标红,这是超市场景的刚需。订单主表和明细表拆开,是为了支持一单多商品,这是进销存和普通 CRUD 最大的区别。

建表时金额字段用decimal(10,2),别用 float,否则对账时会出现0.1+0.2!=0.3的玄学问题。订单号order_no建唯一索引,防止重复提交生成两张单。

2.3 用 MyBatis 逆向工程生成实体和 Mapper 的最小步骤

很多源码包里已经带好了实体类,但你要自己加字段时,手写容易漏。常见做法是用 MyBatis Generator 或 MyBatis-Plus 的代码生成器。这里给一个不依赖插件的纯配置思路,核心是generatorConfig.xml:

<!-- generatorConfig.xml 关键片段 --> <context id="mysql" targetRuntime="MyBatis3"> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/market?useSSL=false&amp;serverTimezone=Asia/Shanghai" userId="root" password="你的密码"/> <!-- 指定实体类包名 --> <javaModelGenerator targetPackage="com.meite.entity" targetProject="src/main/java"/> <!-- 指定 Mapper XML 位置 --> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/> <javaClientGenerator type="XMLMAPPER" targetPackage="com.meite.mapper" targetProject="src/main/java"/> <!-- 只生成 goods 表,避免全库覆盖 --> <table tableName="goods" domainObjectName="Goods" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false"/> </context>

逻辑说明:targetRuntime选 MyBatis3 会生成带 Example 的查询类,但进销存里复杂查询多,Example 反而累赘,所以把三个enableXxxByExample关掉,只留基础 CRUD。tableName一次只写一张表,生成完再改下一张,避免误覆盖已有代码。参数上serverTimezone必须加,否则 MySQL 8 连接会报时区错误,这是java 环境配置里最常翻车的一处。

生成后检查实体类字段类型:stock是 int,purchase_price是 BigDecimal,create_time是 Date。如果生成成了 String,说明数据库字段类型或 JDBC 类型映射有问题,回去改表。

3. 采购入库与销售出库:库存扣减的两种写法与事务边界

3.1 采购入库:先加库存还是先写单

采购入库的流程是:录入采购单 → 保存主表和明细 → 增加商品库存 → 更新采购单状态为「已入库」。顺序上必须先写单再改库存,因为库存变动要有单据可追溯。如果先改库存,写单失败,库存就凭空多了,对不上账。

Service 层方法大致长这样:

@Service public class PurchaseServiceImpl implements PurchaseService { @Autowired private PurchaseOrderMapper orderMapper; @Autowired private PurchaseItemMapper itemMapper; @Autowired private GoodsMapper goodsMapper; // 整个入库动作在一个事务里,任何一步失败全部回滚 @Override @Transactional(rollbackFor = Exception.class) public void stockIn(PurchaseOrder order, List<PurchaseItem> items) { // 1. 写主表,order_no 由前端或工具类生成 order.setStatus(1); // 1 表示已入库 orderMapper.insert(order); // 2. 逐条写明细并加库存 for (PurchaseItem item : items) { item.setOrderId(order.getId()); itemMapper.insert(item); // 3. 库存增加,用 SQL 原子操作而不是先查再改 goodsMapper.increaseStock(item.getGoodsId(), item.getQuantity()); } } }

逻辑说明:@Transactional的rollbackFor = Exception.class很关键,默认只回滚 RuntimeException,如果中间抛了受检异常不回滚,库存就脏了。库存增加不要写成「先 select 出 stock,加完再 update」,那样并发下会丢更新。正确做法是在 Mapper XML 里写一条原子 SQL:

<update id="increaseStock"> UPDATE goods SET stock = stock + #{quantity} WHERE id = #{goodsId} </update>

参数说明:#{quantity}是本次入库数量,正数。这条 SQL 依赖数据库行锁,并发时后一个事务会等前一个提交,保证不丢。代价是高并发下会排队,但超市后台并发量低,完全够用。

3.2 销售出库:库存不足的判断放在哪一层

销售出库比入库多一个约束:库存不能为负。判断库存够不够,不能只在 Java 里查一次就扣,因为查和扣之间可能被其他事务插队。稳妥写法是在扣减 SQL 里带上条件:

<update id="decreaseStock"> UPDATE goods SET stock = stock - #{quantity} WHERE id = #{goodsId} AND stock >= #{quantity} </update>

然后在 Service 里判断影响行数:

int rows = goodsMapper.decreaseStock(item.getGoodsId(), item.getQuantity()); if (rows == 0) { // 影响行数为 0 说明库存不足,抛异常触发回滚 throw new RuntimeException("商品库存不足,出库失败"); }

逻辑说明:把库存判断下推到 SQL 的 WHERE 条件里,利用数据库的原子性,避免「查了够、扣的时候不够」的竞态。返回 0 就抛异常,整个销售单回滚,不会出现扣了一半的情况。这是进销存里最值得抄的一段逻辑,比在 Java 里加 synchronized 靠谱得多。

参数说明:stock >= #{quantity}是安全边界,等于也允许扣,扣完为 0。如果业务要求保留安全库存,把条件改成stock - #{quantity} >= warn_stock。

3.3 退货与冲账:反向操作怎么不把账搞乱

退货分采购退货和销售退货。销售退货是库存加回来、金额退回去;采购退货是库存减回去、应付账款减少。核心原则是「只增单据,不改历史单据」。不要直接去 update 原来那张销售单的金额,而是新生成一张退货单,类型字段区分正负。

常见做法是在 sale_order 表加一个order_type字段,1 是销售,2 是销售退货。统计当日销售额时用SUM(CASE WHEN order_type=1 THEN total_amount ELSE -total_amount END)。这样历史单据永远不动,审计时能查到每一笔。

退货的库存操作和前面相反:销售退货调increaseStock,采购退货调decreaseStock。事务边界和正常单据一样,整单一起提交或回滚。

4. 跑起来之前:环境、依赖和数据库初始化的排查清单

4.1 JDK、Tomcat、MySQL 版本怎么配不打架

java 环境变量使用多个 jdk是很多人卡住的地方。如果你机器上有 JDK 8 和 JDK 17,跑这个 SSM 项目必须让 Tomcat 用 JDK 8。做法是改 Tomcat 的bin/setclasspath.bat(Windows)或setclasspath.sh(Linux),在开头显式设置:

# Linux 下 setclasspath.sh 开头加 export JAVA_HOME=/usr/local/jdk1.8.0_301 export JRE_HOME=$JAVA_HOME/jre

Windows 同理设set JAVA_HOME=...。不改这个,Tomcat 会去用系统默认的 JDK,版本一高,老 Spring 反射就报java.lang.OutOfMemoryError或Unsupported class file major version。java 启动失败怎么解决里一大半是这个问题。

MySQL 建议 5.7,字符集用 utf8mb4,排序规则 utf8mb4_general_ci。建库语句:

CREATE DATABASE market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

4.2 导入 SQL 脚本后必查的三处数据

源码包里的.sql文件导入后,别急着启动,先跑三条检查:

-- 1. 商品表有没有初始数据,没有的话登录后首页是空的 SELECT COUNT(*) FROM goods; -- 2. 管理员账号是否存在,密码字段是不是明文或 MD5 SELECT username, password FROM sys_user WHERE username = 'admin'; -- 3. 检查外键关联的供应商、分类是否有数据 SELECT COUNT(*) FROM supplier;

如果商品表是空的,采购和销售都没法测。常见做法是脚本里带几条测试数据,没有就自己插。密码如果是 MD5,登录时前端传明文、后端加密比对;如果是明文,先跑通再改成加密,别一上来就动登录逻辑。

4.3 连接池和 Mapper 扫描的两个高频配置

applicationContext.xml里 Druid 连接池的url要带时区和编码:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="jdbc:mysql://localhost:3306/market?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean>

Mapper 扫描用MapperScannerConfigurer,basePackage要指到 mapper 接口所在包,别指到实体包:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.meite.mapper"/> </bean>

如果启动报No qualifying bean of type xxxMapper,九成是basePackage写错,或者 Mapper XML 的namespace和接口全限定名对不上。

5. 避坑与排查:进销存系统上线前最容易翻车的五件事

现象一:销售出库后库存变成负数。原因是没有在扣减 SQL 里加stock >= quantity条件,或者判断和扣减分成了两步。解决:把判断下推到 UPDATE 的 WHERE,用影响行数判断成败,返回 0 就抛异常回滚。

现象二:一次采购入库,库存加了两遍。原因通常是前端重复提交,或者 Service 方法被嵌套调用导致事务失效。解决:订单号加唯一索引,数据库层面挡住重复;检查@Transactional是不是加在了 private 方法或同类内部调用上,那样代理不生效。

现象三:中文商品名存进数据库变成问号。原因有三处:数据库字符集不是 utf8mb4、JDBC url 没带characterEncoding=utf8、Tomcat 的server.xml里 Connector 没加URIEncoding="UTF-8"。三处都要查,缺一处就乱码。

现象四:启动报java.lang.OutOfMemoryError: Java heap space。原因不一定是内存真不够,可能是 JDK 版本和 Spring 不匹配导致类加载异常。先确认 Tomcat 用的是 JDK 8,再调catalina.sh里的-Xms512m -Xmx1024m。进程堆大小调整为 8000 还是报错的情况,多半是版本问题不是内存问题。

现象五:MyBatis 查询返回字段为 null。原因一般是数据库列名goods_name和实体属性goodsName没映射上。解决:要么在mybatis-config.xml开mapUnderscoreToCamelCase=true,要么在 SQL 里写别名。开了驼峰映射后,goods_name自动对应goodsName,这是最省事的做法。

6. 把进销存改成能写进简历的项目:三个进阶动作

跑通只是起点。如果你想让这个美特超市进销存管理系统在面试里拿得出手,我建议做三件事,每一件都能对应一个可以聊十分钟的技术点。

第一件,把库存扣减从「数据库行锁」升级成「乐观锁」。在 goods 表加一个version字段,扣减时带上版本号:

UPDATE goods SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{goodsId} AND stock >= #{quantity} AND version = #{version}

影响行数为 0 时重试或提示。这样并发下不会长时间持锁,面试时能讲清楚悲观锁和乐观锁的取舍,比只会背八股文强。

第二件,加一张库存流水表stock_record,每次入库出库都记一条:商品 id、变动数量、变动前库存、变动后库存、单据号、时间。这张表是「后悔药」,对账对不上时能查到每一步。实现上在 Service 里扣减成功后插入,和主业务同一个事务。

第三件,把统计报表用一条 SQL 写出来,而不是在 Java 里循环累加。比如查某商品近 30 天每天的出库量:

SELECT DATE(so.create_time) AS day, SUM(si.quantity) AS out_qty FROM sale_order so JOIN sale_item si ON so.id = si.order_id WHERE si.goods_id = #{goodsId} AND so.order_type = 1 AND so.create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(so.create_time) ORDER BY day;

这条 SQL 用到了 JOIN、GROUP BY、日期函数,面试时能顺带聊索引怎么建——sale_order(create_time)和sale_item(goods_id)各建一个,避免全表扫描。

我自己做这类项目最大的教训是:别一上来就追求功能多,先把「采购入库、销售出库、库存对账」这条主线跑通,每个环节的事务边界想清楚,再去加会员、促销、多仓库。库存对不上是进销存的死穴,而 90% 的对不上都源于事务没管好或者扣减没做原子操作。把这两点守住,这套 SSM 进销存就值得写进简历,也经得起面试官追问。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:18:25

防火墙源码深度拆解:包过滤与状态检测的工程实践

简介&#xff1a;这是一份面向网络安全初学者与防火墙开发爱好者的源代码学习资料&#xff0c;围绕防火墙核心功能的实现展开&#xff0c;适合具备一定C/C基础、希望理解包过滤与网络钩子机制的技术人员参考。压缩包共236个文件&#xff0c;整体约1.23MB&#xff0c;以54个h头文…

作者头像 李华
网站建设 2026/10/9 3:18:16

JavaWeb酒店预定系统实战:class文件反编译与Servlet改造

简介&#xff1a;这是一套面向计算机专业学生与Java Web入门者的酒店客房预定管理系统实战项目&#xff0c;采用Servlet与JSP技术栈&#xff0c;配合Tomcat服务器和MySQL数据库&#xff0c;分为前台用户端与后台管理端&#xff0c;可直接作为毕业设计或课程设计参考。压缩包共1…

作者头像 李华
网站建设 2026/10/9 3:17:41

Servlet+JSP+MySQL房屋租赁系统:从零搭建与避坑指南

简介&#xff1a;这是一套基于Servlet的房屋租赁管理信息系统完整源码&#xff0c;采用JavaServletJSPMySQL技术栈&#xff0c;运行于Tomcat7及以上环境&#xff0c;适合Java Web初学者、课程设计或毕业设计参考者学习使用。系统分为管理员与普通用户两种角色&#xff0c;前台涵…

作者头像 李华
网站建设 2026/10/9 3:17:37

Java实现数值分析、线性代数与机器学习算法:从公式到工程落地

简介&#xff1a;这是一套面向Java开发者、科研人员与学生的基础算法源码包&#xff0c;聚焦数值分析、线性代数与机器学习三大方向&#xff0c;帮助使用者在Java项目中快速嵌入数学计算与建模能力&#xff0c;降低从底层实现到应用验证的门槛。资源共45个文件&#xff0c;以36…

作者头像 李华
网站建设 2026/10/9 3:17:33

Git核心技能实操:从安装配置到疑难杂症的全套考题与参考答案

很多刚接触 Git 的同学容易陷入一个误区&#xff1a;命令背了一堆&#xff0c;碰到真实项目还是一头雾水。最近我在团队内部做了一次 Git 核心技能摸底&#xff0c;把这几年踩过的坑、带新人时常遇到的问题、以及日常评审时发现的高频失误&#xff0c;全部揉成了一套实操考试题…

作者头像 李华
网站建设 2026/10/9 3:17:24

测试文章发布指南:版本号、检查清单与避坑技巧

我们做内容创作这些年&#xff0c;最怕听到的一句话往往不是“标题不够吸引人”&#xff0c;而是“我刚才好像不小心把测试文章发出去了”。说真的&#xff0c;测试文章这种东西&#xff0c;平时它安安静静躺在草稿箱里&#xff0c;一旦在错误的时间被推上线&#xff0c;轻则排…

作者头像 李华