
简介这是一套基于SSM框架的Java美特超市进销存管理系统完整源码面向计算机专业学生与Java初学者可用于毕业设计、课程设计或企业级项目练手。系统采用SpringSpringMVCMyBatis架构运行于JDK1.8与Tomcat7环境数据库使用MySQL5.7配套Maven3.3.9构建管理员默认账号密码均为admin前后台入口清晰便于快速部署与二次开发。压缩包共717个文件约20.28MB其中184个Java源文件承载核心业务逻辑130个Vue组件与44个JS文件构建前端交互另有162个SVG、55张JPG及34个PNG提供界面素材29个XML与2个SQL脚本支撑配置与建库整体结构完整、层次分明。目前已有98人学习关注适合需要完整进销存业务闭环、希望理解SSM分层设计与前后端分离实现思路的读者参考借鉴。1. 从一份 SSM 超市进销存源码说起Java 课设怎么做出生产味很多 Java 学习者手里都攥着一份「基于 SSM 的 XX 管理系统」的课程设计源码美特超市进销存管理系统就是其中典型的一类。它表面上是「增删改查 登录」但真正拉开差距的地方在于进销存这个业务本身对数据一致性、库存扣减、单据流转有硬要求不是随便堆几个 Controller 就能糊弄过去的。我见过太多人把这类项目跑起来就交差结果答辩时被问一句「并发下库存扣成负数怎么办」直接卡壳。这篇笔记就顺着这份 SSM 进销存源码把 Java 课设怎么做出生产味讲清楚——从环境搭建、分层设计、库存扣减的坑到怎么用一套可复现的步骤把它跑通并讲明白。适合正在做 Java 课程设计、想拿 SSM 练手、或者准备把课设写进简历的在校生和转行者。SSM 框架虽然老但它是理解 Java Web 分层、事务、MyBatis 映射最好的练手场比一上来啃 Spring Boot 自动配置要扎实得多。2. SSM 进销存的分层骨架为什么这么拆拆错了会怎样2.1 进销存业务到底需要哪几张核心表进销存三个字拆开就是进货、销售、库存。落到数据库上最少需要这几张表才能把业务闭环跑起来商品表product、供应商表supplier、客户表customer、进货单主表与明细表purchase_order / purchase_order_item、销售单主表与明细表sale_order / sale_order_item、库存表stock、以及用户与角色表sys_user / sys_role。很多人图省事把进货和销售塞进一张 order 表用 type 字段区分短期能跑但一旦要做「进货退货」「销售退货」「调拨」就彻底乱套。我一般会坚持主表 明细表的结构主表存单据头单号、供应商、总金额、状态、创建时间明细表存每一行商品商品 ID、数量、单价、小计。库存表单独存在不和商品表混在一起因为库存是会随单据变动的动态数据商品表是相对静态的基础数据混在一起后期做库存流水会非常痛苦。下面这张表是我做这类项目时固定的核心表清单字段只列关键项表名关键字段作用productid, name, category_id, unit, purchase_price, sale_price商品基础信息stockid, product_id, quantity, warn_quantity, update_time实时库存与预警线purchase_orderid, order_no, supplier_id, total_amount, status, create_time进货单头purchase_order_itemid, order_id, product_id, quantity, price进货明细sale_orderid, order_no, customer_id, total_amount, status, create_time销售单头sale_order_itemid, order_id, product_id, quantity, price销售明细stock_recordid, product_id, change_type, change_quantity, ref_order_no库存流水stock_record 这张流水表是很多人会漏掉的但它恰恰是排查库存对不上的后悔药。只要每次库存变动都往这张表插一条记录后面发现库存数字不对顺着流水一查就知道是哪张单据出的问题。2.2 SSM 三层怎么分Controller 别写业务Service 别碰 HttpServletRequestSSM 的分层是 Controller → Service → Mapper这个大家都知道但实际写的时候最容易犯两个错。第一个错是把业务逻辑写在 Controller 里比如在进货的 Controller 方法里直接算总金额、直接调 Mapper 扣库存。这样写的后果是事务加不上因为 Spring 的声明式事务默认只对 Service 层方法生效而且一旦要复用这段逻辑比如定时任务也要进货就得复制粘贴。第二个错是在 Service 里注入 HttpServletRequest 去拿 session 里的用户 ID这会让 Service 和 Web 容器绑死单元测试根本没法跑。正确做法是把用户 ID 作为参数从 Controller 传进 Service。一个标准的进货 Service 方法大概长这样Service public class PurchaseServiceImpl implements PurchaseService { Autowired private PurchaseOrderMapper orderMapper; Autowired private StockMapper stockMapper; Autowired private StockRecordMapper stockRecordMapper; // 进货入库插单据 加库存 记流水三步必须在一个事务里 Override Transactional(rollbackFor Exception.class) public void createPurchase(PurchaseOrder order, ListPurchaseOrderItem items, Long operatorId) { // 1. 生成单号并写入单据头 order.setOrderNo(generateOrderNo(PO)); order.setStatus(1); // 1 表示已入库 order.setCreateBy(operatorId); orderMapper.insert(order); // 2. 逐行写明细同时累加库存 for (PurchaseOrderItem item : items) { item.setOrderId(order.getId()); orderMapper.insertItem(item); // 库存存在则更新不存在则初始化 int updated stockMapper.increaseStock(item.getProductId(), item.getQuantity()); if (updated 0) { stockMapper.initStock(item.getProductId(), item.getQuantity()); } // 3. 记一条库存流水方便对账 stockRecordMapper.insert(new StockRecord(item.getProductId(), PURCHASE_IN, item.getQuantity(), order.getOrderNo())); } } }这段代码有三个关键点。第一Transactional(rollbackFor Exception.class)里的 rollbackFor 必须写因为 Spring 默认只对 RuntimeException 回滚如果哪天抛了个受检异常事务不回滚单据插了库存没加数据就烂了。第二increaseStock返回影响行数为 0 说明库存记录还不存在需要初始化这个判断不能省否则新商品第一次进货会静默失败。第三流水记录一定要在同一个事务里写不能异步否则事务回滚了流水还在对账时更乱。2.3 MyBatis 映射文件里最容易写错的三个地方MyBatis 的 XML 映射是 SSM 项目里 bug 的高发区。第一个坑是 resultMap 和 resultType 混用。如果数据库字段是product_id实体类是productId而你用了 resultType那 productId 永远是 null因为 MyBatis 默认不会自动做下划线转驼峰除非你在配置文件里开了mapUnderscoreToCamelCasetrue。我一般直接开这个配置省得每个字段都写 resultMap。第二个坑是#{}和${}用混。#{}是预编译占位符能防 SQL 注入${}是字符串拼接只有动态表名、动态排序字段这种场景才用而且必须在 Java 层做白名单校验。第三个坑是批量插入没写对进销存的明细经常要批量插正确写法是用foreachinsert idbatchInsertItems parameterTypejava.util.List INSERT INTO purchase_order_item (order_id, product_id, quantity, price) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.productId}, #{item.quantity}, #{item.price}) /foreach /insert注意 collection 的值要和 Mapper 接口里Param指定的名字一致不写Param的话单参数 List 默认叫 list多参数就得显式命名否则报Parameter list not found这个报错新手能查半天。3. 把项目在本地跑起来环境、配置、启动的完整命令3.1 JDK、Tomcat、MySQL 的版本选择与安装SSM 项目对版本比较敏感选错了启动就报错。我一般用 JDK 8项目里如果看到javax.*包就是 JDK 8 时代的别硬上 JDK 17会报「源发行版 17 需要目标发行版 17」这类编译错误、Tomcat 8.5 或 9、MySQL 5.7 或 8.0。MySQL 8 要注意驱动包用com.mysql.cj.jdbc.DriverURL 后面加serverTimezoneAsia/Shanghai否则时区报错。安装完 MySQL 后建库建表# 登录 MySQL mysql -u root -p # 建库字符集用 utf8mb4 支持 emoji 和生僻字 CREATE DATABASE met_store DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入项目自带的 sql 脚本假设脚本在项目根目录 db 文件夹下 USE met_store; SOURCE /path/to/project/db/met_store.sql;导入脚本后一定要SHOW TABLES;确认表都建出来了有时候脚本里有外键约束导入顺序不对会中途失败但 MySQL 默认不报错继续执行结果就是少几张表启动时报「Table doesnt exist」。3.2 数据库连接与 MyBatis 配置怎么改打开src/main/resources下的jdbc.properties有的项目叫db.properties改这几项jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/met_store?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码参数说明useUnicodetruecharacterEncodingutf8保证中文不乱码serverTimezone解决 MySQL 8 的时区异常useSSLfalse避免本地连接时的 SSL 警告。改完检查applicationContext.xml里有没有正确加载这个 properties 文件常见写法是context:property-placeholder locationclasspath:jdbc.properties/如果这行漏了占位符${jdbc.url}不会被替换启动直接报无法解析。3.3 用 Maven 打包并部署到 Tomcat确认 pom.xml 里的依赖都下载完整后执行打包# 在项目根目录执行跳过测试加快速度 mvn clean package -DskipTests # 打包成功后 target 目录下会生成 war 包 ls target/*.war把 war 包丢进 Tomcat 的webapps目录启动 Tomcat# Linux/Mac sh $TOMCAT_HOME/bin/startup.sh # Windows %TOMCAT_HOME%\bin\startup.bat # 实时看启动日志确认没有报错 tail -f $TOMCAT_HOME/logs/catalina.out日志里看到Deployment of web application archive ... has finished in xxx ms就说明部署成功浏览器访问http://localhost:8080/项目名/即可。如果 404先检查 war 包名和访问路径是否一致如果 500看 catalina.out 里的异常栈八成是数据库连不上或 Mapper 没扫描到。4. 库存扣减与数据一致性进销存最容易翻车的地方4.1 并发下库存扣成负数是怎么发生的销售出库时扣库存最朴素的写法是先查再改// 错误示范先查后改并发下必然超卖 Stock stock stockMapper.selectByProductId(productId); if (stock.getQuantity() quantity) { stock.setQuantity(stock.getQuantity() - quantity); stockMapper.updateById(stock); }这段代码在单线程下没问题但两个销售员同时卖同一件商品时两人都查到库存是 10都判断 10 8 成立然后各自扣 8最终库存变成 2实际卖出去 16 件超卖 6 件。这就是典型的「读-改-写」竞态。解决办法有两种悲观锁SELECT ... FOR UPDATE和乐观锁版本号或条件更新。进销存这种写多读少的场景我更推荐用条件更新一条 SQL 搞定update iddecreaseStock UPDATE stock SET quantity quantity - #{quantity}, update_time NOW() WHERE product_id #{productId} AND quantity #{quantity} /updateService 里判断返回的影响行数int rows stockMapper.decreaseStock(productId, quantity); if (rows 0) { throw new BusinessException(库存不足商品ID productId); }这样把「判断库存够不够」和「扣减」合并成一条原子 SQL数据库层面保证不会扣成负数。返回 0 就抛异常配合Transactional整个销售单回滚。4.2 单据状态流转与事务边界怎么定进销存的单据一般有「草稿 → 已提交 → 已入库/已出库 → 已作废」几个状态。状态流转必须和库存变动绑定只有「已入库」才加库存「已作废」要反向冲减。我见过有人把状态改和库存改分成两个接口结果前端调了一个没调另一个库存和单据对不上。正确做法是一个 Service 方法里同时改状态和库存用事务包住。事务边界要划在 Service 方法上不要划在 Controller也不要在 Service 里手动TransactionTemplate嵌套除非你很清楚传播行为。默认的REQUIRED传播级别已经够用外层有事务就加入没有就新建。4.3 用库存流水表做对账与排查前面提到的 stock_record 表在排查时价值极高。每次库存变动都记一条商品 ID、变动类型进货入库 / 销售出库 / 退货入库 / 作废冲减、变动数量正负、关联单号、操作时间。当发现某商品库存对不上时执行-- 查某商品的所有库存变动按时间排序 SELECT change_type, change_quantity, ref_order_no, create_time FROM stock_record WHERE product_id 1001 ORDER BY create_time DESC; -- 用流水汇总和当前库存对比看是否一致 SELECT (SELECT quantity FROM stock WHERE product_id 1001) AS current_stock, (SELECT SUM(change_quantity) FROM stock_record WHERE product_id 1001) AS record_sum;如果 current_stock 和 record_sum 不相等说明有库存变动没记流水或者流水记了但库存没改顺着时间点就能定位到是哪次操作出的问题。这个习惯我从课设一直带到生产项目省了无数对账的加班时间。5. 避坑与排查SSM 进销存项目里最常见的五个坑5.1 启动报 NoClassDefFoundError 或 ClassNotFoundException现象Tomcat 启动时报找不到某个类比如org.springframework.web.context.ContextLoaderListener。原因war 包里没有把依赖打进去或者 Maven 依赖 scope 写成了 provided 但 Tomcat 里没有对应的 jar。解决检查 pom.xml 里 spring-web、spring-context 这些依赖的 scope如果是 provided要么改成 compile要么把 jar 放进 Tomcat 的 lib 目录。另外确认mvn package时没有报依赖下载失败本地仓库里 jar 是完整的。5.2 中文乱码从数据库到页面全链路排查现象商品名称在页面显示成问号或乱码。原因可能出在三个环节——数据库字符集、连接 URL 字符集、页面编码。解决数据库建库时用 utf8mb4JDBC URL 加characterEncodingutf8JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 %web.xml 里配 CharacterEncodingFilter 强制请求和响应都用 UTF-8。四个地方都对了才不会乱码缺一个都可能出问题。5.3 事务不生效库存扣了但单据没插进去现象销售出库时库存减了但销售单没生成或者反过来。原因Transactional没生效。常见原因有三个——方法不是 public 的Spring AOP 对非 public 方法不代理同类内部方法调用this 调用不走代理异常被 catch 了没往外抛。解决确保 Service 方法是 public不要在 Service 内部直接 this 调另一个带事务的方法要么注入自己要么把逻辑抽到另一个 Servicecatch 块里如果要回滚记得TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或重新抛出异常。5.4 MyBatis 报 Invalid bound statement (not found)现象启动不报错一调 Mapper 方法就报Invalid bound statement (not found)。原因Mapper XML 没被扫描到或者 XML 里的 namespace 和 Mapper 接口全限定名不一致或者方法名对不上。解决检查applicationContext.xml里mapperLocations配置的路径是否覆盖了 XML 实际位置检查 XML 的 namespace 是不是接口的全限定名检查select idxxx的 id 和接口方法名是否完全一致大小写敏感。这三个点挨个对一遍基本能解决。5.5 页面 404Controller 映射和视图解析器配置现象访问某个功能页面报 404。原因可能是 Controller 的RequestMapping路径写错或者视图解析器 prefix/suffix 配错导致返回的视图名拼出来的路径不对。解决先看 Tomcat 日志里有没有No mapping found for HTTP request with URI有的话就是路径问题没有的话看视图解析器配置比如prefix/WEB-INF/jsp/、suffix.jsp那 Controller 返回product/list实际找的是/WEB-INF/jsp/product/list.jsp确认这个文件存在。另外注意ResponseBody和返回视图名不能混用加了ResponseBody就不会走视图解析器了。6. 从课设到简历项目怎么把 SSM 进销存讲出技术深度把项目跑通只是第一步真正让它值钱的是你能讲清楚里面的技术决策。面试官问「你这个进销存怎么保证库存不超卖」你要是能答出「用条件更新UPDATE stock SET quantity quantity - ? WHERE product_id ? AND quantity ?靠数据库行锁保证原子性返回影响行数为 0 就抛异常回滚」这比背八股文有说服力得多。再比如问「事务怎么控制的」你可以讲Transactional的 rollbackFor 为什么要写 Exception、同类调用为什么失效、事务边界为什么划在 Service。这些都是你亲手踩过才能讲明白的细节。我一般还会在项目里加一个小的压测验证用 JMeter 或者简单的多线程代码模拟 100 个并发销售请求看最终库存是不是刚好等于初始库存减去总销量。下面这段代码可以直接拿来验证// 并发扣减库存验证100 个线程各扣 1 件初始库存 100最终应为 0 public class StockConcurrencyTest { public static void main(String[] args) throws InterruptedException { int threads 100; CountDownLatch latch new CountDownLatch(threads); ExecutorService pool Executors.newFixedThreadPool(threads); AtomicInteger success new AtomicInteger(0); AtomicInteger fail new AtomicInteger(0); for (int i 0; i threads; i) { pool.submit(() - { try { // 调用销售 Service 的扣库存方法 boolean ok saleService.decreaseStock(1001L, 1); if (ok) success.incrementAndGet(); else fail.incrementAndGet(); } finally { latch.countDown(); } }); } latch.await(); pool.shutdown(); System.out.println(成功 success.get() 失败 fail.get()); // 再去数据库查 stock 表quantity 应该等于 0 } }跑完这个测试如果成功数是 100、库存是 0说明扣减逻辑没问题如果成功数超过 100 或者库存变成负数说明你的条件更新没写对。这个验证过程本身就是很好的面试素材因为它证明你不只是「跑通了」而是「验证过正确性」。最后说个我自己的习惯每做完一个课设项目我都会把踩过的坑和对应的解决方案记在一个 markdown 文件里跟着项目一起提交。下次做类似项目时翻出来看能省掉大量重复排查的时间。SSM 进销存这个方向技术栈虽然不新但业务逻辑的严谨性训练是实打实的把库存一致性、事务边界、单据流转这三件事吃透比多刷十道八股文管用。希望帮到你。本文还有配套的精品资源点击获取