简介:这是一套基于Java SSM框架开发的美特超市进销存管理系统完整源码,面向计算机专业学生用于毕业设计或课程设计,也可供Java Web初学者练习企业级项目开发。系统采用Spring+SpringMVC+MyBatis架构,运行于JDK1.8与Tomcat7环境,数据库使用MySQL5.7,配套Maven构建与Navicat管理工具,管理员默认账号密码均为admin,前后台入口清晰,便于快速部署与二次开发。压缩包共717个文件,约20.28MB,其中184个Java源文件承载核心业务逻辑,130个Vue组件与44个JS文件构成前端交互层,另有29个XML配置、17个CSS样式及SQL脚本、图片等资源,结构完整、分层明确。目前已有98人学习关注。读者可获得一套可直接运行的进销存项目源码,涵盖商品、库存、订单等模块实现,并附环境配置安装包与前后台访问路径说明,适合作为毕设参考、课设模板或SSM框架实战练手素材。
1. 从一份 SSM 超市进销存源码说起:它到底能解决什么问题
很多做 Java 课程设计或小团队内部工具的朋友,都会碰到同一个场景:手头有一份ssm-91crh.zip这样的压缩包,标题写着「基于 Java 美特超市进销存管理系统」,但打开之后面对一堆controller、service、mapper和jsp,不知道从哪下手,更不知道这套东西能不能直接改成自己门店能用的版本。进销存管理系统这个词听起来很大,落到超市这个场景其实就三件事:进货、卖货、盘库存。SSM(Spring + SpringMVC + MyBatis)是这套系统最常见的 Java 技术底座,也是很多 Java 工程师面试题里绕不开的组合。这篇文章不假装看过那份源码包,而是按这个标题背后最典型、最可靠的落地路径,把「这套系统怎么跑起来、表怎么设计、库存怎么扣、坑在哪」讲清楚,让你拿到任何一份同类 SSM 进销存项目都能照着复现。
2. 美特超市进销存的核心业务与 SSM 分层落地
2.1 先想清楚超市进销存到底要管哪几张表
不管源码包里的表设计成什么样,一个能用的超市进销存,数据库层面至少要覆盖五类实体:商品、供应商、采购单、销售单、库存流水。很多同学一上来就去看 Java 代码,结果被Service层的业务逻辑绕晕,其实先把表关系理顺,后面看代码就是顺藤摸瓜。
我一般会先画一张最小表结构,字段不用多,但关系要闭合:
| 表名 | 作用 | 关键字段 | 与其他表的关系 |
|---|---|---|---|
product | 商品档案 | id, name, barcode, category_id, purchase_price, sale_price | 被采购明细、销售明细引用 |
supplier | 供应商 | id, name, contact, phone | 被采购单引用 |
purchase_order | 采购单主表 | id, supplier_id, order_no, total_amount, status, create_time | 一对多关联采购明细 |
purchase_item | 采购明细 | id, order_id, product_id, quantity, price | 关联商品和采购单 |
sale_order | 销售单主表 | id, order_no, total_amount, pay_type, create_time | 一对多关联销售明细 |
sale_item | 销售明细 | id, order_id, product_id, quantity, price | 关联商品和销售单 |
stock | 库存 | id, product_id, quantity, warn_threshold | 与商品一对一 |
stock_record | 库存流水 | id, product_id, change_type, change_qty, before_qty, after_qty, ref_order | 记录每次进出 |
这张表里最容易被忽略的是stock_record。很多课程设计只更新stock表的数量,不记流水,结果库存对不上时完全没有后悔药,查都不知道从哪查。血泪经验:库存流水表不是可选项,是排查问题的黑匣子。
2.2 SSM 三层怎么分工,别把业务写进 Controller
SSM 的分层逻辑很清晰,但新手最容易犯的错是把库存扣减、金额计算全塞进 Controller。正确的分工是:
- Controller:只负责接收参数、校验非空、调用 Service、返回视图或 JSON。
- Service:业务核心,包括下单时扣库存、计算总价、写流水、事务控制。
- Mapper/DAO:只做单表或简单关联查询,不写业务判断。
下面是一个典型的采购入库 Service 方法,用 Spring 的@Transactional保证「更新库存 + 写流水 + 改采购单状态」三件事要么全成,要么全回滚:
@Service public class PurchaseServiceImpl implements PurchaseService { @Autowired private StockMapper stockMapper; @Autowired private StockRecordMapper stockRecordMapper; @Autowired private PurchaseOrderMapper purchaseOrderMapper; @Override @Transactional(rollbackFor = Exception.class) public void confirmPurchase(Long orderId) { // 1. 查出采购单及其明细 PurchaseOrder order = purchaseOrderMapper.selectById(orderId); if (order == null || !"PENDING".equals(order.getStatus())) { throw new BizException("采购单不存在或状态不允许入库"); } List<PurchaseItem> items = purchaseOrderMapper.selectItemsByOrderId(orderId); for (PurchaseItem item : items) { // 2. 锁定库存行,避免并发扣减(常见做法:select ... for update) Stock stock = stockMapper.selectForUpdate(item.getProductId()); int before = stock.getQuantity(); int after = before + item.getQuantity(); // 3. 更新库存 stock.setQuantity(after); stockMapper.updateById(stock); // 4. 写库存流水 StockRecord record = new StockRecord(); record.setProductId(item.getProductId()); record.setChangeType("PURCHASE_IN"); record.setChangeQty(item.getQuantity()); record.setBeforeQty(before); record.setAfterQty(after); record.setRefOrder(order.getOrderNo()); record.setCreateTime(new Date()); stockRecordMapper.insert(record); } // 5. 更新采购单状态 order.setStatus("CONFIRMED"); purchaseOrderMapper.updateById(order); } }这段代码有三个参数点值得注意:@Transactional(rollbackFor = Exception.class)必须加rollbackFor,否则遇到非运行时异常不会回滚;selectForUpdate是行级锁,防止两个采购单同时入库把库存覆盖;changeType用枚举字符串而不是数字,后期查流水时一眼能看懂。
2.3 用 MyBatis 映射 XML 还是注解,进销存场景怎么选
热搜里常出现「mybatisplus 根据 java 实体类生成创建表的 sql 语句」,说明很多人关心实体类和表结构的同步。在 SSM 进销存里,我一般这样选:
- 单表 CRUD:用 MyBatis-Plus 的
BaseMapper,省掉大量 XML。 - 多表关联查询(如采购单带明细、销售统计):写 XML,因为动态 SQL 和
resultMap更可控。 - 库存扣减:手写
update stock set quantity = quantity - #{qty} where product_id = #{id} and quantity >= #{qty},用数据库行锁保证不超卖。
下面是一个销售出库的 Mapper XML 片段,重点是quantity >= #{qty}这个条件,它让库存不足时更新影响行数为 0,Service 层据此抛异常:
<update id="deductStock"> UPDATE stock SET quantity = quantity - #{qty} WHERE product_id = #{productId} AND quantity >= #{qty} </update>逻辑说明:这条 SQL 把「判断库存是否足够」和「扣减」合并成一次原子操作,避免先查后扣的并发漏洞。参数productId和qty由 Service 传入,返回的affectedRows为 0 就说明库存不够,直接回滚事务。
3. 把 SSM 进销存跑起来的最小步骤与参数配置
3.1 环境配置:JDK、Tomcat、MySQL 的版本对齐
热搜里「java 环境配置」「2012 服务器 jdk 怎么配置 java」「java 环境变量使用多个 jdk」都是高频问题。SSM 项目对版本比较敏感,我一般用这套组合:
- JDK 1.8(SSM 老项目用 JDK 8 最稳,JDK 11 以上容易碰到
javax包缺失) - Tomcat 8.5 或 9.0
- MySQL 5.7 或 8.0
- Maven 3.6+
配置环境变量时,JAVA_HOME指向 JDK 根目录,Path里加%JAVA_HOME%\bin。如果机器上有多个 JDK,改JAVA_HOME后一定要新开命令行窗口,否则java -version还是旧的。这个坑我踩过不止一次。
3.2 数据库初始化与连接池参数
拿到源码包后,先找src/main/resources下的jdbc.properties或applicationContext.xml,把数据库地址、用户名、密码改成自己的。然后执行建表 SQL。如果源码里没有 SQL 文件,就按第 2 章的表格自己建。
连接池用 Druid 的话,几个关键参数:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/market_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_password druid.initialSize=5 druid.minIdle=5 druid.maxActive=20 druid.maxWait=60000 druid.validationQuery=SELECT 1serverTimezone必须加,否则 MySQL 8.0 会报时区错误。maxActive不要设太大,超市内部系统并发不高,20 足够,设大了反而容易把数据库连接占满。
3.3 启动与验证:从登录到完成一笔采购入库
启动 Tomcat 后,访问登录页,用管理员账号进入。验证路径建议按这个顺序走:
- 新增一个供应商。
- 新增一个商品,设置采购价和销售价。
- 创建采购单,选择供应商和商品,填写数量。
- 确认入库,然后去库存页面看数量是否增加。
- 查库存流水,确认有一条
PURCHASE_IN记录。
如果第 4 步库存没变,先看事务有没有提交,再看stock表的product_id是否和采购明细里的一致。常见翻车点是商品建档时id自增,但采购明细里存的是商品编码,两边对不上。
4. 库存扣减与并发:进销存最容易翻车的地方
4.1 超卖是怎么发生的,怎么用 SQL 条件挡住
超市做促销时,同一商品可能被多个收银台同时下单。如果代码写成「先查库存,再判断,再扣减」,两个线程都查到库存为 1,都判断通过,最后扣成 -1,这就是超卖。
解决办法就是把判断和扣减合并成一条 SQL,如第 2 章所示。Service 层拿到affectedRows == 0就抛异常回滚。这个方案不需要分布式锁,单库单表足够用。
4.2 事务传播行为选 REQUIRED 还是 REQUIRES_NEW
采购入库和销售出库都用默认的REQUIRED,也就是加入当前事务。但库存流水记录如果希望「即使主业务回滚也要留痕」,可以用REQUIRES_NEW开新事务。不过大多数进销存场景不需要这么复杂,流水和业务一起回滚反而更干净。我一般保持REQUIRED,除非有审计硬要求。
4.3 库存预警阈值怎么设才不鸡肋
stock表里的warn_threshold不要所有商品都设成同一个值。快消品周转快,阈值可以设高一点;大宗商品周转慢,阈值设低一点。常见做法是在商品档案里加一个「安全库存天数」,用日均销量乘以天数动态算阈值。课程设计里如果没这个字段,至少让管理员能手动改每个商品的阈值。
5. 避坑与排查:SSM 进销存上线前必须过的五道坎
5.1 现象:启动报ClassNotFoundException: javax.servlet.ServletException
原因:Tomcat 10 以上把javax.servlet改成了jakarta.servlet,而 SSM 老项目依赖的是javax。 解决:换回 Tomcat 9,或者在pom.xml里把 servlet-api 版本降到 3.1.0 并确认 scope 是provided。
5.2 现象:中文商品名存进数据库变成问号
原因:数据库字符集不是utf8mb4,或者 JDBC URL 没加characterEncoding=utf8。 解决:建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,JDBC URL 补上useUnicode=true&characterEncoding=utf8。
5.3 现象:采购单确认后库存加了,但流水表没记录
原因:Service 方法里先更新库存,再写流水,但写流水时抛了异常被 catch 吞掉,事务没回滚。 解决:不要在事务方法里吞异常。要么让异常抛出去触发回滚,要么用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。
5.4 现象:销售出库时库存足够却提示不足
原因:stock表里同一商品有多条记录(比如初始化时重复插入),update ... where product_id = ?只更新了其中一条,另一条没变,查询时取到的是没变的那条。 解决:给stock表的product_id加唯一索引,初始化时用insert ... on duplicate key update保证一条商品一条库存记录。
5.5 现象:页面报OutOfMemoryError: Java heap space
原因:进销存报表查询一次性把几万条流水加载到内存,或者 Tomcat 默认堆太小。 解决:报表分页查询,别用select *全量拉;启动脚本里加-Xms512m -Xmx1024m。热搜里「进程堆大小调整为 8000 还是报错」也是同类问题,堆不是越大越好,要看物理内存和 GC 表现。
6. 进阶:把进销存做成能长期用的系统,而不是交完就扔的作业
课程设计交完就扔很常见,但这套 SSM 进销存稍微改改就能给小型超市用。我一般会做三件事让它「活」起来。
第一,加操作日志。用 Spring AOP 拦截 Service 层的增删改方法,把操作人、时间、方法名、参数写进operation_log表。这样谁改了价格、谁删了商品,一查就知道。AOP 切点表达式用@annotation(com.market.annotation.Log)这种自定义注解,比包名匹配更可控。
第二,把库存流水做成对账依据。每天营业结束,用一条 SQL 核对:期初库存 + 采购入库 - 销售出库 = 期末库存。如果对不上,就按stock_record表逐条排查。这个习惯能帮你提前发现很多数据问题。
SELECT p.name, s.quantity AS current_qty, COALESCE(SUM(CASE WHEN r.change_type = 'PURCHASE_IN' THEN r.change_qty END), 0) AS total_in, COALESCE(SUM(CASE WHEN r.change_type = 'SALE_OUT' THEN r.change_qty END), 0) AS total_out FROM product p JOIN stock s ON s.product_id = p.id LEFT JOIN stock_record r ON r.product_id = p.id GROUP BY p.id, p.name, s.quantity HAVING s.quantity <> COALESCE(SUM(CASE WHEN r.change_type = 'PURCHASE_IN' THEN r.change_qty END), 0) - COALESCE(SUM(CASE WHEN r.change_type = 'SALE_OUT' THEN r.change_qty END), 0);这条 SQL 查出来的是「库存数量和流水对不上」的商品,空结果说明账实相符。
第三,别把 JSP 写得太重。如果后期想改成前后端分离,Controller 返回 JSON 即可,Service 和 Mapper 不用动。SSM 的价值就在于分层清晰,改起来不伤筋动骨。
我自己维护这类系统最大的教训是:表结构设计时多花一小时,后面少熬三个晚上。库存流水、操作日志、唯一索引,这三样东西在交作业时看不出差别,真用起来就是救命稻草。希望帮到你。
本文还有配套的精品资源,点击获取