简介:一套基于SSM和JSP的超市进销存管理系统完整毕业设计资源,面向Java方向学生和开发者,适合毕业设计、课程设计及实际项目参考。系统采用Java、MySQL和B/S结构,覆盖管理员、员工、前台三类角色,包含商品信息管理、入库出库记录、销售统计、订单管理等核心业务模块。压缩包共包含1390个文件,以Java源码、JSP页面、JS脚本、CSS样式为主,同时提供数据库SQL脚本和论文、PPT文档,资源包大小31.22MB,目录结构完整清晰。目前已有一百一十人学习,可借助配套论文了解需求分析与设计思路,结合源码快速搭建并二次开发,对掌握SSM框架整合和进销存业务流程有较强参考价值。
1. 拿 SSM + JSP 做超市进销存:这份毕设源码到底值不值得复现
家百利超市进销存管理系统是典型的 java 毕业设计选题,技术栈锁死在 SSM(Spring + SpringMVC + MyBatis)加 JSP 这套经典组合上。比起网上那些只有几个首页的空壳项目,这份资源带源码、论文和答辩 PPT,业务覆盖进货、销售、库存、供应商、报表统计,几乎是照着毕业设计的评分点设计的。适合两类人:一类是 java 基础一般、需要一个完整项目撑起毕设的应届生;另一类是工作后想补 SSM 全流程、拿现成代码做二次开发的初级工程师。值不值得复现,关键看它能不能在你的电脑上跑起来、论文能不能改得不像抄的,这篇就把这两件事讲透。
2. 项目骨架与核心业务:从表结构到进销存主链路
2.1 数据库设计:六张核心表撑起整个进销存
进销存系统的数据模型没有太高深的技巧,核心就一句话:商品、单据、库存三者对账不能乱。这份项目里的表结构我拆过一遍,典型设计是六张主表,外加几张辅助表如退货表、操作日志表。先看主表分工:
| 表名 | 职责 | 关键字段 |
|---|---|---|
| product_info | 商品主档 | product_code(唯一编码)、product_name、purchase_price、sale_price、warn_limit |
| supplier_info | 供应商档案 | supplier_code、supplier_name、contact、phone |
| purchase_order | 进货单主表 | order_no、supplier_id、total_amount、create_time、user_id |
| purchase_order_detail | 进货单明细 | order_id、product_id、quantity、price、subtotal |
| sale_order | 销售单主表 | order_no、member_id(可空)、total_amount、create_time |
| sale_order_detail | 销售单明细 | order_id、product_id、quantity、price、subtotal |
| stock_info | 库存表 | product_id、stock_count、last_update_time |
两张主表和两张明细表是典型的父子表结构,进货单、销售单都走一主多明细。库存表单独拆出来而不是在商品表里直接存库存量,这是这类项目最常见的做法,理由很直接:进货和销售都要更新库存,单独一张表加行锁比频繁更新商品主表更安全,也方便以后做库存流水审计。
设计上有一个容易忽略的点:商品表里存着 purchase_price 和 sale_price 两个价格字段,但明细表里也有 price。为什么不做联表查询?因为进货价可能随批次变化,销售价也可能临场折扣,明细表里的 price 是“成交价”,主表里的 price 是“默认价”,这两者一旦混用,月底对账必翻车。你要是改这个项目,千万别为省字段把明细表的 price 删掉。
2.2 一条进货单怎么走通 Controller → Service → Mapper
我拿到这种 SSM 项目的第一步是找一条完整业务链,进货入库是最典型的一条,从浏览器请求到数据库更新一共经过四层。先看进货单的 Service 层代码逻辑,这类项目里写法基本一致:
@Service public class PurchaseOrderServiceImpl implements PurchaseOrderService { @Autowired private PurchaseOrderMapper purchaseOrderMapper; @Autowired private PurchaseOrderDetailMapper detailMapper; @Autowired private StockMapper stockMapper; @Transactional @Override public void createOrder(PurchaseOrder order, List<PurchaseOrderDetail> details) { // 1. 生成单号:日期 + 流水号,例如 PO20240512001 order.setOrderNo(generateOrderNo()); order.setTotalAmount(calculateTotal(details)); purchaseOrderMapper.insert(order); // 2. 批量插入明细 for (PurchaseOrderDetail detail : details) { detail.setOrderId(order.getId()); detailMapper.insert(detail); } // 3. 更新库存:存在则累加,不存在则初始化 for (PurchaseOrderDetail detail : details) { stockMapper.addStock(detail.getProductId(), detail.getQuantity()); } } }这段代码有三个设计要点。第一,@Transactional挂在 createOrder 上,保证主表、明细、库存三步操作要么全部成功要么全部回滚,没有事务的话,明细插入失败但库存已经加了,对账时会多出一批“幽灵库存”。第二,单号生成放在业务层而不是数据库自增,是为了保证单号可读且不依赖数据库特性,MySQL 自增主键只做内部 ID 用。第三,stockMapper.addStock是原子操作,SQL 类似UPDATE stock_info SET stock_count = stock_count + #{quantity} WHERE product_id = #{productId},而不是先查出来再 set 新值,避免并发下重复覆盖。
Controller 层做的事情很薄,接收前端表单参数、手动组装 order 和 details 两个对象,然后调 service。注意它没有在 Controller 里写任何业务判断,这是 SSM 项目的标准分层约定,职责边界乱了的代码一眼就能看出来。
2.3 销售出库与库存扣减:先判断再扣还是扣完再查
进货是加库存,销售是减库存,减库存比加库存多一道关卡:库存够不够。不少新手在 Service 里写“先查库存,if 够用再扣”,在高并发的玩具项目里没问题,但进销存系统的扣库存逻辑通常更严谨:
UPDATE stock_info SET stock_count = stock_count - #{quantity} WHERE product_id = #{productId} AND stock_count >= #{quantity}这行 SQL 把“判断”和“扣减”合并成一个原子操作,影响行数为 0 说明库存不足或商品不存在,Service 拿到结果后抛出业务异常回滚事务。为什么不先SELECT stock_count再判断?因为两条语句之间有间隙,两个请求同时读到同一行余额时都会通过判断,最后库存变成负数。虽然超市进销存的并发量不大,但作为毕设,答辩老师看的是你有没有这个意识。
销售出库流程中还有一个前置动作:锁定库存还是在销售单生成时就扣减。这份项目走的是后者,即提交销售单的同时扣减库存,不设独立的“预留库存”状态——那种设计常见于电商系统,超市收银场景用不到。你再顺带看一眼销售退货:退货单要做的是反向操作,给库存加回去,同时记一笔负数金额的销售明细,方便统计毛损。
对账这么顺下来你就发现了,整条主链路的难点不在 CRUD,而在事务边界和库存更新方式。把这章的代码看明白,后面改动和答辩说理都有底气。
3. 把项目跑起来:JDK、Maven、Tomcat 与数据库连接的完整步骤
3.1 环境版本怎么配才不打架
SSM + JSP 是上了年纪的组合,版本选不对最容易出幺蛾子。我建议直接按下面这套来,都是经过大量项目验证的搭配:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 高版本 JDK 对 JSP 和 Tomcat 的兼容性反而容易出问题 |
| Maven | 3.6.x | 3.8 以上对中央仓库访问策略有调整,没必要冒险 |
| Tomcat | 8.5.x | 内嵌 JSP 编译器,和 JDK 8 配合最稳 |
| MySQL | 5.7 或 8.0 | 5.7 驱动用 com.mysql.jdbc.Driver,8.0 要用新驱动 |
| IDEA | 2020 以上均可 | Ultimate 版自带 Tomcat 集成 |
这里强调两点。第一,JDK 不要上 17,Tomcat 8.5 在 JDK 17 下能跑但 JSP 编译偶尔报模块访问错误,折腾半天没有收益。第二,MySQL 用 8.0 的话,驱动类和连接串参数必须跟着改,这个坑后面单独说,你现在只要知道 5.7 和 8.0 的配置不通用。
3.2 导入项目后要改的五个配置点
项目导入 IDEA 后先别急着点启动,按照下面顺序改配置,能省掉后面大把排查时间。
第一个必改的是数据库连接配置,在 resources 目录下的 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=123456MySQL 8.0 用户把驱动和连接串改成这样,不然启动直接报连接失败:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456serverTimezone不指定的话,MySQL 8.0 默认时区配置会导致日期字段插入报错;allowPublicKeyRetrieval=true是解决 8.0 使用 caching_sha2_password 认证时的公钥获取问题。这两个参数缺一个都会让你在启动阶段白费半小时。
第二个是 MyBatis 的 mapper 扫描路径。检查 applicationContext.xml 里 MapperScannerConfigurer 或者 Mybatis-Spring 的配置:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.supermarket.dao" /> </bean>basePackage必须和项目里的 DAO 接口包路径完全一致,少一层或多一层,启动时 Spring 容器不会报错,但一调接口就抛 Invalid bound statement (not found)。
第三个是 SpringMVC 的注解扫描和视图解析器,在 spring-mvc.xml 里:
<context:component-scan base-package="com.supermarket.controller" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>这里要注意,Controller 扫描包不能和 Service 扫描包重叠出歧义,而且 JSP 文件必须放在 prefix 指定的目录下,你看到页面 404 时优先检查这里。
第四个是 Tomcat 的部署配置。IDEA 的 Run/Debug Configurations 里,Deployment 选项卡的 Application context 建议设为/supermarket,和数据库名区分开。这个值决定浏览器访问路径,设为/则直接 ip:8080 访问,设成/supermarket则访问路径为http://localhost:8080/supermarket/login.jsp。
第五个是项目自身编码。检查 IDEA 右下角编码,项目文件和 properties 文件统一 UTF-8,否则中文乱码问题会在页面和数据库里同时出现,到时候你分不清是 JSP 的问题还是数据库连接串的问题。
3.3 从启动日志判断项目是否正常
配置全部改完,点启动按钮后不要干等页面,看控制台日志按顺序确认三件事。第一,Spring 容器有没有完整初始化,看到ContextLoaderListener - Root WebApplicationContext: initialization completed说明 service 和 mapper 都装配成功了。第二,MyBatis 有没有把 mapper 映射文件加载进去,日志里能看到类似Mapped Statements collection already contains value的提示,如果你看到的是Invalid bound statement异常,回到上一节检查 namespace 和 DAO 包路径。第三,Tomcat 有没有正常部署项目,看到Deployment of web application directory ... has finished后访问登录页。
一个常见翻车点:Tomcat 已经启动了但页面 404。先确认访问地址的 context path 对不对,再看 controller 的 RequestMapping 值,最后看 JSP 是不是放在 WEB-INF 下。按这个顺序排查,比到处改代码强得多。
3.4 数据库初始化:表结构和测试数据怎么导入
启动项目前要把数据库建好。项目里一般带 supermarket.sql,用 Navicat 或 MySQL 命令行导入:
mysql -u root -p -h localhost < supermarket.sql我一般习惯用命令行而不是图形工具导入,因为项目自带的 sql 有时带了建库语句,图形工具执行容易报错。导入成功后确认一下:所有表是否建出来、sys_user 表里是否有一条 admin 账号、product_info 是否又几条示例商品。没有测试数据的话,登录后是空界面,你也不知道系统到底好没好。sql 文件里如果没有示例商品,自己手动插两条,别跳过这步——后面调试业务链全靠这几条数据。
4. 避坑:这五个问题占了调试时间的 80%
4.1 一启动就报 ClassNotFoundException: javax.servlet.jsp.jstl.core.Config
现象:Tomcat 启动过程中抛出异常,页面一直 500,控制台能看到找不到 jstl 相关类。原因:项目的 pom.xml 里没引 JSTL 依赖,或者引了但没加 scope。SSM 项目虽然标配了 JSTL 标签库,但很多源码包切掉了这个配置。解决:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>加完后重新导入 Maven 项目,再启动确认。如果你是 JDK 9 以上,还会碰到 javax 包被移出的问题,那就更麻烦,所以我前面才反复强调用 JDK 8。
4.2 数据库连接失败:Access denied 或 Public Key Retrieval is not allowed
现象:日志里报Access denied for user 'root'@'localhost',或者Public Key Retrieval is not allowed。原因:前者是密码不对或用户名不对,后者是 MySQL 8.0 默认认证插件是 caching_sha2_password,老驱动在非 SSL 连接下拿不到公钥。解决:密码不对就重置 MySQL 账号;认证问题按第 3 章说的,把驱动换成com.mysql.cj.jdbc.Driver,连接串加上allowPublicKeyRetrieval=true。顺带检查 pom.xml 里 mysql-connector-java 的版本,5.1.x 对 MySQL 8.0 基本无解,直接升到 8.0.x。
4.3 页面跳转正常但接口报 Invalid bound statement (not found)
现象:页面能打开,列表数据全空或 Ajax 请求返回 500,控制台明确报Invalid bound statement。原因:mapper 接口扫描到但对应的 XML 文件没加载,最常见是 resources 目录下的 mapper XML 文件后缀是.xml但放错了目录,或 namespace 写错。解决:打开 target/classes 目录看有没有把 mapper XML 拷进来,没有的话在 pom.xml 的 build 里加:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>这段配置是把写在 java 包下的 mapper XML 一并打包,很多源码包就坑在这里。
4.4 JSP 页面 EL 表达式原样输出,${user.name} 直接显示在页面上
现象:页面不报错,但${...}写的东西没有解析,明晃晃地显示在页面上。原因:web.xml 的版本太老,Servlet 2.4 以下默认不启用 EL。很多毕业设计项目 web.xml 头声明还停留在 2.3,JSP 引擎就不解析表达式。解决:把 web.xml 的头部标准声明改掉:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> </web-app>4.5 中文乱码:登录后用户名、商品名全是问号
现象:页面内容有的是 UTF-8,有的是 GBK,数据库里存进去的名字在页面上显示成???。原因:三个环节的编码不一致,数据库连接串、页面 contentType、Tomcat URI 编码至少要统一。按这套改一遍就能解决:
# jdbc.properties jdbc.url=...&characterEncoding=utf8<!-- JSP 页面头部 --> <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8"。这三处全改完,清空浏览器缓存重新登录验证。
5. 把别人的毕设变成自己的:论文、PPT 与功能改造的三个抓手
5.1 论文和 PPT 怎么用:结构拆解与答辩逻辑
源码包里带的论文不能直接拿去交,照着改结构比从零写到快得多,但改法有讲究。论文动三个部分:需求分析里的“用户角色”和“功能需求”要结合你实际演示的功能改,数据库设计章节直接用第 2 章的表结构图重画成 ER 图,测试章节必须补充你亲手跑的测试用例和截图。核心代码展示部分不要整段粘贴,选关键业务链比如进货单事务那 20 行代码,加注释说明设计意图,这就是答辩时的亮点。
PPT 按十页规划:选题背景、系统架构图、功能结构图、数据库 ER 图、界面截图三页、测试结果、总结展望。注意系统架构图画 SpringMVC 的执行流程,不要画成网络拓扑图,评委想看的是你懂不懂框架的请求流转,不是你会不会画连接线。
答辩时被问最多的永远是三个问题:数据库表之间什么关系、事务怎么保证一致性、为什么选 SSM 不选 Spring Boot。你按第 2 章的理解去答,顺着表结构讲到父子表,再讲到@Transactional,基本不会冷场。
5.2 加一个库存预警功能:从 SQL 到页面的完整改动
把别人的毕设变成自己的,最直接的办法是加功能。我建议加库存预警,改动小、演示效果好,字段都现成。商品表里已经有 warn_limit 字段,只要查询时把库存量和预警值比对并标注出来。先改 Mapper 的查询 SQL:
SELECT p.id, p.product_name, p.product_code, s.stock_count, p.warn_limit, CASE WHEN s.stock_count < p.warn_limit THEN '预警' ELSE '正常' END AS stock_status FROM product_info p LEFT JOIN stock_info s ON p.id = s.product_id然后在 Controller 里加一个查询方法,返回带 stock_status 的商品列表。JSP 页面在商品列表表格中加一列,用<c:if>判断字符串,status 等于“预警”时给单元格加一个红色字体样式:
<c:if test="${item.stockStatus == '预警'}"> <span style="color:red;">库存不足</span> </c:if> <c:if test="${item.stockStatus != '预警'}"> <span>库存正常</span> </c:if>这个功能演示时效果很直观:把某个商品库存修改到预警线以下,刷新页面立即标红。答辩时你可以顺带讲一讲 LEFT JOIN 和 INNER JOIN 的差别,为什么用 LEFT JOIN——因为要查出所有商品,哪怕它还没有库存记录。
5.3 答辩前必须验证的十个功能点
改完代码别直接去答辩,按下面这个清单快速过一遍,每项确认 OK 再划掉。
| 序号 | 验证点 | 具体操作 |
|---|---|---|
| 1 | 登录与权限 | 用 admin 登录,非管理员账号登录后看不到用户管理菜单 |
| 2 | 商品增删改查 | 新增、编辑、删除各执行一次,列表刷新无异常 |
| 3 | 进货全流程 | 新建进货单选供应商加商品,库存数量同步增加 |
| 4 | 销售全流程 | 新建销售单,库存数量同步减少,金额汇总正确 |
| 5 | 库存异常处理 | 销售数量大于库存时报错提示,不生成销售单 |
| 6 | 搜索与分页 | 关键字搜索商品、供应商,分页切换正常 |
| 7 | 表单校验 | 必填项留空提交,有提示且不报 500 |
| 8 | 事务回滚 | 造一个进货单明细为空的情况,观察主表是否回滚 |
| 9 | 数据统计 | 报表页面的销售总额、进货总额与明细对得上 |
| 10 | 会话超时 | 长时间不操作后跳转登录页 |
第 8 项容易被忽略,但评委很爱现场试。@Transactional只在 RuntimeException 下回滚,如果你的 Service 手动 catch 了异常没往外抛,事务就失效了,这个细节能拿出来讲透很加分。
6. 进阶动作:把销售报表导出到 Excel,再看一眼事务边界
答辩和项目完善阶段,我强烈建议给报表页加一个导出 Excel 的功能。超市进销存系统的报表本来就适合放 Excel,评委也爱看这种实用功能。用 Apache POI 实现也不复杂,Controller 层加一个方法,返回类型设为 void,通过 HttpServletResponse 写出文件:
@RequestMapping("/export") public void export(HttpServletResponse response) throws IOException { List<SaleOrderVO> list = saleOrderService.getSaleReportList(); HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("销售报表"); String[] headers = {"单号", "商品名称", "数量", "成交价", "小计", "日期"}; HSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum = 1; for (SaleOrderVO vo : list) { HSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(vo.getOrderNo()); row.createCell(1).setCellValue(vo.getProductName()); row.createCell(2).setCellValue(vo.getQuantity()); row.createCell(3).setCellValue(vo.getPrice()); row.createCell(4).setCellValue(vo.getSubtotal()); row.createCell(5).setCellValue(vo.getCreateTime()); } response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=sale_report.xls"); workbook.write(response.getOutputStream()); workbook.close(); }这段代码的要点有三个:文件名不要写死路径,用响应头告诉浏览器下载;HSSFWorkbook 对应 xls 格式,数据量大时换成 XSSFWorkbook 对应 xlsx;workbook.close()一定要写,POI 打开的文件句柄不释放,第二次导出就会报文件被占用。导出后别忘记验证一下数字精度,小数位丢失的问题在 Excel 导出上很常见,POI 的数值单元格如果直接传 double,显示可能变成 49.9999,四舍五入格式要自己在 settCellValue 前处理好。
我最早改这类毕设项目时吃过一次大亏:给进货单加了导出功能,兴冲冲跑起来发现导出的 Excel 全部打不开,仔细一看是文件流没关,Tomcat 里堆了几十个文件句柄。从那以后我每次拿到一套源码,都强制先跑通主链路再改代码,改完功能必须过一遍“新功能能不能正常返回、异常输入会不会抛 500、多操作几次会不会内存溢出”这三道检查才敢收工。这套习惯救了我好几次,也分享给你,希望帮到你。
本文还有配套的精品资源,点击获取