简介:面向计算机相关专业毕业生准备的Java毕业设计项目,采用SSM框架实现超市管理系统,系统运行在浏览器服务器模式下,后端使用Java与MySQL数据库。资源包中包含完整项目源码、文字说明文档、数据库初始化脚本以及演示视频,覆盖从系统搭建、编码开发到答辩展示的完整链路。系统功能分为前台与后台:前台面向普通用户,提供网站首页、员工信息、商品信息、积分商品等展示与查询;后台面向管理员,支持系统用户管理、商品类别管理、积分商品管理、商品问题管理等多种业务操作,结合录入的演示视频可直观感受各模块运行效果。压缩包内共910个文件,除jsp页面、java源码、class编译文件、jar依赖库、sql脚本、xml配置外,还有大量gif、jpg、png等图片资源用于界面设计参考,整体大小57.01MB,目录按业务分层组织,利于快速定位和学习;内容预览中可见支付宝相关处理类,说明项目包含支付接口模块,值得深入研究。目前已有186人学习,适合毕业设计参考,也适合希望掌握SSM整合开发、B/S结构项目完整流程的Java学习者。
1. 为什么这个SSM超市管理系统,是Java毕业设计里最不该卡住的项目
提起Java毕业设计,基于SSM框架的超市管理系统几乎是模板库里出现频率最高的名字之一。它不像秒杀系统那样要解释高并发,也不像推荐项目那样依赖算法,业务边界非常清楚:商品、库存、供应商、收银、进货退货,任何一个认真学过Java基础的人都能把代码从头读到尾。同时Spring、SpringMVC、MyBatis三个框架全占,答辩时考官问原理你有话可说,问业务你也有场景可讲。可现实是,很多同学拿到源码压缩包之后,第一晚就卡在环境上——JDK版本不对、Maven依赖下不动、MySQL密码对不上、Tomcat启动报错。这篇笔记我就按“先看懂架构、再跑通项目、最后改成自己的作品”的顺序,把SSM超市管理系统拆开讲清楚,适合正在做Java课程设计或者毕业设计、手里正好持有一套这类源码但还没跑通的人。
2. 看懂SSM超市管理系统的架构:先弄明白三件事,再动手改代码
2.1 SSM三个框架分别管哪一层,为什么这个组合适合毕设
SSM是Spring、SpringMVC、MyBatis三个框架的组合,对应Java Web项目里三层架构的三块拼图。Spring管对象和事务:Service层的类由Spring容器创建和注入,声明式事务用@Transactional一行注解就能包住多个数据库操作。SpringMVC管请求分发:浏览器发来的HTTP请求由DispatcherServlet接收,按URL找到对应的Controller方法,再把返回的字符串解析成跳转路径或JSON。MyBatis管数据库访问:写SQL不再用JDBC里那一大段Connection、PreparedStatement、ResultSet的样板代码,只需要定义Mapper接口,在XML里写SQL,框架自动完成参数绑定和结果集映射。
这套组合是目前Java课程设计案例源码里覆盖面最完整的一套。为什么不是Spring Boot?因为SSM要求你手动配置web.xml、spring-mvc.xml、applicationContext.xml、mybatis-config.xml,每一步配置都能被考官追问,答得出来就是“真的懂”,而Spring Boot把很多细节藏起来了,回答“自动配置”反而容易被追问到底。所以毕业设计选SSM,不是为了技术先进,而是为了把Java Web的核心知识点完整暴露出来。如果你的目标是求职,SSM和Spring Boot的区别本身也是Java面试题里的高频考点,做完这个项目你等于把答案背熟了。
2.2 超市业务的功能模块与数据库表设计
超市管理系统在业务上围绕“进销存”三个字展开。进货是跟供应商打交道,销售是跟顾客打交道,库存是两者之间的缓冲。源码里最常见的模块划分是:用户管理(多角色登录)、商品管理(增删改查、上下架)、库存管理(入库、出库、盘点)、销售管理(收银、退货、日结)、供应商管理、进货管理。权限上通常分管理员、收银员、仓库管理员、采购员,管理员拥有全部菜单,收银员只能访问收银和查询,仓库管理员管入库出库。
对应的数据库表,一般包含这样几张核心表:
| 表名 | 关键字段 | 作用 |
|---|---|---|
| sys_user | id, username, password, realname, role | 登录账号与角色 |
| goods_info | id, goods_name, barcode, price, stock, category_id | 商品基本信息与库存 |
| supplier_info | id, supplier_name, contact, phone, address | 供应商档案 |
| stock_in | id, supplier_id, goods_id, quantity, in_price, create_time | 进货入库流水 |
| sale_order | id, order_no, total_amount, create_time, user_id | 销售单头 |
| sale_order_detail | id, order_id, goods_id, price, quantity | 销售明细 |
表之间靠外键串起来:sale_order_detail通过order_id指向sale_order,通过goods_id指向goods_info;stock_in通过supplier_id指向supplier_info。这里要注意,商品库存字段stock直接放在goods_info里是课程设计最常见的做法,优点是查询快、代码好写,缺点是没有库存流水,盘点时说不清每一次变动。如果后面想升级,再补一张goods_stock_log表就行,第一版不用过度设计。
提示:看源码时先打开数据库脚本,把这几张表的关系画在纸上,比先看代码效率高一倍。我就是因为先看代码,绕了很大一圈才搞清楚字段含义。
2.3 一次“进货入库”请求的完整调用链
把SSM的调用链走一遍,是答辩时最值钱的一段话。以新增进货单为例:用户在JSP页面填写商品、数量、进货价,点击提交,表单POST到/stockIn/add。DispatcherServlet先根据URL找到StockInController.add()方法,Controller负责接收参数、调用Service,不写业务逻辑。Service里先往stock_in表插入一条进货记录,再更新goods_info表的库存和最新进货价,两个操作必须在一个事务里,要么都成功,要么都失败。Mapper层由MyBatis生成SQL执行,最后Controller把页面重定向到进货列表,用户看到最新结果。
Controller的典型写法是这样的:
@Controller @RequestMapping("/stockIn") public class StockInController { @Autowired private StockInService stockInService; @RequestMapping("/add") public String add(StockIn stockIn) { boolean ok = stockInService.addStockInWithGoods(stockIn); return ok ? "redirect:/stockIn/list" : "error"; } }关键在@RequestMapping("/add"),它和表单的action路径必须一致;方法参数StockIn stockIn是SpringMVC的自动参数绑定,要求表单字段名和实体类属性名一致,这是新手最容易忽略的一件事——字段名拼错了不报错,只存进去一个null。返回的redirect:/stockIn/list是重定向,刷新页面不会重复提交表单,比直接返回视图名更安全。
Service层的事务写法是另一个重点:
@Service @Transactional public class StockInServiceImpl implements StockInService { @Autowired private StockInDao stockInDao; @Autowired private GoodsDao goodsDao; @Override public boolean addStockInWithGoods(StockIn stockIn) { // 第一步:写入进货单 stockInDao.insert(stockIn); // 第二步:更新商品库存 goodsDao.updateStock(stockIn.getGoodsId(), stockIn.getQuantity()); return true; } }这里的@Transactional是Spring声明式事务,默认遇到RuntimeException就回滚。意思是第二步如果抛异常(比如商品ID不存在),第一步插入的进货单也会被撤销,不会留下“只有进货单没有库存变化”的脏数据。查看源码时重点看Service实现类上有没有这个注解,很多简化版项目把它漏掉,一旦数据出问题,整张表状态就是乱的。
3. 把SSM超市管理系统源码跑起来:四步环境配置与最小启动流程
3.1 版本搭配:这个组合最不容易翻车
拿到源码先别急着解压。SSM是2015年前后流行的框架组合,仓库里的代码大概率是按JDK 1.8写的,所以环境版本直接决定你能不能跑起来。我验证过多次,最稳的组合是:JDK 1.8、Tomcat 8.5、Maven 3.6.3、MySQL 5.7,开发工具用IDEA 2020到2023之间的任意版本。JDK 11也可以跑大部分项目,但个别老版本CGLIB代理在JDK 17上会直接报IllegalAccessError,不建议冒险。MySQL 8.0也能用,但要注意驱动类名变了,这一点后面我会单独讲。
Java环境变量配置是第一个坑。Windows上装完JDK后,系统变量里要配JAVA_HOME指向JDK安装根目录,PATH里加%JAVA_HOME%\bin。配置完不要直接打开IDEA,先开一个命令行窗口验证:
java -version mvn -versionjava -version输出里看到1.8.0_xxx就对了,如果显示的是11或17,说明JAVA_HOME被其他软件改过了。mvn -version除了看Maven版本,还要看它底下的Java version是不是也指向1.8,Maven和IDEA用的JDK不是同一个,是很多“明明配好了却编译报错”的根源。
3.2 用IDEA导入Maven项目并配置依赖下载
SSM源码有两种常见组织方式:一种是Maven工程,根目录有pom.xml;另一种是老式Eclipse Web Project,目录里直接是src和WebContent。前者是主流。Maven工程导入IDEA的步骤是:File → New → Project from Existing Sources,选择根目录下的pom.xml,IDEA会识别出Maven结构。导入后第一件事,打开File → Settings → Build Tools → Maven,确认Maven home path指向你本机的Maven,再检查Runner → JRE选的是1.8。
依赖下载慢是老项目的通病,因为Spring 4、MyBatis 3这些依赖加起来有好几十个,第一次会从中央仓库慢慢拉。我一般会在Maven的settings.xml里配一个阿里云镜像,这一步能省半小时:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>aliyun public repository</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>配完后在IDEA里点Maven → Reload All Projects,等进度条跑完。如果某个依赖反复下载失败,看IDEA右下角的日志,常见的是网络超时,重试几次基本能过。
3.3 建库、改连接、部署Tomcat,跑通登录页
依赖下载完不代表能跑,数据库还没有。打开压缩包里的SQL脚本(一般叫db_supermarket.sql或者schema.sql),用命令行或者Navicat执行。命令行方式最稳:
mysql -uroot -p登录后执行:
CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARSET utf8mb4; USE supermarket; SOURCE /your/path/db_supermarket.sql;执行完用SHOW TABLES;确认表都建出来了。然后找到源码里的jdbc.properties(有的叫db.properties),把数据库连接改为你本机的账号密码:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456如果你装的是MySQL 8.0,第一行驱动要改成com.mysql.cj.jdbc.Driver,否则启动时会报ClassNotFoundException。最后配置Tomcat:Run → Edit Configurations → Tomcat Server → Local,Deployment标签页里点+ → Artifact,选项目名:war exploded,Application context填/supermarket,启动后访问http://localhost:8080/supermarket。看到登录页就说明环境通了。压缩包里的演示视频,作用就是让你先看一遍作者跑通时的操作顺序;如果你的版本和视频不一致,以你本地实际为准,不用逐帧对齐。
提示:启动Tomcat时如果端口被占用,改Tomcat的
server.xml里8080端口,或者找到占用进程关掉,别硬等。
4. 核心功能逐段拆解:登录拦截、分页查询与库存扣减的代码长什么样
4.1 登录认证与权限拦截:不写过滤器,页面就裸奔
超市管理系统的所有功能必须登录后才能用,这一层在SSM里通常是HandlerInterceptor实现的。拦截器比Filter更细,可以精确到某个URL规则,还能拿到Handler信息。典型写法是:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }preHandle返回false表示请求被拦截,页面跳去登录;返回true放行。getContextPath()是拿项目名,这样无论部署成/supermarket还是其他路径,重定向都不会写死。
光有拦截器类还不够,要注册到SpringMVC配置里才生效:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/css/**"/> <mvc:exclude-mapping path="/js/**"/> <mvc:exclude-mapping path="/images/**"/> </mvc:interceptor> </mvc:interceptors>path="/**"表示拦截所有路径,exclude-mapping放行登录接口和静态资源。这里如果把静态资源也拦了,页面会丢样式,但很多人第一时间想不到是拦截器的问题。源码里如果没有这个拦截器,你也可以自己加,这是给毕设“加工作量”又不容易出错的点。
4.2 商品分页查询:手写LIMIT和PageHelper两种方式
商品列表页一定需要分页,否则几百条商品全查出来,页面会卡。老项目里常见两种做法:一种是手写LIMIT,简单直接,适合展示SQL功底;另一种是引入PageHelper插件,一行代码搞定,适合追求效率。先看手写方式,Controller里接收页码:
@RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { int start = (pageNum - 1) * pageSize; List<Goods> goodsList = goodsDao.selectPage(start, pageSize); int total = goodsDao.count(); model.addAttribute("goodsList", goodsList); model.addAttribute("total", total); model.addAttribute("pageNum", pageNum); return "goods/list"; }对应的Mapper XML:
<select id="selectPage" resultType="com.demo.entity.Goods"> SELECT * FROM goods_info ORDER BY id LIMIT #{start}, #{limit} </select> <select id="count" resultType="int"> SELECT COUNT(*) FROM goods_info </select>LIMIT #{start}, #{limit}里的start是偏移量,计算公式是(当前页-1)*每页条数,比如第1页start为0,第2页start为10。参数名必须和注解里写的@RequestParam对应上,否则SpringMVC会报参数缺失。手写分页的缺点是要查两次库:一次数据、一次总数,数据量大时性能一般,但课程设计完全够用。
PageHelper的写法是去掉LIMIT,在Service里加一行PageHelper.startPage(pageNum, pageSize),MyBatis执行时自动拼上分页SQL。要确认源码里有没有引入pagehelper.jar,没有的话在pom.xml里加依赖。答辩时如果被问到分页原理,手写版能说清楚“先查总数再查当前页”,比背插件用法更能体现java基础。
4.3 收银扣库存:为什么必须加事务,减库存和生成单子的先后顺序
超市收银是整个系统里最需要严谨的模块。顾客买了一堆商品,系统要做三件事:生成销售单、生成销售明细、扣减每种商品的库存。这三件事不一致的后果很严重——单子显示卖出去了,仓库库存没变,盘点时对不上账。所以这个方法必须用事务包起来:
@Transactional public boolean sale(List<SaleItem> items) { for (SaleItem item : items) { Goods goods = goodsDao.selectById(item.getGoodsId()); if (goods == null) { throw new RuntimeException("商品不存在,ID:" + item.getGoodsId()); } if (goods.getStock() < item.getQuantity()) { throw new RuntimeException("商品库存不足:" + goods.getGoodsName()); } // 先扣减库存 goodsDao.reduceStock(item.getGoodsId(), item.getQuantity()); // 再插入明细 saleOrderDetailDao.insert(item); } return true; }这段代码有三个细节值得注意。第一,查询库存后要立刻判断,而不是直接减,否则会把库存扣成负数。第二,抛出的是RuntimeException而不是普通Exception,因为Spring声明式事务默认只对运行时异常回滚,抛普通异常事务不会回滚,这是一个非常经典的面试陷阱。第三,先扣库存再写明细,顺序上如果插入明细失败,事务回滚会连同库存扣减一起撤销,最终结果仍然一致。
另外,这里还有一个容易被忽略的点:goods查出后,减库存的SQL要写成UPDATE goods_info SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},让数据库来判断库存够不够,而不是只靠Java层的if判断。多个收银员同时卖同一件商品时,Java层的判断有竞态风险,数据库条件判断才可靠。
5. 避坑:SSM超市管理系统从导入到运行最常见的5个翻车点
5.1 启动Tomcat就报ClassNotFoundException: org.springframework.web.servlet
- 现象:Tomcat启动到一半,控制台抛出
ClassNotFoundException,类名是org.springframework.web.servlet.DispatcherServlet,页面直接500。 - 原因:IDEA里这个项目标记被弄乱了。Maven依赖已经下载到本地仓库,但没有被部署到Tomcat的运行时classpath里,Tomcat找不到Spring的jar包。
- 解决:右键项目 →
Maven → Reload Project,然后打开File → Project Structure → Artifacts,在WEB-INF/lib下确认能看到Spring相关jar包;如果看不到,删掉原Artifact重新新建Web Application Exploded。这一步做完再重启Tomcat。
5.2 登录页能显示,但CSS样式全部丢失,页面纯文本混乱
- 现象:功能正常,但页面没有任何样式,浏览器F12看到一堆404,路径都是
css/或js/开头的。 - 原因:SpringMVC的
DispatcherServlet在web.xml里被映射到了/或者/*,把图片、CSS、JS请求也当成Controller处理了。 - 解决:在
spring-mvc.xml里加<mvc:default-servlet-handler/>,让静态资源交给Tomcat默认Servlet处理;或者显式配置资源映射<mvc:resources mapping="/css/**" location="/css/"/>。另一个稳妥做法是把DispatcherServlet的映射路径改成*.do,动态请求走以.do结尾的URL,静态资源天然不受影响。
5.3 往数据库插入中文变成问号
- 现象:页面上填的商品名称是“农夫山泉”,存进数据库变成
???,或者查询出来乱码。 - 原因:三层字符集不一致。数据库建库时用了
latin1,或者表不是utf8;JDBC连接串里没有指定编码;Tomcat接收POST请求时默认用ISO-8859-1解析。 - 解决:按三层统一。建库语句写成
CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4;jdbc.url里加useUnicode=true&characterEncoding=utf8;Spring里加一个CharacterEncodingFilter,encoding设为UTF-8,forceEncoding设为true,放在web.xml所有过滤器的第一位。
5.4 修改jdbc.properties后,程序仍然连旧数据库
- 现象:明明改了密码或库名,重启Tomcat后报的仍然是老连接错误,甚至打印日志里显示的还是旧值。
- 原因:Spring的
<context:property-placeholder>在容器启动时把properties读进内存,IDEA的target目录里还残留编译前的旧版本文件;有时候Tomcat的exploded目录也缓存了旧的配置文件。 - 解决:每次改完配置文件做三件事:
Build → Rebuild Project,Maven → Clean,删掉Tomcat的work/Catalina目录缓存。这个问题的迷惑性很强,很多“改了不生效”的玄学问题,最后都是缓存惹的祸。
5.5 演示视频里的页面功能,和我本地跑出来的对不上
- 现象:视频里明明有“会员管理”菜单,本地登录后没有;视频里的商品列表能导出Excel,本地点了没反应。
- 原因:演示视频录制时间和源码版本可能不一致,数据脚本也可能是旧版覆盖不全;还有人用了同一个库但之前跑过别的项目,残留表互相对不上。
- 解决:以源码里自带的SQL脚本为准,新建一个干净的库重新source,不要复用别的项目数据库。核对功能时对照说明文档列出的功能清单,而不是视频画面。视频只用来帮你看清操作顺序,比如默认账号密码、菜单入口位置,功能差异以代码和说明为准。
6. 二次开发与答辩:把SSM超市管理系统变成你自己的作品
6.1 加一个会员积分功能,把改动控制在三张表以内
最简单的升级方案是给系统加“会员消费积分”。改三处:sys_user表加score字段,sale_order表加member_id字段,在销售Service里结算后加积分。代码改动量小,又能在答辩时作为“我独立完成的功能”讲清楚。核心逻辑就一行,放在生成销售单之后:
// 每消费10元积1分,memberId为0表示非会员 if (order.getMemberId() != null && order.getMemberId() > 0) { int score = (int) (order.getTotalAmount() / 10); userDao.updateScore(order.getMemberId(), score); }升级字段直接写原建表脚本里,避免手动执行重复语句报错。这个功能的好处是边界清晰:非会员不加积分、会员订单才计算、积分计算向下取整,答辩被追问时每个边界都能说清楚。
6.2 答辩前做一轮边界回归验证
用默认管理员账号登录,逐项检查:商品库存能不能被减成负数、重复提交收银订单会不会生成两条销售单、删除一个正在被进货单引用的商品会怎样、用户密码有没有用MD5存。这些问题不需要高深技术,只要把异常输入走一遍就能发现。我建议整理一张A4纸,左边写操作步骤,右边写预期结果,答辩演示时按纸走,避免现场手忙脚乱。哪怕只有一个功能出现异常,也比被考官指出“你连自己项目都不熟”要强。回答框架时讲三件事:表怎么设计、请求怎么走、事务怎么保证。把第2章的调用链和第4章的事务逻辑讲顺,SSM三个框架的职责自然就带出来了,这部分比背Java面试八股文更实用。
6.3 讲一个你真正踩过的坑,比背原理更有说服力
答辩时如果有追问环节,主动讲一个自己排错的过程。比如静态资源404那次,你是怎么发现是拦截器路径配错的;或者中文乱码那次,你如何确认是数据库还是连接串的编码问题。考官想确认的不是你记得多少术语,而是你有没有独立解决问题的能力。我当初赶进度时跳过数据库脚本直接跑,结果登录页能开但商品列表全空白,后来一条条对比SQL才发现是表结构变了。说白了,源码只是别人的骨架,你踩过坑、加过功能、能说清楚边界,这套SSM超市管理系统才真正属于你。希望这篇笔记能帮你少走几步弯路。
本文还有配套的精品资源,点击获取