news 2026/10/23 1:53:36

SSM超市进销存系统实战:从源码到上线的核心设计与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM超市进销存系统实战:从源码到上线的核心设计与避坑指南

简介:这是一套基于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 1

serverTimezone必须加,否则 MySQL 8.0 会报时区错误。maxActive不要设太大,超市内部系统并发不高,20 足够,设大了反而容易把数据库连接占满。

3.3 启动与验证:从登录到完成一笔采购入库

启动 Tomcat 后,访问登录页,用管理员账号进入。验证路径建议按这个顺序走:

  1. 新增一个供应商。
  2. 新增一个商品,设置采购价和销售价。
  3. 创建采购单,选择供应商和商品,填写数量。
  4. 确认入库,然后去库存页面看数量是否增加。
  5. 查库存流水,确认有一条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 的价值就在于分层清晰,改起来不伤筋动骨。

我自己维护这类系统最大的教训是:表结构设计时多花一小时,后面少熬三个晚上。库存流水、操作日志、唯一索引,这三样东西在交作业时看不出差别,真用起来就是救命稻草。希望帮到你。

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

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

Revit MCP升级:AI终于能改模型了——能力拆解与实操

最近好几个做 BIM 的朋友私信我同一个问题&#xff1a;Revit MCP 现在到底能不能改模型了&#xff1f;我印象里刚放出消息那阵子&#xff0c;官方版本确实只支持只读操作&#xff0c;大家兴致勃勃接上去之后发现&#xff0c;AI 能把模型信息问得明明白白&#xff0c;却动不了任…

作者头像 李华
网站建设 2026/10/17 6:13:19

把WSL当成日常开发主力:安装配置与高频问题排查指南

1. 为什么我把 WSL 当作日常开发主力&#xff1a;能力边界与适用场景 如果你是一个长期在 Windows 上工作、又离不开 Linux 命令行环境的开发者&#xff0c;那么 WSL&#xff08;Windows Subsystem for Linux&#xff0c;Windows 的 Linux 子系统&#xff09;几乎是最顺手的入场…

作者头像 李华
网站建设 2026/10/17 8:21:27

RabbitMQ灰度发布高吞吐优化:从连接参数到分流策略的完整调优指南

做 RabbitMQ 的灰度发布方案&#xff0c;最容易被忽略的问题通常不是“消息发不出去”&#xff0c;而是“消息吞吐量突然撑不住了”。尤其是 Spring Boot Java 这种技术栈本身就是 MQ 封装层的场景&#xff0c;你会发现自己加了灰度路由、加了分流标签&#xff0c;系统反而在高…

作者头像 李华
网站建设 2026/10/17 0:49:03

Python 3.11升级后SELinux模块缺失:编译与策略双层排查指南

在Anolis OS 8或者CentOS 8上把Python从系统自带的3.6升级到3.11&#xff0c;装完跑起来才发现import selinux直接报ModuleNotFoundError&#xff0c;这种情况我处理过不止一次。尤其是那些要调用libselinux接口去查询文件上下文、做策略判断的脚本&#xff0c;基本卡在这一步就…

作者头像 李华