多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

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

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」这类标题时第一反应是去搜现成源码第二反应是发现跑不起来。进销存这三个字看着简单——进货、销售、库存但真正落到代码里它是一套围绕商品流转的账目系统采购单进来库存要加销售单出去库存要减退货要能反向冲账盘点要能对得上数。美特超市这个场景的价值在于它把「多商品、多供应商、多收银员」的日常经营压缩成一个可以单机跑通的管理后台适合拿来练 SSMSpring 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.0JDK 用 1.8 最稳Tomcat 用 8.5 或 9.0。别一上来就 JDK 17 配老版本 Springjava 启动失败怎么解决这类问题十有八九是版本对不上。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_stocksupplier供应商id, supplier_name, contact, phone, addresspurchase_order采购单主表id, order_no, supplier_id, total_amount, status, create_timepurchase_item采购单明细id, order_id, goods_id, quantity, pricesale_order销售单主表id, order_no, user_id, total_amount, pay_type, create_timesale_item销售单明细id, order_id, goods_id, quantity, price库存字段stock放在 goods 表里这是最直接的做法但要注意并发。warn_stock是预警库存低于它就在首页标红这是超市场景的刚需。订单主表和明细表拆开是为了支持一单多商品这是进销存和普通 CRUD 最大的区别。建表时金额字段用decimal(10,2)别用 float否则对账时会出现0.10.2!0.3的玄学问题。订单号order_no建唯一索引防止重复提交生成两张单。2.3 用 MyBatis 逆向工程生成实体和 Mapper 的最小步骤很多源码包里已经带好了实体类但你要自己加字段时手写容易漏。常见做法是用 MyBatis Generator 或 MyBatis-Plus 的代码生成器。这里给一个不依赖插件的纯配置思路核心是generatorConfig.xml!-- generatorConfig.xml 关键片段 -- context idmysql targetRuntimeMyBatis3 jdbcConnection driverClasscom.mysql.cj.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/market?useSSLfalseamp;serverTimezoneAsia/Shanghai userIdroot password你的密码/ !-- 指定实体类包名 -- javaModelGenerator targetPackagecom.meite.entity targetProjectsrc/main/java/ !-- 指定 Mapper XML 位置 -- sqlMapGenerator targetPackagemapper targetProjectsrc/main/resources/ javaClientGenerator typeXMLMAPPER targetPackagecom.meite.mapper targetProjectsrc/main/java/ !-- 只生成 goods 表避免全库覆盖 -- table tableNamegoods domainObjectNameGoods enableCountByExamplefalse enableUpdateByExamplefalse enableDeleteByExamplefalse/ /context逻辑说明targetRuntime选 MyBatis3 会生成带 Example 的查询类但进销存里复杂查询多Example 反而累赘所以把三个enableXxxByExample关掉只留基础 CRUD。tableName一次只写一张表生成完再改下一张避免误覆盖已有代码。参数上serverTimezone必须加否则 MySQL 8 连接会报时区错误这是java 环境配置里最常翻车的一处。生成后检查实体类字段类型stock是 intpurchase_price是 BigDecimalcreate_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, ListPurchaseItem 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 里写一条原子 SQLupdate idincreaseStock UPDATE goods SET stock stock #{quantity} WHERE id #{goodsId} /update参数说明#{quantity}是本次入库数量正数。这条 SQL 依赖数据库行锁并发时后一个事务会等前一个提交保证不丢。代价是高并发下会排队但超市后台并发量低完全够用。3.2 销售出库库存不足的判断放在哪一层销售出库比入库多一个约束库存不能为负。判断库存够不够不能只在 Java 里查一次就扣因为查和扣之间可能被其他事务插队。稳妥写法是在扣减 SQL 里带上条件update iddecreaseStock 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_type1 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.batWindows或setclasspath.shLinux在开头显式设置# Linux 下 setclasspath.sh 开头加 export JAVA_HOME/usr/local/jdk1.8.0_301 export JRE_HOME$JAVA_HOME/jreWindows 同理设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 iddataSource classcom.alibaba.druid.pool.DruidDataSource property nameurl valuejdbc:mysql://localhost:3306/market?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ property nameinitialSize value5/ property namemaxActive value20/ /beanMapper 扫描用MapperScannerConfigurerbasePackage要指到 mapper 接口所在包别指到实体包bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.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 没带characterEncodingutf8、Tomcat 的server.xml里 Connector 没加URIEncodingUTF-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开mapUnderscoreToCamelCasetrue要么在 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 进销存就值得写进简历也经得起面试官追问。希望帮到你。本文还有配套的精品资源点击获取
返回列表