
简介这是一套基于SSM框架与JSP技术的超市进销存管理系统毕业设计资源包内含Java源码、配套论文与答辩PPT适合计算机相关专业毕业生或课程设计者参考学习。系统采用Java、MySQL与B/S架构覆盖超市进销存全流程管理员可维护用户、员工、商品分类与信息处理入库/出库记录、销售统计、订单及留言等员工可执行商品与库房操作前台提供商品浏览、新闻资讯与留言反馈。压缩包共1390个文件以JSP页面、Java类、JS脚本、CSS样式、PNG/GIF图片及SQL脚本为主配套论文、PPT和若干配置文件整体大小约31.22MB目录结构清晰。目前已有110人学习/下载对于想快速上手SSMJSP项目、了解进销存系统设计与开发的读者是一份完整可用的学习素材。1. 家百利超市进销存管理系统这个SSMJSP项目到底能交付什么如果你正在搜“java基于ssmjsp 家百利超市进销存管理系统 源码”大概率是在做毕业设计或者找一套能直接改改用的JavaWeb课设。这个标题里最值钱的不是“家百利”这个超市名而是那一整套“源码论文PPT”的组合——它把一个典型的进销存业务塞进了SSM框架和JSP页面里从商品入库、销售出库到库存统计全链路都能跑通。说白了它就是一个小型后台管理系统的样板间适合那些想搞懂SpringSpringMVCMyBatis怎么协作、又不想从零开始写增删改查的人。很多学生拿到源码后的第一反应是“导入IDE就能跑”但真做起来会发现数据库脚本要改、配置文件要改、Tomcat版本还会坑你一把。这篇笔记就是把“能跑”变成“跑得明白”的过程我会把项目里最常见的三个黑匣子——建表、三层调用、事务处理——全部拆开讲。不管你是想交毕设还是想借这个项目练手照着下面的路子走比你闷头看源码省半天时间。2. 为什么选SSMJSP进销存系统的架构选型与表设计2.1 SSM三件套的分工从HTTP请求到SQL执行的完整链路SSM不是一种单独的技术而是Spring、SpringMVC、MyBatis三个框架的合称。在“家百利超市进销存管理系统”里Spring负责管对象SpringMVC负责接请求MyBatis负责写SQL。很多第一次接触SSM的人会被它绕晕但你只要记住一条链路浏览器发起请求DispatcherServlet先拦下来找到对应的Controller方法Controller里不写SQL只调Service接口Service实现类处理业务逻辑再调Mapper接口Mapper接口对应XML文件里的SQL语句最终返回结果给JSP渲染。这里有一个实用经验进销存系统的业务逻辑比普通CRUD重所以Spring的声明式事务必须配置在Service层而不是Controller层。我见过不少源码把Transactional写在Controller上结果一旦Service里抛出异常事务根本不会回滚。正确做法是在接口实现类上加上Transactional让Spring自动接管提交和回滚。至于MyBatis它最反直觉的是“接口和XML文件的绑定”如果你在mapper包下建了接口却把XML放在resource目录下且文件名不匹配启动时会直接报BindingException。2.2 JSP在后台管理系统里的位置服务端渲染的取舍JSP在前几年被渲染成“过时技术”但在毕设和内部管理后台里它依然很实用。家百利这种超市进销存系统用户是超市收银员和店长页面不需要给成千上万的人同时访问服务端渲染反而省事——后端把数据塞进requestJSP用EL表达式和JSTL标签直接显示不用写一堆前端接口。JSP里还可以嵌入Java代码虽然不推荐把复杂逻辑写在页面里但做个小循环、做不平等判断比前后端分离的项目快得多。不过JSP有一个容易踩的坑页面上出现% %脚本片段时很多人顺手往里面写业务代码结果把Service注入到了页面里。这种写法在单机演示时能跑但代码一多就变成意大利面。我一般建议只在JSP里用EL和JSTLJava代码留在Servlet和Controller。比如展示商品列表用c:forEach遍历就行不需要在页面里写for循环。如果你拿到源码发现某个JSP里一大段% %先不要删看看它是否依赖了某个request属性改成EL表达式会更干净。2.3 家百利超市业务模型核心表结构设计进销存系统的数据库设计决定了整个项目的一半工作量。家百利这种超市维度比较简单商品、供应商、采购、销售、库存。常见的表结构如下表名说明核心字段sys_user系统用户id, username, password, rolesupplier供应商id, name, contact, phonegoods商品id, name, spec, unit, stock, price_in, price_outpurchase进货主表id, supplier_id, user_id, purchase_time, total_moneypurchase_detail进货明细id, purchase_id, goods_id, quantity, pricesale销售主表id, user_id, sale_time, total_moneysale_detail销售明细id, sale_id, goods_id, quantity, price这套设计的核心思路是“主表明细表”双层结构。每一次进货是一张主表记录从哪家供应商进的货、总金额多少至于进了哪几种商品、每种多少个、单价多少则放在明细表里。这样做的好处是统计时直接对主表按日期或供应商分组不用去明细表里逐条算。而库存字段我建议直接冗余在goods表里虽然违反了一点范式但当你做销售出库时一条UPDATE就能扣减库存查询时也不用JOIN来JOIN去。2.4 库存表的设计陷阱库存数量是冗余字段还是实时计算写进销存系统最纠结的就是库存字段。有人觉得库存应该实时计算把purchase_detail和sale_detail的差值SUM出来这样永远不会数据不一致。听起来很美但代价是每次查商品列表都要做聚合查询数据一大就慢。最常见且可靠的做法是在goods表里放一个stock字段进货时加库存销售时减库存同时勤做库存盘点。如果要做得分高一点给goods表加一个version字段做乐观锁。原因很简单超市收银台有好几个同一时间可能多个收银员卖出同一个商品。如果A和B同时读到stock10A卖出2个回写为8B卖出3个也回写为8数据库最后记录8实际库存变成了5。用乐观锁在UPDATE语句里加“WHERE id#{id} AND version#{version}”更新成功后version加1就能减少这种并发翻车。当然毕设级别只要能把事务和扣减逻辑写清楚评委一般不会追究并发细节但你要是想在答辩时多说一句这个版本号是个亮点。3. 从源码到跑通环境、导入、建库三步走3.1 环境清单与版本匹配JDK/Tomcat/MySQL/Maven的搭配老手看源码第一眼看的就是版本。这个SSMJSP项目常见的环境配置是JDK 1.8、Tomcat 8.5或者9.0、MySQL 5.7、Maven 3.6.x、IDEA或Eclipse。为什么不用Tomcat 10因为Tomcat 10以后把Servlet API的包名从javax.换成了jakarta.而很多SSM老项目的依赖还是javax.servlet直接部署会报ClassNotFoundException。如果你手头只有Tomcat 10最省事的办法是改依赖但通常麻烦不如直接装Tomcat 9。表格版本匹配组件推荐版本说明JDK1.8SSM官方文档和大多数依赖编译基于1.8Tomcat8.5/9.0兼容javax.*不要用10以上MySQL5.7/8.05.7最稳8.0需改驱动名Maven3.6.3太低可能拉不下依赖IDEA2021.3社区版就够用MySQL 8.0要注意驱动类名变成了com.mysql.cj.jdbc.Driver而且时区参数serverTimezone不能省不然连接时会报错。这个细节在源码里常被坑因为很多博客给的是老配置。3.2 导入源码IDEA中从zip解压到跑起的最小步骤假设你已经拿到了家百利的源码包里面会有sql文件、src目录、pom.xml、还有论文和PPT。打开IDEA直接用“Open or Import”选择项目根目录IDEA会识别Maven项目并自动下载依赖。如果你的网络不好这一步可能卡在下载Spring包上建议先用国内镜像仓库具体做法是改Maven的settings.xml文件加上阿里云镜像。的配置长这样。注意这里只是镜像配置不会影响项目本身。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/central/url /mirror配置好之后让Maven把依赖拉完。接下来不要急着点运行先看项目结构。常见结构是com.jbl.controller、com.jbl.service、com.jbl.dao、com.jbl.entity以及resources目录下的spring-context.xml、spring-mvc.xml、mybatis-config.xml、db.properties。如果点完运行发现tomcat没有配置Artifact需要先创建一个Web artifact将项目打包成war包。对不熟悉IDEA的人我建议直接使用Maven的tomcat7插件在pom.xml里配置好插件后执行“tomcat7:run”命令比手动部署Tomcat省事。mvn clean tomcat7:run -Dmaven.tomcat.port8085启动日志里看到“Starting ProtocolHandler”就说明web容器起来了。这个命令把端口改成8085是为了避免和本机其他Tomcat冲突当然你要用8080也行。3.3 数据库初始化建库建表SQL与初始数据源码包里一般会带一个.sql脚本比如jbl_supermarket.sql。用Navicat或者MySQL Workbench导入之前先手动建一个数据库注意字符集一定要指定UTF-8否则后边中文乱码。导入时不建议直接用命令行source因为脚本里的编码有时和环境冲突我一般会先用记事本打开确认没有乱码再复制到客户端执行。下面是一段核心建表SQL提取版本并非完整脚本但思路一致CREATE DATABASE IF NOT EXISTS jbl_supermarket DEFAULT CHARSET utf8mb4; USE jbl_supermarket; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT cashier ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, spec VARCHAR(50), unit VARCHAR(10), stock INT DEFAULT 0, price_in DECIMAL(10,2), price_out DECIMAL(10,2), version INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里用utf8mb4而不是utf8是因为MySQL的utf8实际上最多存3字节一些生僻字和表情符号会报错。进销存系统里虽然不太会存表情但为了防止乱码直接用utf8mb4更稳。另外所有表都用InnoDB引擎原因是InnoDB支持事务和外键而MyISAM不支持这一点在销售扣库存时至关重要。3.4 必改的配置参数db.properties、log4j、Tomcat端口项目能不能连上数据库全看db.properties这个文件。我见过太多人在这里改错。最常见的是数据库密码没有改或者MySQL8的驱动名没换。下面的代码是一份兼容MySQL5.7和8.0的配置注意看driverClass和连接字符串。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/jbl_supermarket?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456参数说明driverClass用com.mysql.cj.jdbc.Driver这是MySQL8的驱动名但MySQL5.7也兼容。url里的serverTimezoneAsia/Shanghai很重要不设的话会报时区错误。useSSLfalse是关掉SSL告警本地开发不用加密。username和password改成你自己的。改完db.properties之后再改一下日志级别。项目里的log4j.properties如果写的是DEBUG启动时会刷一大堆SQL日志影响看报错。我一般会把它调到INFO或者给某一个包单独开DEBUG。log4j.rootLoggerINFO, stdout log4j.logger.com.jbl.daoDEBUG这样只对MyBatis的Mapper包打印SQL其他框架的日志压到INFO调试起来最舒服。最后如果你用外部Tomcat部署记得检查conf/server.xml里的端口是否被占用或者直接用Maven插件自带Tomcat避免端口冲突。4. 进销存核心功能从页面表单到数据库事务4.1 进货入库Controller接收参数Service开启事务Mapper批量插入进销存系统里进货算是“写”操作中最复杂的一类因为它要同时操作两张表purchase主表和purchase_detail明细表。前端的JSP表单会把供应商ID、商品ID集合、数量、单价提交到ControllerController把数据封装成Purchase对象和List 再交给Service层。Controller层不应该出现业务逻辑最干净的写法是如下这样注意看参数封装。Controller RequestMapping(/purchase) public class PurchaseController { Autowired private PurchaseService purchaseService; RequestMapping(/add) public String add(Purchase purchase, RequestParam(goodsId) Integer[] goodsIds, RequestParam(quantity) Integer[] quantities, RequestParam(price) Double[] prices) { // 这里只做参数组装 ListPurchaseDetail details new ArrayList(); for (int i 0; i goodsIds.length; i) { PurchaseDetail d new PurchaseDetail(); d.setGoodsId(goodsIds[i]); d.setQuantity(quantities[i]); d.setPrice(prices[i]); details.add(d); } purchaseService.addPurchase(purchase, details); return redirect:/purchase/list; } }代码逻辑说明Controller用RequestParam数组接收页面里同名的多个参数比如goodsId、quantity、price这些是JSP表格里每行输入框的name属性。数组长度必须一致否则会数组越界。Service层拿到主表和明细表后在同一个事务里先插主表获取主键ID再把ID塞进每个明细对象最后批量插入明细。4.2 销售出库库存扣减与流水记录的原子性销售的逻辑比进货多一步扣库存。过程中最怕的是“卖了两件商品主表写成功明细表写失败库存却没有变”这种数据不一致问题只能靠事务来解决。在Service实现类上加上Transactionalinternal方法里的所有数据库操作要么全部成功要么全部回滚。Service public class SaleServiceImpl implements SaleService { Autowired private SaleMapper saleMapper; Autowired private GoodsMapper goodsMapper; Override Transactional(rollbackFor Exception.class) public void addSale(Sale sale, ListSaleDetail details) { saleMapper.insertSale(sale); for (SaleDetail detail : details) { saleMapper.insertSaleDetail(detail); Goods goods goodsMapper.selectById(detail.getGoodsId()); if (goods.getStock() detail.getQuantity()) { throw new RuntimeException(库存不足); } goodsMapper.reduceStock(detail.getGoodsId(), detail.getQuantity()); } } }这里的重点在Transactional(rollbackFor Exception.class)。默认情况下Spring只对运行时异常回滚像RuntimeException这种没问题但如果你在业务里抛了一个自定义异常但它没有继承RuntimeException事务就不会回滚。所以写rollbackFor是稳妥习惯。reduceStock的Mapper方法不要用select后update更聪明的做法是直接在UPDATE语句里判断库存UPDATE goods SET stock stock - #{quantity}, version version 1 WHERE id #{goodsId} AND stock #{quantity}如果返回的受影响行数为0说明库存不足直接抛异常。这种写法减少了并发问题也是面试加分点。4.3 JSP页面EL表达式、JSTL循环、Ajax局部刷新JSP这层主要做两件事把数据库里的列表展示出来以及接收用户输入。家百利这种管理系统页面风格一般比较朴素一个商品管理页面通常用表格展示名称、规格、库存、单价。遍历集合用的是JSTL c:forEach而不是Java for循环。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 tr th商品名/th th库存/th th销售价/th th操作/th /tr c:forEach varg items${goodsList} tr td${g.name}/td td${g.stock}/td td${g.priceOut}/td tda hrefsale/add?goodsId${g.id}售卖/a/td /tr /c:forEach /table这个页面的逻辑说明${g.name}走的是EL表达式它实际上调用了goods对象的getName()方法。c:forEach里items属性绑定的goodsList是在Controller里用model.addAttribute(goodsList, list)塞进来的。这套机制的好处是页面里没有Java代码不会出现空指针。如果想让页面局部刷新库存变化可以用jQuery的Ajax。区别于传统表单提交这里请求后端后不跳转直接更新某个 标签的数字。$.post(goods/detailStock, {id: goodsId}, function(data) { $(#stock_ goodsId).text(data); });后端Controller返回整数类型SpringMVC会自动转换成JSON文本前提是配置了fastjson或jackson依赖。如果没有依赖你可以直接在方法上返回ModelAndView但那样就不是局部刷新了。4.4 权限控制拦截器放行登录后的session校验一个超市系统不可能让所有人都能进入后台所以权限拦截是基本功能。最常见做法是写一个HandlerInterceptor拦截所有.do结尾的请求或特定路径检查session里有没有登录用户。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }然后在spring-mvc.xml里配置拦截器。mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login.jsp/ mvc:exclude-mapping path/user/login/ mvc:exclude-mapping path/static/**/ /mvc:interceptor /mvc:interceptors这段配置的意思是除了登录页、登录请求和静态资源外其余URL都进行session校验。这里有一个隐蔽问题如果你只拦截了Controller路径没拦截JSP直接访问用户可以直接在浏览器输入路径访问某个JSP页面绕过拦截器。解决办法是JSP页面放在WEB-INF目录下外部无法直接访问只能通过Controller转发进去。拿到的源码如果JSP没有在WEB-INF下那权限拦截形同虚设答辩时可以提一句“我将页面移到了WEB-INF”。5. SSMJSP进销存系统避坑与排查5个必踩的坑5.1 环境级翻车从Tomcat到MySQL连接每一条都见过血坑1Tomcat启动报ClassNotFoundException: javax.servlet.Filter现象把源码部署到Tomcat后启动直接报错定位到的类名是javax.servlet.Filter或javax.servlet.ServletException。一个接一个的NoClassDefFoundError但依赖明明都引入了。原因Tomcat 10开始Servlet规范包名从javax.servlet换成了jakarta.servlet而项目里的SpringMVC 4或5还是按旧包名编译的运行时找不到类。解决换回Tomcat 8.5或9.0。如果你用的是IDEA内置Tomcat可以在Run Configuration里点击“”选择Tomcat Server Local然后把Tomcat路径改成本地9的安装目录。这一点我在配置家百利系统时踩过换成Tomcat 9再启动所有的javax异常全部消失。坑2连接数据库报Communications link failure或Unknown character set现象启动项目后第一次查询数据库报表控制台出现“Communications link failure”或者“Unknown initial character set index”。原因大部分情况是JDBC连接字符串里缺少serverTimezone参数或者数据库字符集是latin1而驱动按UTF-8解析。MySQL 8更严格没有serverTimezone连握手都完成不了。解决在db.properties的URL上统一使用“?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse”。同时把数据库表的字符集改成utf8mb4。改完记得重启Tomcat因为连接池可能在启动时缓存了旧的连接参数。坑3JSP页面中文乱码控制台中文也乱码现象浏览器访问JSP页面时商品名称显示为“ʳ”数据库里却是正常中文。而控制台打印的SQL日志中文也是乱码。原因三道环节编码不一致。第一是MySQL连接没有指定characterEncoding第二是JSP页面本身的pageEncoding没有设置为UTF-8第三是Tomcat在connector上的URIEncoding没有配置。解决JSP页面头部加上% page contentTypetext/html;charsetUTF-8 languagejava %在spring-mvc.xml里配置CharacterEncodingFilter最后在Tomcat的config/server.xml里给Connector增加URIEncodingUTF-8。三层都统一后中文再乱码的概率就很低了。5.2 业务逻辑翻车事务、参数绑定和懒加载的经典错误坑4销售扣库存时库存被扣成负数现象超市收银台有两个用户同时卖出同一个商品最终数据库里该商品的库存变成了负数。原因你用的扣减逻辑是“先select stock出来在Java里判定是否大于quantity然后update stock新的值”。这种方式存在时间差两个请求同时读到stock10都通过了if判断一个回写8另一个回写7最终库存计算错乱。如果加上了正常判断第二个请求update时没有重新校验它依然能成功。解决最省事的方式是把判断和扣减放在一条UPDATE语句里。如前面写的那样UPDATE goods SET stockstock-#{quantity} WHERE id#{id} AND stock#{quantity}。如果该update返回0行就说明库存不足直接在Service层抛RuntimeException让事务回滚。这样哪怕一万个并发进来数据库也能保证不会扣成负数。坑5MyBatis Mapper接口有多个参数时报binding exception或parameter not found现象在Service里调用一个Mapper接口方法传入商品ID和数量两个参数运行时报“Parameter goodsId not found. Available parameters are [param1, param2, ...]”感觉像源码有问题。原因MyBatis在处理多参数时如果没有用Param注解它只能通过参数位置param1、param2来绑定SQL里的#{}占位符号。你的SQL里写的是#goodsId自然找不到。更让人迷惑的是单参数时不会报错因为MyBatis会直接用参数对象那一个属性。解决给接口方法的每个参数加上Param(goodsId)和Param(quantity)。或者如果不愿意加注解就把两个参数封装成一个Bean对象。毕设源码里经常偷懒省略注解当你二次开发时就会撞上。搜“mybatis源码”的人应该都见过这个坑它本质上是MyBatis的ParamNameResolver在起作用。答辩时能把原理讲透会成为亮点。坑6查询列表时懒加载报LazyInitializationException现象商品列表页面能显示但一旦点击查看该商品对应的供应商详情时抛出LazyInitializationException: could not initialize proxy - no Session。原因MyBatis默认在Session关闭后不允许加载延迟属性。你在查询商品时设置了关联供应商的懒加载页面在渲染到${goods.supplier.name}时才去加载但此时SqlSession已经关闭了。解决最简单粗暴的做法是在spring-mvc.xml中把spring的OpenSessionInViewFilter配置进去。这样每个请求会绑定一个MyBatis的SqlSession直到页面渲染完才关闭。但要注意这个过滤器必须在DispatcherServlet之前配置否则不生效。另一种方式是在Mapper XML里用JOIN一次性查出所有关联数据关掉懒加载。对家百利这个小系统直接JOIN查询更安全不增加多少SQL复杂度也不会带来玄幻的Session问题。6. 让系统从“能跑”到“能交付”的验证与部署技巧先别急着把项目丢给评委按下面这张清单做一遍功能验收。打开登录页用admin账号登录进入进货管理新增一张进货单在选择商品时故意填库存不足的数量看系统是否给出提示再做一张正常销售单确认销售列表里的总金额正确、商品库存自动减少最后去库存报表页看数据是否与前面的操作对上。这一轮跑通核心功能就没大问题了。验证项操作预期结果登录拦截未登录直接访问商品列表URL跳转到登录页进货新增进货单并提交采购主表加一条记录商品库存增加销售新增销售单并提交销售主表加一条记录商品库存减少库存不足销售超过库存的商品系统报“库存不足”事务回滚中文乱码查看所有页面无一字乱码入库和显示一致接下来把Web项目打包成war包建议在pom.xml里配置一下finalName然后用Maven打包。命令是mvn clean package执行完target目录里会出现war文件把它丢到Tomcat的webapps目录里启动Tomcat就会自动解压部署。这一步能帮你摆脱IDE依赖也让交付过程更接近真实环境。部署完可以用curl快速验证一下首屏是否有HTML返回不要直接用浏览器打开而忽略控制台日志。curl -I http://localhost:8080/jbl_supermarket/login.jsp如果返回200和Content-Type:text/html说明系统已经在独立Tomcat里跑起来了。部署到生产环境前还要做一件小事把db.properties里的密码硬编码换成环境变量或用Jasypt加密。这个系统的源码可能默认把数据库密码写在明文中答辩时提一句“生产环境不会明文存储密码”比单纯说功能要加分。最后说一个我的个人习惯做这类SSMJSP项目一定不要把Controller写得太肥。我曾经在做一个类似“家百利”的进销存系统时把所有增删改查逻辑都塞进Controller结果改了三天代码越改越乱。后来花一天时间把每个模块的Service拆出来Controller只负责传参和返回页面顿时清爽了很多。源码里的复杂逻辑你也要先顺着Controller看进去不要直接在JSP里找业务那样只会越看越昏。这套方法我沿用到现在也希望帮到你。本文还有配套的精品资源点击获取