简介:面向高校计算机与软件工程专业毕业设计场景的超市管理系统完整项目源码,采用SpringBoot后端与Vue前端的前后端分离架构,覆盖商品库存、销售、会员、促销、供应链及报表统计等模块,适合需要完成毕设选题、课程设计或学习企业级项目结构的学生与开发者参考。压缩包共2000个文件,约23.17MB,包含大量JavaScript与CSS前端资源、61个Java后端源码、43个Vue组件,以及SQL脚本、XML配置、JSON数据和HTML页面等,后端服务、前端界面、数据库初始化与部署配置均有对应材料。已有87人学习下载。读者可据此梳理前后端分离项目的目录组织、接口调用、权限控制与库存销售流程,并借助数据库脚本快速还原运行环境,理解模块划分和常见配置,为二次开发与答辩展示提供可复用的工程模板。
1. 超市管理系统毕设选型:SpringBoot+Vue 这套组合到底能不能打
去年帮几个同学看毕业设计,十份里有六份是“超市管理系统”,但真正能跑起来、答辩时经得起追问的不到三份。问题不在题目老旧,而在于多数人只堆了 CRUD,没想清楚一套零售后台到底该有哪些业务闭环。这份基于 SpringBoot + Vue 的超市管理系统,覆盖商品档案、库存出入库、收银下单、供应商与会员管理几条主线,前后端分离,数据库用 MySQL,属于那种“拿来能跑、拆开能改”的典型工程型毕设。它适合两类人:一是时间紧、需要一套结构完整可演示系统的毕业生;二是想借一个真实业务场景,把 SpringBoot 分层、Vue 路由与权限、订单事务这几块练熟的新手。下面按“先跑通、再改懂、最后避坑”的顺序拆。
2. 环境搭建与前后端跑通:从零到登录页出现的完整链路
2.1 技术栈清点与版本对齐
拿到一个 Java 毕设压缩包,第一件事不是双击 IDEA 打开,而是先看依赖清单。这套系统的典型结构是后端 SpringBoot + MyBatis-Plus + MySQL,前端 Vue + Element UI(或 Element Plus)+ Axios,构建工具 Maven 与 npm。前后端分离项目的翻车高发区,几乎全在版本上:JDK 8 和 JDK 17 对 SpringBoot 2.x/3.x 的支持完全不同,Node 版本过高会让老版本 node-sass 直接编译失败。
常见做法是先统一下面这张表,再动手:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 17 | 与 SpringBoot 主版本匹配,2.x 配 8,3.x 配 17 |
| SpringBoot | 2.7.x | 毕设生态最稳,教程与依赖最多 |
| MySQL | 5.7 / 8.0 | 8.0 需注意驱动类名与时区参数 |
| Maven | 3.6+ | 配好国内镜像,否则依赖拉取极慢 |
| Node.js | 14.x / 16.x | 老 Vue2 项目不要上 18+ |
| Vue | 2.x 或 3.x | 看 package.json,别自己臆测 |
我一般会先把pom.xml和package.json各扫一遍,把版本号记下来,再决定装哪套环境。这一步花五分钟,能省后面两小时的报错排查。
2.2 数据库初始化与配置修改
超市管理系统的表通常有十几张:商品表、分类表、库存表、入库单、出库单、订单主表与明细表、供应商表、会员表、用户与角色表。压缩包里一般带一个.sql文件,先建库再导入。
# 登录 MySQL 后执行 CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 退出后用命令行导入,避免图形工具编码问题 mysql -u root -p supermarket < supermarket.sql导入完成后必须改后端配置文件,这是新手最容易漏的一步:
# application.yml 关键片段 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8 用 cj,5.7 可去掉 cj url: jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码逻辑说明:serverTimezone不写会在 MySQL 8 上抛出时区异常;useSSL=false是为了本地开发免证书告警;characterEncoding=utf8保证中文商品名不乱码。数据库账号密码写错的表现是启动即报Access denied,此时不用怀疑代码,先回到这里核对。
2.3 后端启动与接口自测
改完配置直接跑主启动类,控制台出现 Tomcat 端口监听即算成功,默认 8080。启动失败常见两类:一是端口被占用,改server.port即可;二是 MyBatis 的 mapper 扫描路径没配,报Invalid bound statement,需要在启动类加@MapperScan("com.xxx.mapper")。
起来之后别急着开前端,先用浏览器或接口工具打一个登录接口:
# 测试登录接口是否连通,注意实际路径以 controller 注解为准 curl -X POST http://localhost:8080/user/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'参数说明:Content-Type必须是 JSON,后端用了@RequestBody时表单格式会直接 415;返回体里一般带code、msg、data三段,code为 200 说明链路通了。如果返回 401 或提示密码错误,去数据库用户表看密码是否经过 MD5 或 BCrypt 加密——很多包里的初始密码是加密后的串,直接比对明文当然对不上。
2.4 前端安装依赖与跨域处理
前端目录下先装依赖再启动:
npm install # 依赖多时加 --registry 走国内源 npm run serve # Vue2 项目;Vue3 用 npm run dev启动后浏览器访问 8080 或 8081,能出现登录页就成功一半。此时最常见的坑是登录请求 404 或跨域报错。原因在于前端 Axios 的 baseURL 与后端实际端口不一致,或者后端没配跨域。两种解法:一是在vue.config.js里配代理,二是后端加全局跨域配置。推荐前者,配置干净:
// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 指向后端 changeOrigin: true, pathRewrite: { '^/api': '' } // 按后端是否有统一前缀决定 } } } }逻辑说明:pathRewrite最容易配错,如果后端接口本身带/api前缀,这行要删掉,否则会变成双前缀导致 404。改完配置必须重启前端服务,热更新不会重载代理配置,这一点很多人栽过。
3. 核心业务模块拆解:商品、库存与订单三条主线怎么落地
3.1 商品与分类的分层写法
商品模块是整个系统的地基,分类表与商品表是一对多关系。后端典型分层是 Controller 接参、Service 写业务、Mapper 做持久化,实体类用 MyBatis-Plus 注解映射表名。看一份商品分页查询的 Service 实现就明白套路:
@Service public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService { @Override public Page<Goods> pageQuery(int pageNum, int pageSize, String keyword, Long categoryId) { // 构造分页对象,页码从 1 开始 Page<Goods> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); // 商品名模糊搜索,库存预警可在此追加条件 wrapper.like(StringUtils.hasText(keyword), Goods::getName, keyword); wrapper.eq(categoryId != null, Goods::getCategoryId, categoryId); wrapper.orderByDesc(Goods::getCreateTime); return this.page(page, wrapper); } }逻辑说明:LambdaQueryWrapper的条件方法第一个参数是布尔值,为 false 时该条件不拼接,这是实现“动态查询”最省事的方式,比手写 XML 里一堆<if>清爽。参数上pageNum从 1 开始,若前端分页组件从 0 开始传,会出现第一页数据重复,这是联调时的高频问题。分类下拉框数据则单独走一个/category/list接口,前端在created钩子里拉一次缓存起来即可,不必每次打开弹窗都请求。
3.2 入库出库与库存联动的写法
库存是超市系统区别于普通 CRUD 的地方。核心逻辑是:入库单审核通过后增加库存,出库或销售后扣减库存,并且要防止超卖。简单实现是在 Service 里用一个事务包住“写单据 + 改库存”两步:
@Service public class StockServiceImpl { @Autowired private GoodsMapper goodsMapper; @Autowired private StockRecordMapper stockRecordMapper; @Transactional(rollbackFor = Exception.class) public void inbound(Long goodsId, Integer num, String operator) { // 先查商品是否存在,避免脏数据 Goods goods = goodsMapper.selectById(goodsId); if (goods == null || num <= 0) { throw new RuntimeException("商品不存在或数量非法"); } // 更新库存,用 SQL 原子自增而不是先查后改 int rows = goodsMapper.increaseStock(goodsId, num); if (rows == 0) { throw new RuntimeException("库存更新失败"); } // 记录流水,便于对账与追溯 stockRecordMapper.insert(new StockRecord(goodsId, num, "IN", operator)); } }逻辑说明:@Transactional(rollbackFor = Exception.class)必须显式指定回滚异常类型,默认只回滚运行时异常,捕获后不抛会静默失败。库存更新走UPDATE goods SET stock = stock + #{num} WHERE id = #{id}这种原子语句,而不是“查出库存、加完再写回”,后者在并发下必然丢更新。参数operator记录操作人,答辩时被问“怎么追溯谁改的库存”就靠这张流水表。
3.3 订单与收银的事务边界
收银下单是业务闭环的终点,涉及订单主表、订单明细表、库存扣减三处写入,必须放在同一个事务里。这里有个容易忽略的细节:明细表要先有主表 ID,所以顺序是插主表拿自增 ID、批量插明细、逐条扣库存。
@Transactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { Order order = new Order(); order.setOrderNo(generateOrderNo()); // 建议 时间戳+随机数,避免并发重复 order.setMemberId(dto.getMemberId()); order.setTotalAmount(dto.getTotalAmount()); orderMapper.insert(order); // 插入后 order.getId() 自动回填 for (OrderItem item : dto.getItems()) { item.setOrderId(order.getId()); orderItemMapper.insert(item); // 扣库存带库存充足判断,返回 0 行说明库存不足 if (goodsMapper.decreaseStock(item.getGoodsId(), item.getNum()) == 0) { throw new RuntimeException("商品库存不足:" + item.getGoodsName()); } } }逻辑说明:generateOrderNo如果用纯时间戳,秒级并发下会撞号,加随机后缀或使用序列更稳。扣库存用UPDATE ... SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},靠影响行数判断是否超卖,比先查再判可靠得多。会员价、折扣计算建议放在 Service 里统一算,前端只做展示,否则改个活动规则要动两处。
4. 权限与数据展示:菜单、角色和报表这几处最容易做假
4.1 登录鉴权与角色菜单
毕设系统里权限往往是“做了但不严”,表现为登录后菜单写死,换个角色照样能看到全部页面。合格的做法是后端返回角色标识或菜单树,前端按角色动态渲染路由。登录成功后把 token 与用户信息存本地,请求拦截器统一带上:
// request.js 拦截器片段 service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; // 后端过滤器解析 } return config; });逻辑说明:token 存localStorage刷新不丢,但存在被脚本读取的风险,毕设场景够用;生产环境更常见的是 HttpOnly Cookie。后端用拦截器或过滤器校验,未登录统一返回 401,前端拦截响应码跳登录页。菜单树一般存角色表的一个 JSON 字段或单独菜单表,别在数据库里给每个用户存一份菜单 ID 列表,后面加页面会改到崩溃。
4.2 库存预警与统计报表
答辩时最容易被问的两个问题:库存不足怎么办、销售数据怎么看。库存预警就是一条查询:stock < warning_stock的商品列表,前端用红色标签标出,进入首页时拉一次。统计报表则按日期分组聚合订单明细,常见三种:日销售额趋势、商品销量排行、分类销售占比。
-- 近 7 天销售额趋势,按天分组 SELECT DATE(create_time) AS day, SUM(total_amount) AS amount FROM `order` WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day;逻辑说明:DATE_SUB(CURDATE(), INTERVAL 6 DAY)表示含今天在内共 7 天,若写成INTERVAL 7 DAY会多出一天导致图表空点。销量排行用GROUP BY goods_id ORDER BY SUM(num) DESC LIMIT 10。前端图表用 ECharts 即可,数据为空时记得给个空态提示,否则图表一片空白,演示时很尴尬。
4.3 前后端联调的字段对齐
联调阶段最耗时的不是写代码,是字段对不上。后端实体用驼峰goodsName,数据库列名用下划线goods_name,MyBatis-Plus 默认开启驼峰映射,一般不用管;但前端表单 model 里的字段名必须和后端 DTO 完全一致,多一个字母就是null。我习惯先把接口文档或 Controller 的入参对象打印出来,对着写前端表单,比来回猜快得多。日期字段尤其注意:后端返回yyyy-MM-dd HH:mm:ss字符串还是时间戳,前端表格的格式化函数要跟得上,否则显示Invalid Date。
5. 避坑与排查:这套系统跑不起来时先看这五处
5.1 启动报数据库连接失败
现象:启动即退出,日志里Communications link failure或Access denied for user。原因通常是 MySQL 没启动、端口不是 3306、账号密码与配置不符,或者 MySQL 8 用了旧驱动类名。解决:先命令行mysql -u root -p确认能连上,再核对application.yml的 url、用户名、密码三处,驱动类名 8.0 必须是com.mysql.cj.jdbc.Driver。
5.2 接口 404 或跨域报错
现象:登录按钮点下去,控制台报 CORS 或 404。原因:前端 baseURL 与后端端口不一致,代理pathRewrite配错导致路径多一层或少一层。解决:浏览器 Network 面板看实际请求地址,和后端 Controller 的@RequestMapping逐段比对,先去掉pathRewrite试一次,能通说明就是前缀问题。
5.3 页面能开但数据全空
现象:列表页加载转圈后无数据,接口返回 200 但data为 null。原因:分页参数名不匹配(前端传page,后端收pageNum),或返回结构是{records:[], total:0}而前端取的是data.list。解决:直接看接口返回报文,按实际字段名改前端取值,别硬改后端去迁就前端。
5.4 中文乱码
现象:商品名在数据库里正常,页面显示问号。原因:数据库连接串缺characterEncoding=utf8,或建库时用了 latin1。解决:连接串补上编码参数,已建库的执行ALTER DATABASE supermarket CHARACTER SET utf8mb4,表也需一并转换。
5.5 修改后不生效
现象:改了代码重启还是老样子。原因:前端代理配置、环境变量这类改动必须重启 dev server;后端若开了热部署,改 mapper XML 有时不重载。解决:养成“改配置必重启、改 XML 必 rebuild”的习惯,别在缓存上浪费时间。
6. 二次开发与答辩加分:把一套 CRUD 改成有说服力的作品
想把这份系统从“能跑”推到“能讲”,重点不在加页面,而在补业务深度。我一般会先做三件事:给库存扣减加乐观锁或原子更新,证明自己懂并发;给订单加状态机(待支付、已支付、已取消、已退款),让流程有来有回;给关键操作加日志表,答辩被问“数据错了怎么查”时有话可说。
再往上可以挑一个点做深,比如销售预测或智能补货。不必上复杂模型,用近 30 天日均销量乘以补货周期,减去当前库存和安全库存,得出建议补货量,就是一个能自圆其说的算法:
// 简易补货建议:日均销量 * 补货周期 + 安全库存 - 当前库存 public int suggestReplenish(Long goodsId, int cycleDays, int safeStock) { Integer sold = orderItemMapper.sumRecentDays(goodsId, 30); // 近30天销量 if (sold == null || sold == 0) return safeStock; // 无销量按安全库存备 int avgDaily = sold / 30; int suggest = avgDaily * cycleDays + safeStock - goodsMapper.getStock(goodsId); return Math.max(suggest, 0); // 负数归零 }参数说明:cycleDays是供应商到货周期,safeStock是安全库存下限,两者都可以做成系统参数配置,演示时现场改一个值看建议变化,比干讲有说服力。验证方法也简单:挑三个销量差异大的商品,手工算一遍和建议值对比,对得上就说明逻辑没错。
另外提醒一句,答辩演示前一定用干净数据跑一遍全流程:建商品、入库、开会员、下单、退款、看报表,每一步都截图存档。我吃过一次亏,演示时库存流水为空,被追问是不是库存没联动,其实是测试数据被清过。从那以后我每次交付前都强制走一遍完整业务链路再清库重导,宁可多花二十分钟。希望帮到你。
本文还有配套的精品资源,点击获取