简介:一套基于SSM框架的超市进销存管理系统,面向计算机专业毕业设计或课程设计场景,提供完整的项目源码与部署配置。系统采用Java语言开发,后端由Spring、SpringMVC、MyBatis构成,前端使用Vue组件化页面,涵盖商品、库存、销售、供应商等进销存核心模块,可帮助学习者快速理解前后端分离项目结构并二次开发。压缩包内含717个文件,大小约20.28MB,其中主要包含184个Java源码文件、130个Vue页面组件、44个JavaScript脚本、29个XML配置文件、2个SQL数据库脚本,以及6个Windows批处理启动/构建脚本,辅以SVG矢量图标、图片、Word文档和环境说明,目录分类清晰,方便导入主流IDE后按脚本运行。资源同时注明JDK1.8、MySQL5.7、Maven3.3.9等环境要求,并提供管理员账号密码及前后台入口地址,降低本地部署门槛。目前已有97人学习下载,适合作为Java Web综合项目训练或毕业设计参考。
1. 一套能跑通的SSM进销存,比你想的更吃细节
很多做Java毕业设计的人,拿到一套完整的SSM进销存管理系统源码时,第一反应是“能跑就行”。但真把它解压、导入、改配置、启动之后,才发现坑全在后头:数据库版本不对、Maven仓库缺包、Tomcat部署路径和前端静态资源对不上,任何一个环节都能卡住半天。这套基于Spring + SpringMVC + MyBatis的美特超市进销存管理系统,实际上是一个典型的SSM三层结构项目,前台走Vue打包后的静态页面,后台走admin/dist,业务上覆盖了供应商、商品、进货、销售、库存、退货等核心环节。对它最合理的定位不是“改改就能交差的作业”,而是一个能用来理解SSM整合、MyBatis动态SQL、订单库存事务控制的现成样本。适合正在做Java毕业设计、课设,或者刚接触传统Web开发想看看真实项目怎么组织代码的人。
2. SSM框架整合与项目目录结构拆解
2.1 三大框架在项目里各自干了什么
SSM不是一个新框架,而是Spring、SpringMVC、MyBatis三个框架的组合方案。在这个进销存项目里,分工非常明确:Spring负责管理业务层对象和事务,SpringMVC负责接收HTTP请求并路由到Controller,MyBatis负责把Java方法和SQL映射起来。理解了这个分工,再去看源码时就不会在主配置文件里迷路。
常见的整合方式中,Spring的配置文件会扫描com.controller之外的包,而SpringMVC的配置文件只扫描Controller层。代码里一般有两个XML:applicationContext.xml负责数据源、事务、MyBatis的SqlSessionFactory,spring-mvc.xml负责开启注解驱动、视图解析器和静态资源放行。初学者容易犯的错误是让两个容器都扫描Service,导致事务失效或者Bean重复实例化。在美特超市这套源码里,建议直接沿用原项目的分包方式,不要随意加注解扫描路径。
2.2 核心配置文件清单与改动点
拿到解压后的项目,第一件事不是急着启动,而是把配置文件的依赖关系理清。常见的关键配置如下:
| 文件名 | 作用 | 必须改的点 |
|---|---|---|
jdbc.properties | 数据库连接参数 | 数据库名、用户名、密码 |
applicationContext.xml | Spring核心配置 | 数据源、mapper扫描路径 |
spring-mvc.xml | Web层配置 | 静态资源路径、注解驱动 |
mybatis-config.xml | MyBatis全局配置 | 下划线转驼峰、日志 |
pom.xml | Maven依赖管理 | JDK版本、Tomcat插件端口 |
jdbc.properties里最容易被忽略的是MySQL连接驱动的URL参数。MySQL 5.7和8.x的驱动类名不同,驱动版本也直接影响是否报Public Key Retrieval is not allowed。项目要求用MySQL 5.7,那driverClassName=com.mysql.jdbc.Driver没问题,同时jdbc:mysql://localhost:3306/meite?useUnicode=true&characterEncoding=utf8要留意数据库名要和建表SQL里的库名一致。
2.3 从启动脚本看项目构建流程
解压后的文件列表里出现了run.bat、2-run.bat、build.bat、3-build.bat,这说明项目不只是给你一个war包,而是希望你通过Maven直接构建。build.bat里一般是先执行mvn clean package再操作target目录,run.bat则可能直接调用mvn spring-boot:run或者执行java -jar。这里有一个容易被忽略的细节:如果项目用的是tomcat7插件,那么run.bat里写的可能是tomcat7:run。
我一般会建议先手动执行一次mvn clean install -DskipTests,确认依赖能下载完,再跑脚本。因为批处理脚本里往往没有打印详细的Maven日志,一旦失败只会弹出一个快速闪过的窗口。把构建过程拆开做,能更快定位是网络问题还是代码编译问题。
2.4 MyBatis映射文件的组织方式
这套系统的DAO层和Mapper XML通常放在com.meite.mapper和mapper/目录下。MyBatis的Mapper接口和XML文件必须保持同名且包路径一致,否则启动时会报Invalid bound statement (not found)。进销存业务里,多表联查的场景很多,比如查询商品列表时要关联分类表和供应商表,所以很多Mapper里用上了<resultMap>自定义映射。
在MyBatis里,开启驼峰映射是很实用的配置,放在mybatis-config.xml里:
<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>mapUnderscoreToCamelCase的作用是把数据库里的product_name自动映射为Java对象里的productName,这样就不用在每个<resultMap>里手工写列名和属性的对应关系。logImpl用标准输出日志,调试SQL时直接在控制台看输出,省得再配Log4j2。这里的逻辑是:当数据库字段命名习惯是下划线、Java属性命名习惯是驼峰时,这个开关能省掉大量冗余映射代码。
3. 进销存数据库设计:库存是怎么算出来的
3.1 核心表结构与表关系
进销存系统的核心不是用户表,而是围绕商品库存构建的几张业务表。美特超市管理系统里至少会有商品表、供应商表、进货入库表、销售出库表、库存表,另外还可能有进货退货表和销售退货表。商品表存的是静态信息,比如名称、条码、单位、进价、售价;库存表存的是当前剩余数量;而入库单和出库单则记录了每一次数量变动的流水。
库存数量不建议直接存在商品表里,因为一旦发生退货或修改订单,直接修改商品表里的数量字段会导致历史数据丢失。常规做法是库存表独立存在,当前库存通过入库存量减去出库存量来维护,或者保存一个冗余字段来提高查询速度。这套系统应该选择了更实用的折中方案:库存表里有stock_num字段,每次入库、出库、退货都同步更新这个字段,同时保留流水记录以便对账。
3.2 建表SQL的关键片段
参考进销存业务的设计惯例,商品表、入库单表的建表语句大致如下:
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_name` varchar(100) NOT NULL, `category_id` int(11) DEFAULT NULL, `supplier_id` int(11) DEFAULT NULL, `purchase_price` decimal(10,2) DEFAULT NULL, `sale_price` decimal(10,2) DEFAULT NULL, `unit` varchar(20) DEFAULT NULL, `status` tinyint(4) DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `stock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) DEFAULT NULL, `stock_num` int(11) DEFAULT '0', `warning_line` int(11) DEFAULT '10', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;purchase_price和sale_price用decimal(10,2)而不是float,是为了避免商品金额出现精度丢失。stock表里单独维护一个warning_line字段,作为库存预警阈值,比在商品表里用is_warning这种布尔字段要灵活很多,因为不同商品的库存警戒线可能完全不同。注意在一些库存表设计中,还会加入last_update_time字段,用来在并发修改时做乐观锁判断。
3.3 多表联查时的VO与DTO设计
直接操作实体类在进销存查询里很别扭。比如查询商品列表,页面要显示的列包括商品名、分类名、供应商名、当前库存,而Product对象里只有category_id和supplier_id,没有名称。常见的做法是额外定义一个ProductVO,在Mapper里直接多表联查映射过去。这样前端拿到的JSON结构是扁平的,不用在Controller里再循环补字段。
在MyBatis的XML里,多表查询时要注意列名冲突。比如product表有id,supplier表也有id,如果直接SELECT *,MyBatis会不知道把id映射到哪个属性。解决办法是给查询列起别名:
SELECT p.id AS product_id, p.product_name, s.id AS supplier_id, s.supplier_name, st.stock_num FROM product p LEFT JOIN supplier s ON p.supplier_id = s.id LEFT JOIN stock st ON p.id = st.product_id WHERE p.status = 1这里的id别名滑块加了前缀,配合mapUnderscoreToCamelCase可以将product_id映射为productId。需要注意的是,LEFT JOIN的顺序会影响查询结果,如果库存表没有对应记录,stock.stock_num会是NULL,所以在Java里要对它做空值判断,或者用COALESCE(st.stock_num, 0)处理。
4. 进销存核心业务实现:入库、出库与库存联动
4.1 进货入库的Service层逻辑
进货入库不是简单执行一条INSERT语句就结束。一次入库操作至少要完成两件事:在入库单表purchase_in里插入一条主单记录,同时更新库存表stock中的stock_num。如果这个库设计得细一点,还会往purchase_in_item里写入每种商品的入库明细。
Service层必须加上事务控制,保证“插入入库单”和“更新库存”要么都成功、要么都失败。在Spring里最直接的方式是用@Transactional:
@Override @Transactional(rollbackFor = Exception.class) public boolean addPurchaseOrder(PurchaseOrderVO orderVO) { PurchaseIn in = new PurchaseIn(); in.setSupplierId(orderVO.getSupplierId()); in.setTotalAmount(orderVO.getTotalAmount()); purchaseInMapper.insert(in); for (PurchaseInItem item : orderVO.getItemList()) { item.setPurchaseInId(in.getId()); purchaseInItemMapper.insert(item); Stock stock = stockMapper.selectByProductId(item.getProductId()); if (stock == null) { stock = new Stock(); stock.setProductId(item.getProductId()); stock.setStockNum(item.getQuantity()); stock.setWarningLine(10); stockMapper.insert(stock); } else { int newNum = stock.getStockNum() + item.getQuantity(); stock.setStockNum(newNum); stockMapper.updateById(stock); } } return true; }rollbackFor = Exception.class这一句很重要,它告诉Spring任何运行时异常或受检异常都要回滚。很多毕业设计里的坑就是不写这个参数,遇到SQL异常时理解不一致,事务没有回滚,导致库存已经改了但入库单缺失。这里对库存更新做了两个分支:库存记录不存在时新建,存在时做加法。这个逻辑比直接UPDATE stock SET stock_num = stock_num + #{quantity}更稳健,因为后者依赖数据库里已经存在该商品库存记录,而入库第一个商品时通常还没有对应记录的。
4.2 销售出库与库存扣减的并发问题
销售出库对应的逻辑是创建销售单、扣减库存。这里有一个业务规则上的选择:是先判断库存够不够再扣,还是直接扣完再判断结果。对于单机部署的课设项目,先查后扣也能跑通,但一旦将来布置到多人访问环境,就可能出现超卖。MySQL的UPDATE语句是行级锁,利用这个特性可以一次性完成“判断并扣减”:
UPDATE stock SET stock_num = stock_num - #{quantity} WHERE product_id = #{productId} AND stock_num >= #{quantity}这条SQL的执行结果是:如果库存不足,影响行数为0;如果库存足够,库存扣减成功且影响行数为1。Service层根据updateResult == 1判断是否允许继续创建销售单。这种做法本质上是用数据库的行锁来保证并发安全。如果有乐观锁的需求,可以再给stock表加一个version字段,在SQL里加上AND version = #{oldVersion},更新时同时把version加1。
在实际的Java方法里,返回给前端的信息也要匹配。比如库存不足时,不应该抛出500异常,而是返回一个业务状态码:
Map<String, Object> result = new HashMap<>(); if (rows == 0) { result.put("code", 500); result.put("msg", "商品 “" + productName + "” 库存不足"); return result; } result.put("code", 0); result.put("msg", "出库成功");这种设计将业务异常和系统异常区分开,前端拿到code=500时只弹一个提示框,不会影响其他数据渲染。SpringMVC里通常用@RestController或@ResponseBody直接把Map序列化成JSON返回,所以不需要额外再封装统一的Result类。毕业设计里如果追求更规范一点,可以定义Result工具类,但保持轻量即可。
4.3 库存预警与报表查询SQL
库存预警的查询逻辑比较简单,就是找出所有当前库存低于警戒线的商品。但要注意两个细节:一是stock_num为NULL的记录也需要处理,二是关联商品表时要过滤掉已删除状态。用一条SQL就能解决:
SELECT p.id, p.product_name, p.sale_price, IFNULL(s.stock_num, 0) AS stock_num, IFNULL(s.warning_line, 10) AS warning_line FROM product p LEFT JOIN stock s ON p.id = s.product_id WHERE p.status = 1 AND IFNULL(s.stock_num, 0) < IFNULL(s.warning_line, 10)这里用IFNULL把NULL库存当作0处理,避免漏掉那些还没来得及建库存记录的商品。页面上做预警展示时,还可以把差值算出来:warning_line - stock_num就是还需要补货的数量。报表查询往往是进销存系统里最容易出现慢查询的地方,常见优化策略是在product_id和purchase_time上建联合索引。如果数据量超过十万条,还应该考虑按月分表,但在毕业设计场景中不需要过度设计。
4.4 Controller层接收参数与返回JSON
SpringMVC里接收前端Vue传来的JSON对象,常见写法是用@RequestBody绑定PurchaseOrderVO。注意PurchaseOrderVO内部包含一个List<PurchaseInItem>的子列表,那么前端Vue里提交的数据结构必须与之对应,比如{"supplierId":1,"totalAmount":100,"itemList":[{"productId":1,"quantity":20}]}。如果字段名对不上,Spring解析时会直接报400或者得到null。
@PostMapping("/purchase/add") @ResponseBody public Map<String, Object> addPurchase(@RequestBody PurchaseOrderVO orderVO) { boolean ok = purchaseService.addPurchaseOrder(orderVO); Map<String, Object> result = new HashMap<>(); result.put("code", ok ? 0 : 500); result.put("msg", ok ? "添加成功" : "添加失败"); return result; }这种写法把Controller层做得非常薄,所有业务操作都委托给Service,Controller只负责接收参数和返回结果。@PostMapping限定只能POST请求,避免前端用GET请求传递订单数据。这里有一个小坑:前端表单里如果有日期字符串,比如"2025-03-12 10:00:00",后端VO用Date类型接收时会因为格式不对报400。建议在后端添加@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,或者让前端直接传时间戳。
5. 部署验证与两个提高印象分的技巧
5.1 用一套固定环境变量避免“在我电脑上能跑”
这个项目要求的JDK1.8、Tomcat7、MySQL5.7、Maven3.3.9,是很多SSM老项目的标准环境。建议把Maven的settings.xml里的本地仓库路径固定为D:\maven-repo,同时把JDK编译级别设置成1.8。pom.xml里需要确认<maven.compiler.source>和<maven.compiler.target>的值是1.8,否则在JDK17环境上编译时会提示无效的目标发行版。另一个容易出错的地方是Tomcat的URL编码,如果请求参数带中文,需要在Tomcat的server.xml里加一行:
<Connector URIEncoding="UTF-8" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />URIEncoding="UTF-8"负责处理URL路径和查询字符串中的中文编码。如果不加,前端搜索商品名称时传中文会直接乱码,而且这种乱码在浏览器控制台看不到,只能从后端日志里发现参数值变成了???。这也是验证接口时最值得先检查的一项:在后端Controller里打日志,确认接收到的中文参数是否正确。
5.2 验证事务是否真正生效
很多毕业设计做完后,答辨老师都会问“你这里事务控制了吗”。面对这个问题,光靠嘴说不行,要现场演示。最简单的验证方式是故意在Service里制造一个异常:在更新库存之后抛一个RuntimeException,然后调用入库接口。如果数据库里的库存没有变化,说明事务生效;如果库存变了但入库单没有,说明事务没管用。
常见的原因是Spring事务没有生效,比如@Transactional加在私有方法上,或者同类内部调用。Spring的代理机制决定了@Transactional只对外部调用生效。这段代码里,Service实现类的addPurchaseOrder不能直接内部调用本类的另一个带事务保护的私有方法,否则事务不会开启。验证时要在实现类上显式写@Service,并且从Controller注入Service接口,而不是自己new一个实现类。
5.3 给前端页面增加一个当前库存列
如果想让这个系统在课设演示时看起来比原始版本更完整,可以给商品列表页面加一个“当前库存”的展示列。前端页面是基于Vue的dist编译后的文件,直接改编译产物比较麻烦,更合理的做法是找到源码目录重新构建。但很多人拿到的源码里没有前端源码,这时可以用一个折中技巧:在后端Controller返回商品列表时,用自定义VO把库存字段合并进去,让JSON里直接包含stockNum,前端页面里如果原本就有展示库存的DOM元素但字段名不对,可以悄悄改成对得上。
经过通配符处理后会提高代码检索命中率,比如在SQL里用LIKE '%特%'这类搜索条件,然后确保VO序列化时包含库存信息的字段名与前端一致。如果前端页面是直接基于这个后端的字典映射的,那修改VO之后,刷新页面就能看到库存数据。这个方法不需要重新编译前端,只需要重新打包后端项目。
启动服务后,访问路径遵循原说明:后台管理入口是localhost:8080/项目名/admin/dist/index.html,前台是localhost:8080/项目名/front/dist/index.html。默认管理员账号和密码都是admin,登录后重点检查商品入库、销售出库、库存预警三个菜单,因为这三个页面几乎覆盖了SSM开发里的增删改查、事务和联合查询,也是答辩时最容易被问业务逻辑的地方。
本文还有配套的精品资源,点击获取