简介:一套面向毕业设计场景的JSP+SSM生鲜超市进销存管理系统完整源码包,适合计算机相关专业学生用于课程设计、毕业答辩或项目二次开发。系统覆盖商品信息管理、基本档案管理、进货采购管理、销售出库管理四大模块,前端采用JSP,后端为SSM框架,数据库使用MySQL,并支持Eclipse、MyEclipse、STS、IDEA等常见开发工具。资源包共428个文件,约35.79MB,包括82个Java源文件与对应class文件、60个JSP页面、77个依赖JAR包、38个XML配置、SQL数据库脚本、论文文档、环境工具包及同框架项目的安装教程视频,结构完整、便于直接导入运行。目前已有89人学习下载。内含源码、数据库脚本、论文、工具包和教程,尤其针对商品监管不合格提示、销售出库记录、供应商管理等业务细节有完整实现,可帮助理解SSM整合流程与进销存业务逻辑。
1. 生鲜超市进销存为什么值得做:这个标题到底交付了什么
如果你正在找毕业设计题目,看到「JSP SSM 生鲜超市进销存管理系统」这个组合,第一反应可能是:又是进销存,做了八百遍的题目。但生鲜超市和普通进销存有个本质差别——商品会坏。蔬菜放三天就黄叶,牛奶临期要打折,冻品化冻就得报损。这套系统把 SSM(Spring + Spring MVC + MyBatis)这套经典 JavaWeb 技术栈架在生鲜场景上,核心不是管库存数量,而是管批次、管保质期、管损耗。标题里「B源码含文档含教程」的意思是交付物齐全:源码能跑、有设计文档可交、有教程能照着复现。适合三类人:正在选题的本科生、想快速搭一套完整 SSM 项目练手的开发者、以及需要一套带生鲜业务亮点的毕设去答辩的人。
2. SSM 在进销存里的分工:从三层架构到业务闭环
2.1 为什么是 SSM 而不是 Spring Boot
先回答一个大概率会纠结的问题:现在企业里新项目基本都上 Spring Boot 了,毕设还写 JSP + SSM 是不是过时?我的看法是:选题不是选最新,是选最能讲清楚的。SSM 的好处在于三层架构界限分明——Controller 管请求分发,Service 管业务逻辑,Mapper 管数据库操作,每一层都是显式写出来的。这在毕业设计答辩里特别占优势:评委问「你的事务放在哪一层」,你直接指到 Service 类上的@Transactional就行,不用像 Spring Boot 那样在一堆自动配置里找。
另一个现实原因是从零搭建一个 SSM 项目能完整覆盖框架整合过程:Spring 容器怎么加载、Spring MVC 怎么拦截请求、MyBatis 怎么和 Spring 整合,每一步都是手动配置出来的。这个过程本身就是毕设的工作量和技术含量。如果直接用 Spring Boot,框架帮你干了大部分事,反而没东西可写。所以选 SSM 不算退而求其次,是把它当成学习产出的一部分。
至于 JSP 做页面,放在当下看确实复古,但毕设场景里它有个不可替代的优势:后端传数据到页面直接用 EL 表达式和 JSTL 标签就能渲染,不需要单独写 Vue 和接口。更关键的是——答辩现场如果你用前后端分离,评委要看你启动前端工程、看接口文档、看跨域配置;而 JSP 项目启动 Tomcat 就能点着页面讲功能,演示成本低很多,翻车概率也低。常见做法是 JSP 只在表现层输出数据,业务逻辑全部收敛在 Service 层,这样就算页面丑一点,代码结构是经得起问的。
2.2 生鲜进销存和普通进销存的三点差异
做这类型项目之前,建议先把业务差异想清楚,否则写出来就是一个换皮的标准进销存,答辩时不好讲。生鲜场景下至少有三个地方不一样。
一是库存要按批次管理。同一种牛奶,上午进的批次保质期到月底,下午进的批次保质期到下月中旬,不能混在一起算总库存,否则临期商品无法识别。这种需求自然要求在库存表上带出生产日期、到期日这些字段,而不只是「数量」。
二是价格会动态调整。普通超市标价基本固定,生鲜商品经常有早市价、晚市折扣、临期特价,销售时不能直接按商品表里的基准价去算销售额。这就需要销售明细里冗余一份成交单价,把当时实际卖多少钱记下来,后续对账才不会扯皮。
三是要有损耗处理路径。蔬菜水果在运输和存储过程中会有正常损耗、报损、退货,这些不能直接扣库存了事,要有独立的损耗单或报损记录,保证账面库存和实际盘点能对上。很多初版系统把损耗逻辑塞进销售模块里,结果月底统计毛利时怎么都对不上,就是因为损耗混在了销售数据里。
2.3 功能模块拆解与页面流转
按照常见实现方案,这套系统的功能可以拆成七个模块:登录与用户管理、供应商管理、商品管理(含分类)、采购入库、销售出库、库存查询与盘点、报表统计。核心业务闭环是:先维护供应商和商品档案,再走采购单入库,库存增加;顾客下单走销售单出库,库存减少;库存低于预警线时提示补货;生鲜批次临近保质期时进入预警列表。
页面流转一般是这样的:登录页验证通过后进入主框架页,左侧是菜单树,右侧是功能页面。采购业务流程是「采购单列表 → 新建采购单 → 选择供应商和商品 → 入库确认」,销售流程是「销售单列表 → 新建销售单 → 加商品 → 结算出库」。这套流转逻辑和实际超市操作能对应上,演示时也容易讲。
整个代码包的基础包名通常按 com.项目名.controller、service、mapper、entity 这类结构切分,Controller 只做参数接收和页面跳转,Service 里写业务判断,Mapper 里是 MyBatis 的接口和 XML 映射。理解了这条链路,后面改代码和定位问题都会顺手很多。
3. 数据库设计先解决生鲜的保质期与批次:核心表结构与字段说明
3.1 整体表结构一览
进销存系统的数据库表数量一般在十张左右,核心不是表多,而是表之间的关系要符合业务流转。下面是一个在毕设项目中最常见的表清单,字段做了精简,落地时会根据教程里的 SQL 脚本微调:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| admin_user | 系统用户 | id、username、password、real_name |
| supplier | 供应商 | id、supplier_name、contact、phone |
| category | 商品分类 | id、category_name |
| goods | 商品档案 | id、goods_name、category_id、unit、purchase_price、sale_price、stock_warning |
| goods_batch | 商品批次库存 | id、goods_id、batch_no、quantity、purchase_price、produce_date、expire_date、supplier_id |
| purchase_order | 采购单 | id、order_no、supplier_id、total_amount、create_time、status |
| purchase_item | 采购明细 | id、order_id、goods_id、quantity、price、amount |
| sale_order | 销售单 | id、order_no、total_amount、create_time、status |
| sale_item | 销售明细 | id、order_id、goods_id、quantity、price、amount |
| loss_order | 损耗报损单 | id、goods_id、quantity、reason、create_time |
这个结构里最值得关注的是goods_batch表。很多新手做进销存都是 goods 表里直接挂一个 stock 字段,库存不够就减,进货就加。这个设计在生鲜场景一定翻车——因为同一种商品不同批次的进货价可能不同,保质期也不一样,如果都混在一个数字里,后面既算不出临期商品,也算不准毛利。引入批次表之后,总库存变成了「所有批次 quantity 之和」,卖出时按先进先出或指定批次扣减,语义清楚,代码也好写。
3.2 商品表与批次表的核心建表语句
以下是常见的建表语句形态,可以直接对照教程里的 SQL 脚本理解:
CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(100) NOT NULL, category_id INT, unit VARCHAR(20) DEFAULT '斤', purchase_price DECIMAL(10,2), sale_price DECIMAL(10,2), stock_warning INT DEFAULT 10, status TINYINT DEFAULT 1 ); CREATE TABLE goods_batch ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, batch_no VARCHAR(50), quantity INT DEFAULT 0, purchase_price DECIMAL(10,2), produce_date DATE, expire_date DATE, supplier_id INT );这里有几个设计细节值得多说一句。expire_date是冗余字段——理论上可以由produce_date + 保质期天数推算出来,但专门存一列会让后续写「查三天内过期商品」的 SQL 简单非常多,直接WHERE expire_date BETWEEN ...就行,不用每行都算日期加减。purchase_price在批次表里也存了一份,卖货算毛利时用批次进价来匹配销售明细,才是最接近真实情况的算法,这也是为什么要给批次表保留供应商字段的原因——方便追溯这批货是谁供的。
3.3 为什么销售明细必须存一份成交单价
生鲜商品的价格是动态的,上午十块一斤,晚上八块一斤,临期可能五块处理。如果销售出库时不做价格快照,只记录商品 ID,那么一个月后回看这张销售单,你只能看到商品当前价格,根本不知道当时卖了多少钱。
常见的正确做法是 sale_item 表里显式存price和amount两个字段,下单那一刻从页面传入的成交价落库。采购明细也是一样,进货价可能随供应商供货批次波动,所以 purchase_item 里也要存price。这样做的好处是对账和毛利统计都是基于事实数据而不是推断数据。很多翻车的报表,问题就是出在这里——报表里的金额是「现算」的而不是「存档」的。
3.4 预置数据与初始化账号
毕设项目一般会在 SQL 脚本里预置一个管理员账号,常见做法是admin / admin123,密码不会用明文存储,通常是 MD5 处理后入库。导入数据库后第一件事就是用这个账号登录系统,然后去「用户管理」里改密码。源码包里自带的初始化脚本还会插入少量商品分类和供应商数据,目的是让页面不至于空荡荡,演示时也能直接走采购到销售的完整流程。
4. 把源码跑起来:环境版本搭配与三步启动配置
4.1 JDK、Tomcat、MySQL 的版本怎么搭不容易翻车
这个项目用的是老牌技术栈,版本选得对不对直接决定你能不能跑起来。常见的稳定搭配是:JDK 1.8、Tomcat 8.5、MySQL 5.7、Maven 3.6,集成开发环境用 IDEA。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SSM 项目普遍基于 JDK8 编译,不要用 17 及以上 |
| Tomcat | 8.5 | 与 JDK8 配合最稳,9.0 也能用但没必要冒险 |
| MySQL | 5.7 | 如果本地只有 8.0 也可以,但要换驱动和连接参数 |
| Maven | 3.6 | 只要能正常下载依赖就行,3.8 以上也兼容 |
| IDEA | 2020 之后任意版本 | 社区版也能开 Maven 项目,专业版只有 Tomcat 集成更顺 |
这里最容易踩的位置是 MySQL 8.0。很多新电脑直接装了 8.0,项目里的 mysql-connector-java 还是 5.x 的旧驱动,连数据库时直接报ClassNotFoundException或者Public Key Retrieval is not allowed。如果你是 8.0 数据库,按后面的避坑章节处理即可。
4.2 导入工程与初始化数据库
拿到源码压缩包后,第一件事不是急着打开 IDEA,而是先把数据库准备好。先在 MySQL 里建一个库,然后导入 SQL 脚本:
mysql -u root -p CREATE DATABASE fresh_manager DEFAULT CHARACTER SET utf8mb4; exit; mysql -u root -p fresh_manager < /your/path/fresh_manager.sql这段命令的逻辑是:先登录 MySQL 创建名为 fresh_manager 的数据库,指定 utf8mb4 字符集是为了让中文存储不出现乱码;然后退出交互式登录,用重定向把 SQL 脚本灌进这个新建的库。如果 SQL 脚本文件里有CREATE DATABASE和USE语句,那第一步手动建库可以省略,直接执行第二条命令导入即可。
导入完成之后验证一下表结构是否齐全:
mysql -u root -p USE fresh_manager; SHOW TABLES;正常会看到前面表格里列出的那几张表。如果SHOW TABLES结果为空或者少表,大概率是 SQL 脚本中途报错中断了,检查脚本里有没有DROP DATABASE之类的破坏性语句,或者本地 MySQL 的 sql_mode 是否限制了某条语句执行。
4.3 改数据库连接配置
SSM 项目的数据源配置一般在src/main/resources下的 properties 文件里,名字可能是db.properties或jdbc.properties。找到它并改成你本地数据库的账号密码:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/fresh_manager?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的数据库密码这四行分别定义了 JDBC 驱动类名、数据库连接地址、用户名和密码。characterEncoding=utf8是保证写入数据库的中文不乱码,useSSL=false是为了避免本地连接时出现的 SSL 警告。
如果用的是 MySQL 8.0,需要把驱动类名改成com.mysql.cj.jdbc.Driver,同时在连接地址末尾加上serverTimezone=Asia/Shanghai,否则会报时区错误。这个坑几乎每个人都会踩一次,后面避坑章节专门展开。
4.4 启动前检查依赖是否完整
SSM 项目导入 IDEA 后,Maven 会自动下载依赖。如果网络不好或本地仓库缺包,经常出现依赖下载一半的情况。启动前可以先在 IDEA 终端跑一次编译:
mvn clean package -DskipTests这条命令会清理旧的编译产物、重新编译源码并打包成 war 文件。-DskipTests是跳过测试代码,毕设项目一般没有写单元测试,加上它省时间。如果这一步报错,先去pom.xml里检查依赖坐标是否正确——最常见的报错是仓库里找不到对应版本的依赖,把版本号改成本地 Maven 仓库里已有的版本即可。
打包成功后,就可以配 Tomcat 启动了。常见操作是把 war 包拷贝到 Tomcat 的 webapps 目录后启动 Tomcat,或者直接在 IDEA 里配置本地 Tomcat 运行。推荐后者,因为 Debug 模式看链路更方便。启动成功后访问:
http://localhost:8080/fresh_manager/能跳转到登录页,说明项目跑通了。如果端口不是 8080,看一下 Tomcat 配置里改成了多少。默认管理员账号在 SQL 脚本里预置,通常是 admin 加一串初始密码,具体以文档里的说明为准。
5. 避坑排查:跑通后最常翻车的五个点
5.1 页面 404 或者 ClassNotFound:依赖没进 lib
现象:Tomcat 启动没报错,但一访问项目地址就 404,或者控制台报ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet。
原因:IDEA 在部署 Web 项目时,Artifacts 里的 WEB-INF/lib 没有打包 Maven 依赖。这个问题在导入别人的项目时特别常见——IDEA 识别了 Maven 结构,但部署配置没有把依赖带上。
解决:打开 File → Project Structure → Artifacts,选中当前项目的部署描述,在右侧 Available Elements 里找到项目依赖的库,右键选 Put into WEB-INF/lib。然后重新构建部署。这个操作做完,刷新页面就正常了。顺手看一眼 WEB-INF 目录下有没有 class 文件,如果连 class 都没有,那是编译输出目录没配对,把 Module 的编译输出路径改到 WEB-INF/classes 下。
5.2 MySQL 8 连不上数据库:驱动和时区双重问题
现象:启动日志里报No suitable driver found或者The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。
原因:本地装的是 MySQL 8.0,但项目里用的还是 5.x 的驱动包,或者连接参数里没带时区信息。MySQL 8.0 对时区要求变严格了,不显式指定就会报错。
解决:pom.xml 里确认 mysql-connector-java 的版本改成 8.0.26 或相近版本,然后在 jdbc.properties 里把驱动类名改为com.mysql.cj.jdbc.Driver,连接地址末尾加serverTimezone=Asia/Shanghai。改完记得重新构建项目,让新 jar 包进入 WEB-INF/lib。这里有个容易忽略的小细节:如果改完 pom 但没重新构建 Artifacts,旧 jar 仍然在部署目录里,照样连不上。
5.3 页面中文乱码:三层编码要统一
现象:页面显示中文全是问号,或者存入数据库后变成乱码,但控制台输出正常。
原因:JSP 页面编码、Tomcat 请求编码、数据库表字符集三者没统一。最常见的组合是一处用了 UTF-8,另一处用了 GBK,或者数据库表是 latin1 字符集。
解决:按三层依次排查。JSP 页面头部要写pageEncoding="UTF-8";Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8";数据库表和库本身要确认是 utf8mb4 字符集。如果表建出来已经是 latin1,不要只改连接参数,直接对表执行:
ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;执行完再把数据重新导入一次,乱码问题基本能根治。平时写代码时也要注意前端提交过来的数据经过过滤器没有,项目里一般会有一个字符编码过滤器配置在 web.xml,确保它拦截了所有请求路径。
5.4 库存数量对不上:事务和控制逻辑的锅
现象:销售单创建成功后,库存总数没扣减,或者扣了两次;采购入库同样操作两次,数量翻倍。
原因:两个层面导致。一是 Service 方法里没有加事务,Mapper 的更新操作各自独立提交,中间任何一步报错,库存就会停留在不一致的状态。二是控制逻辑写错了位置,比如把库存扣减写在了循环里又套了判断,或者页面重复提交了订单。
解决:事务层面,在 Service 实现类的方法上显式加@Transactional,确保扣库存、生成销售明细、更新商品销量这几步在同一个数据库事务里。控制层面,点击提交按钮后要立刻禁用按钮,同时后端在创建订单前先查一下同号订单是否存在,用订单号做幂等控制。这就避免了手抖双击造成两条销售单各扣一次库存的尴尬局面。
5.5 启动端口被占用或者热部署没生效
现象:Tomcat 启动直接报Port 8080 was already in use,或者改完代码刷新页面还是旧页面。
原因:上一轮 Tomcat 没关闭干净,或者 IDEA 的热部署配置没开。Windows 下经常出现关掉了 IDEA 但 Java 进程还占着端口的情况。
解决:端口被占用的处理是开命令行执行:
netstat -ano | findstr 8080 taskkill /PID 对应PID /F先通过 netstat 找到占用 8080 端口的进程 PID,再用 taskkill 强制结束。如果是热部署不生效,检查 IDEA 里 Tomcat 配置的 On frame deactivation 选项是否选了 Update resources,这是 JSP 页面即时刷新的关键;Java 代码改动则需要 DevTools 或手动重启 Tomcat,SSM 项目没有自动热部署的底子,别指望改个 Controller 就能自动生效。
6. 给系统加一个保质期预警:在毕设里做出业务亮点
前面说过,生鲜进销存和普通进销存的本质差异在保质期和损耗。如果只是把 CRUD 写完,系统能跑,但缺乏记忆点。这里有一个很容易落地、又能在答辩时讲出亮点的进阶功能:保质期预警。
思路不复杂,Spring 的@Scheduled注解就能实现定时任务。在 Service 层写一个方法,每天凌晨扫描一次 goods_batch 表,把到期日距今小于等于三天的批次找出来,写入一张预警通知表或者直接更新商品表的预警状态字段。页面端在登录后的主框架页加一个提醒区域,显示「有 N 个批次临近保质期」。核心代码结构如下:
@Component public class ExpireCheckTask { @Autowired private BatchMapper batchMapper; @Scheduled(cron = "0 0 2 * * ?") public void checkExpiringBatches() { List<GoodsBatch> list = batchMapper.selectExpiringBatches(3); for (GoodsBatch batch : list) { batchMapper.updateExpireWarning(batch.getId(), 1); } } }这段代码逻辑是:@Scheduled里的 cron 表达式表示每天早上两点执行一次;selectExpiringBatches(3)查询出所有剩余保质期小于等于三天的批次;对查出来的批次把预警标记置为 1。参数 3 就是预警阈值,你可以根据业务场景调成 1 或者 7。
Mapper 里对应的查询 SQL 是:
<select id="selectExpiringBatches" resultType="GoodsBatch"> SELECT * FROM goods_batch WHERE expire_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL #{days} DAY) AND quantity > 0 </select>这段 SQL 的条件逻辑是:过期时间在现在到 N 天之后之间,并且还有剩余库存。之所以排除quantity <= 0的批次,是为了避免已经卖完的批次还打扰预警列表。验证这个方法很简单:造一条测试数据,把 expire_date 改成明天,手工调用一次任务,再去页面看预警区域有没有出现这条记录。这个动作本身就是答辩演示时的绝佳素材——你不仅做了预警功能,还有验证过程和数据支撑。
我在自己做类似项目时有个血泪经验:最早把保质期逻辑只做在商品表上,以为存一个总保质期天数就行,后来发现同一商品不同批次根本没法区分,最后全部重构才理顺。所以如果你按这个方向做,一定把批次表作为核心,商品表只放档案信息,所有库存动作落到批次维度上。这个设计决策在答辩时讲出来,比堆功能更有说服力。
除此之外,还可以把预警做成可配置的,在系统参数表里存一个 warn_days 字段,让管理员自己设置提前几天提醒。改动量不大,但功能完整度会明显上台阶。希望这些内容帮到你,把毕设做成一个敢在演示时点开每个页面的作品。
本文还有配套的精品资源,点击获取