SpringBoot+Vue超市管理系统:完整源码拆解与毕业设计二次开发指南

发布时间:2026/10/23 17:59:25
SpringBoot+Vue超市管理系统:完整源码拆解与毕业设计二次开发指南

简介:基于SpringBoot与Vue的超市管理系统是一套可直接运行的前后端分离毕业设计项目,面向计算机专业学生和Java全栈初学者,覆盖商品库存、销售数据、会员信息、促销活动及供应链等核心业务,能够帮助读者快速理解现代Web系统从后端服务到前端交互的完整搭建流程。压缩包共包含2000个文件,大小约23.17MB,以js脚本和css样式占据多数(分别为1614个和233个),另有61个java后端源码、43个vue前端组件、sql数据库初始化脚本及xml、json、html等配置文件,层次清晰,便于按模块检索。项目内设用户权限管理、实时库存监控、销售事务处理、报表统计与系统设置等典型功能模块,同时展示SpringBoot自动配置、嵌入式服务器与Vue组件化、路由状态管理等关键技术的实际应用,并附带可直接导入的数据库初始化脚本,可独立部署体验,也可作为课程设计、毕业答辩或二次开发参考。已有87人学习下载,适合需要从零搭建前后端分离项目并希望获得完整工程结构的开发者。

1. 基于SpringBoot+Vue的超市管理系统:这不是课程设计,是一套能直接答辩的完整代码

做毕设想找个“能跑、能改、能讲”的项目,超市管理系统往往是第一个跳进脑子里的名字。但真正上手时,很多人卡在第一关:下载的源码要么缺数据库,要么前端启动就报错,要么后端接口和前端页面对不上。这套基于SpringBoot+Vue的超市管理系统,我拆了两天,结论是:它不属于“教材Demo”,而是一套结构完整的毕业设计级代码——后端有完善的登录鉴权、商品CRUD、销售统计,前端有Element UI的管理后台界面,数据库脚本齐全,改个连接配置就能跑通全流程。特别适合正在做毕业设计、需要快速搞清楚“代码怎么组织、接口怎么设计、二次开发怎么入手”的人。

如果你只想找个能跑的系统交差,它合格;如果你想在答辩时讲清楚“我做了什么”,它的模块划分和注释也够你用。下面的内容,我会按“项目长什么样 → 怎么跑起来 → 核心代码怎么讲 → 怎么改造成自己的”来拆,全程按一线开发视角,不绕弯。

2. 系统全貌与结构盘点:先看清这个项目的“骨架”再动手

2.1 技术栈与项目定位

先从技术选型说起。这类管理系统,SpringBoot + Vue 是最成熟、也是答辩时最稳妥的组合,这套源码的技术栈属于“经过大量同类项目验证过的标配”:后端是 Spring Boot 2.x 搭配 MyBatis Plus,数据库用的 MySQL,安全性上使用了 JWT 做无状态登录鉴权,前端是 Vue 2 + Element UI + Axios,构建工具是 npm 生态。之所以说它是“标配”而不是“过时”,是因为这套组合的教学案例最多、遇到问题最容易搜到解决方案,对需要快速消化项目的开发者来说,这就是最大的优势。

这套系统里最值得看的设计是“前后端分离”:前端工程和后端工程是两个完全独立的目录,通过 HTTP 接口通信,前端不直接访问数据库。这种设计的好处是,你在答辩时可以理直气壮地说“我采用了前后端分离架构”,而且后端的接口可以单独用 Postman 测试,不用每次调接口都打开页面。

2.2 目录结构速览:代码放在哪,心里要有个数

拿到源码后,第一件事不是急着启动,而是先把目录结构认清楚。这套项目的目录规划是比较符合工程规范的:根目录下通常分成backend(后端)和frontend(前端)两个平行模块,有些版本还会附带sql或database目录来存放初始化脚本。

supermarket-system/ ├── backend/ # Spring Boot 后端工程 │ ├── src/main/java/ # 后端 Java 源码 │ ├── src/main/resources/ # 配置文件、mapper XML │ └── pom.xml # Maven 依赖描述 ├── frontend/ # Vue 前端工程 │ ├── src/ # 前端源码(views/components/api) │ ├── package.json # npm 依赖描述 │ └── vue.config.js # 前端开发服务器配置 └── sql/ # 数据库脚本(建库建表+初始化数据)

后端 Java 源码内部会按照经典的三层架构分包:controller管接口路由,service管业务逻辑,mapper管数据库操作。实体类(entity)和工具类(utils)会单独放一层。前端则按照views(页面)、components(组件)、api(接口封装)来组织。这样一个包对应一个职责,不管是答辩还是二次开发,都能快速定位代码位置。

2.3 功能模块划分:不是只有增删改查的“空壳”

很多毕设系统的问题在于功能太薄——只有用户表和一张商品表来回增删改查。这套系统在功能上做得比较深入,核心功能模块覆盖了超市管理的典型业务链:登录与权限控制,商品分类与商品信息管理,库存管理,供应商和进货记录,销售结账(购物车模式),以及销售记录统计。每个模块都有对应的数据库表和完整的接口支撑,不是前端写死了静态数据来充数。

比如“商品管理”不是只放一张商品表就完事,而是拆了分类、供应商、库存多条关联关系;“销售结账”模拟了真实超市场景——选择商品后先加入购物车,结账时一次性扣除库存并生成销售单。这种设计带来的好处是,你在答辩时可以讲出一个“业务闭环”,而不是停留在单表操作上。

2.4 数据库设计:表结构背后的关联关系

数据库是这个项目的灵魂,拿到sql目录下的脚本文件后(通常名为supermarket.sql),我建议你先用 Navicat 或者命令行导入执行,然后重点关注表之间的关系。这套系统的数据表设计比较规范,核心表包括:sys_user(用户/管理员表)、goods_category(商品分类表)、goods_info(商品信息表)、supplier(供应商表)、stock_info(库存表)、sale_records(销售记录表),以及sale_detail(销售明细表)等。

表之间的关联是典型的“主键-外键”逻辑:goods_info通过category_id关联分类表,通过supplier_id关联供应商表,sale_detail通过sale_id关联销售记录主表,通过goods_id关联商品表。这些关联在外键约束上不一定严格设置(很多毕设项目为了数据灵活性会省略物理外键),但逻辑关系在接口层是通过 MyBatis Plus 的查询条件来体现的。如果你想让数据库设计的含金量看起来更高,可以在答辩 PPT 里画一张 ER 图,把这张表关系放进去。

3. 本地运行实战:从零启动前后端,把项目跑通在浏览器里

3.1 后端启动:改一处配置,剩下的交给 Maven

后端工程是标准的 Maven 项目,启动前需要保证本机具备 Java 8 或 11 环境、Maven 3.6+,以及 MySQL 5.7 或 8.x。拿到代码后,第一步是把sql目录下的脚本导入本地数据库。我在操作时习惯这样执行(库名根据实际脚本调整):

# 登录 MySQL 后执行,假设数据库名为 supermarket_db mysql -u root -p < sql/supermarket.sql

脚本执行成功后,可以用SHOW TABLES;看看是否生成了上面提到的那些表。接下来打开后端配置文件application.yml(或者.properties),这是启动前唯一必须改动的地方:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/supermarket_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 # 改成你自己本地的密码,别用博客里抄来的 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: logic-delete-field: isDeleted

这里最值得注意的两个坑,我很有发言权。第一,serverTimezone一定要加上,否则 MySQL 8.x 会报“无法解析时间区”的错;第二,用户名密码要改成你本地能连上 MySQL 的账号。项目拿默认密码root/root是跑不通的,因为每个人的本地 MySQL 初始化密码不一样。改完之后,用 IDEA 打开工程,等 Maven 依赖下载完,直接运行src/main/java下的启动类即可。如果是在命令行操作,用mvn spring-boot:run也能达到同样效果。

启动成功的标志是控制台出现 Tomcat started on port 8080,此时后端就绪。你可以先用浏览器访问http://localhost:8080/api/user/login做一个快速测试,如果返回错误信息“用户名不能为空”,说明路由已经通了,只是参数不完整。

3.2 前端启动:npm install 与开发服务器

前端是标准的 Vue CLI 工程,启动方式很固定。建议用 Visual Studio Code 打开frontend目录,在终端执行两段命令:

# 第一步:安装依赖,时间取决于网络状况,几分钟到十几分钟不等 npm install # 第二步:启动开发服务器 npm run serve

执行完第二段命令后,控制台会输出一个本地访问地址(通常是http://localhost:8081或http://localhost:3000),打开它就能看到系统的登录页面。这里有一个很容易被忽视的细节:前端开发服务器默认监听的是 8080 端口,而后端 Spring Boot 也占用了 8080,两者会冲突。因此,项目里的vue.config.js一般会做一个端口偏移和代理转发:

module.exports = { devServer: { port: 8081, // 前端页面端口 proxy: { '/api': { target: 'http://localhost:8080', // 后端接口地址 changeOrigin: true } } } }

配置里的小逻辑是这样:前端把请求发到http://localhost:8081/api/xxx,开发服务器会把/api开头的请求原样转发到http://localhost:8080/api/xxx,从而绕开浏览器的跨域限制。很多人在这一步卡住,症状是前端页面打开了、但登录时一直提示“网络错误”,原因多半是代理没配好,或者忘了修改target到自己实际的后端端口。

3.3 常见运行问题排查与血泪经验

在跑这个源码的过程中,有三个问题属于高频“杀手”,我直接给你结论,省得自己去踩坑。

问题一:数据库连接失败。现象是后端启动时直接报Communications link failure或者Access denied for user。原因基本是application.yml里的密码错误,或者 MySQL 服务没有启动。解决方式很简单:先确认 MySQL 服务开启(Windows 用服务管理器,Mac 用brew services list),再核对用户名密码,本地不确定密码的先去 MySQL 命令行验证。

问题二:npm install 报错或极慢。现象是安装依赖过程中卡在某个包上,或者直接报node-gyp相关错误。原因通常有两个:Node 版本过老或过新、npm 默认源在国外。解决办法是给 npm 配置国内镜像源,命令是npm config set registry https://registry.npmmirror.com,然后删除node_modules目录重新 install 一次。为了避免玄学问题,建议直接用 Node 14 或 Node 16,这两个版本对 Vue CLI 项目兼容性最好。

问题三:前端能打开但登录时提示“跨域”。现象是浏览器控制台报CORS policy或ERR_PROXY_CONNECTION_REFUSED。原因是你可能直接访问了http://localhost:8080这个端口(后端端口),而不是通过前端开发服务器(8081)打开页面。正确操作是始终用npm run serve输出的那个地址访问系统,通过代理转发请求,而不是直接访问后端的服务端口。

4. 核心模块拆解:一个请求从进入到返回的完整链路

4.1 登录鉴权模块:JWT 是怎么保护接口的

登录模块是后端唯一不经过鉴权的接口,也是整个系统权限控制的起点。从代码结构上,后端通过一个拦截器(Interceptor)来拦截除登录接口以外的所有请求,拦截器会检查请求头里的token字段。前端在登录成功后会把 token 存到localStorage,之后每次请求都通过 Axios 拦截器自动带上 token。

先看后端的 JWT 工具类,这段代码是整套系统的核心依赖,建议逐行读懂:

public class JwtUtil { // 签名密钥,实际项目中应该放到配置文件中 private static final String SECRET = "supermarket-secret-key"; // token 有效期,设定为 24 小时 private static final long EXPIRE = 24 * 60 * 60 * 1000; // 生成 token public static String createToken(Integer userId, String username) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + EXPIRE); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(username) .setIssuedAt(nowDate) .claim("userId", userId) // 自定义字段 .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析 token,获取用户名 public static String getUsername(String token) { return parseToken(token).getSubject(); } // 解析 token,获取用户ID public static Integer getUserId(String token) { return (Integer) parseToken(token).get("userId"); } }

这段代码的逻辑并不复杂:创建 token 时放入用户名、用户ID和过期时间,解析 token 时取出这些信息。重点在于claim("userId", userId)这行——它允许你在后续的业务代码里直接拿到当前登录用户是谁,而不必每次查数据库。拦截器中的判断逻辑通常是这样:先尝试从请求头获取 token,如果 token 为空或解析失败,就直接向响应写入“401 未登录”的 JSON,拦截掉请求;如果解析成功,则放行并把用户ID传递到业务层。

前端配合的部分是 Axios 拦截器,在request.js或http.js中:

axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['token'] = token; // 后端从请求头取 } return config; }) axios.interceptors.response.use(response => { const data = response.data; if (data.code === 401) { // token 过期,跳回登录页 router.push('/login'); } return data; })

这段代码的作用是让前端每次请求都自动携带 token,并在 token 失效时统一跳回登录页。注意它的应用场景很典型:比如用户登录后停留了很久,token 过期了,下次点击任何操作都会被拦截器踢回登录页。你可以在答辩时这样讲:“我通过前端拦截器统一处理登录态,而不是在每个页面里重复判断”,这句就体现了工程化思维。

4.2 商品管理模块:分页查询的前后端协作

商品列表是管理系统的门面,也是兼顾实用性的核心模块。后端拿到分页参数(当前页和页大小),用 MyBatis Plus 的Page对象执行分页查询,返回给前端的数据结构包含records(每页数据)、total(总条数)、current(当前页)等字段。前端拿到这些数据后通过 Element UI 的el-table渲染成表格。

后端商品查询的 Service 层代码:

@Override public PageResult<GoodsInfo> selectGoodsPage(int page, int pageSize, String goodsName, Integer categoryId) { // 构造查询条件 LambdaQueryWrapper<GoodsInfo> wrapper = new LambdaQueryWrapper<>(); // 名称不为空时,按名称模糊查询 if (StringUtils.isNotBlank(goodsName)) { wrapper.like(GoodsInfo::getGoodsName, goodsName); } // 分类ID不为空时,按分类精确过滤 if (categoryId != null) { wrapper.eq(GoodsInfo::getCategoryId, categoryId); } // 按最后修改时间倒序排序 wrapper.orderByDesc(GoodsInfo::getUpdateTime); // 执行分页查询 Page<GoodsInfo> pageInfo = new Page<>(page, pageSize); Page<GoodsInfo> result = goodsInfoMapper.selectPage(pageInfo, wrapper); // 组装返回结果 PageResult<GoodsInfo> pageResult = new PageResult<>(); pageResult.setRows(result.getRecords()); pageResult.setTotal(result.getTotal()); return pageResult; }

这里的核心技巧在于LambdaQueryWrapper—— 它避免了字符串拼接 SQL 的风险,也让代码在编译期就能发现字段名错误。比如你如果把GoodsInfo::getGoodsName写成getGoodsPrice,编译直接报错;而如果写字符串“goods_name”,只有运行时才会暴露问题。这种写法建议保留,答辩时被问“你如何防止 SQL 注入”,直接指这段代码。

前端商品列表页的核心逻辑是把后端返回的数据映射到表格中:

<el-table :data="tableData" v-loading="loading" border stripe> <el-table-column prop="goodsName" label="商品名称" width="180"></el-table-column> <el-table-column prop="goodsPrice" label="售价" width="120"></el-table-column> <el-table-column prop="stock" label="库存" width="120"></el-table-column> <el-table-column label="操作" width="200"> <template slot-scope="scope"> <el-button size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table>

参数说明:prop对应后端返回 JSON 字段名,label是表格列的显示名,slot-scope用来获取当前行的数据。如果后端返回的字段名和前端对不上,表格会显示为空白,这是前后端联调时最容易踩的坑之一。联调的标准做法是:先打开浏览器 F12 查看网络请求的响应体,确认 JSON 字段名,再回到前端修改prop属性。

4.3 销售结账模块:购物车与库存扣减的完整闭环

销售结账是这套系统业务逻辑的“含金量”所在。它模拟了真实收银台的场景:用户选择商品、加入购物车,点击结账后系统生成一条销售记录,同时把商品库存减去对应数量。实际操作中,前端会维护一个购物车数组,包含goodsId、goodsName、price、count字段,提交结账时把整个数组发给后端。

后端 Controller 层处理销售单提交的接口:

@PostMapping("/sale") public Result saveSale(@RequestBody SaleDTO saleDTO) { // saleDTO 中包含购物车商品列表和总金额 List<SaleItemDTO> items = saleDTO.getItems(); // 构建销售记录主表数据 SaleRecords saleRecord = new SaleRecords(); saleRecord.setSaleNo(generateSaleNo()); // 生成唯一单号 saleRecord.setTotalAmount(saleDTO.getTotalAmount()); saleRecord.setSaleUserId(CurrentUserHolder.getUserId()); // 从 token 获取 saleRecord.setSaleTime(new Date()); saleRecordsMapper.insert(saleRecord); // 插入主表 // 循环处理每个商品:扣减库存 + 插入销售明细 for (SaleItemDTO item : items) { // 1. 扣减库存,SQL 中带上 stock >= count 条件防止负数 int result = stockInfoMapper.deductStock(item.getGoodsId(), item.getCount()); if (result == 0) { throw new RuntimeException("商品 " + item.getGoodsName() + " 库存不足"); } // 2. 插入销售明细 SaleDetail detail = new SaleDetail(); detail.setSaleId(saleRecord.getId()); detail.setGoodsId(item.getGoodsId()); detail.setGoodsName(item.getGoodsName()); detail.setPrice(item.getPrice()); detail.setCount(item.getCount()); saleDetailMapper.insert(detail); } return Result.success("结账成功"); }

这段代码里最值得讲的是库存扣减:deductStock方法在 mapper 层执行的是带条件的 UPDATE 语句,即UPDATE stock SET stock = stock - #{count} WHERE goods_id = #{goodsId} AND stock >= #{count}。这种做法能够避免并发下单时把库存扣成负数,是实际生产环境里防超卖的简化版实现。你可以在答辩时说“我在 SQL 层面用带条件的更新来保证库存数据的正确性”,这句值得重点强调。

前端购物车界面则会根据后端接口返回错误信息做提示,比如库存不足时弹窗提示“商品XXX库存不足”。整个销售链路是前后端配合最紧密的部分,建议你拿到代码后,用三个商品各买两个,然后再去数据库查看sale_records和sale_detail两张表里的数据,验证业务闭环是否成立。

5. 二次开发实战指南:如何把这套代码改造成你自己的毕设

做毕设最怕的是“拿别人的代码直接交,一问就露馅”。这套系统虽然完整,但要变成你自己的,必须做几处有区分度的改造。我给的思路是:只改一处核心业务,加一个完整功能,替换一套外观。

5.1 汉堡式改造方案:从基础优化到独立功能

第一档改造,替换主题色和系统名。这是投入产出比最高的小改:把前端src目录下App.vue和assets样式文件里的主题色变量统一改掉,把登录页的标题从“超市管理系统”改成你自己的名字(比如“校园小卖部管理系统”“生鲜便利店进销存系统”),后端返回的Result类里msg字段的默认提示文案也一并替换。这处改动虽然简单,但能让页面整体的视觉感受“脱胎换骨”。

第二档改造,优化订单号生成规则。现在源码里的generateSaleNo()大概率是利用当前时间戳拼出来的纯数字单号,你可以改成“前缀+日期+随机数”的结构。比如:XS202405211530001234,前缀XS代表销售,接年月日时分秒,最后四位是随机数。这样在展示层上比纯时间戳更有设计感,也方便日后按单号前缀做统计。改造点在后端service.impl的销售单号工具方法中,改动量很小但效果直观。

第三档改造(推荐方案),增加一个“销售排行榜”统计模块。在商品管理页面新增一个 Tab 或独立页面,后端通过查询sale_detail表按商品ID分组统计销量,用小组件或列表展示排名前10的商品。这个功能只需要新增一个接口、一个前端页面,但业务逻辑闭环、演示效果好、答辩时能讲“数据分析”,属于性价比很高的毕设加分项。

后端统计接口的核心代码长这样:

// 按商品ID分组,统计销售总数,按数量倒序返回前10条 @GetMapping("/sale/top10") public Result getTop10() { List<Map<String, Object>> list = saleDetailMapper .selectMaps(new QueryWrapper<SaleDetail>() .select("goods_id, SUM(count) as total_count") .groupBy("goods_id") .orderByDesc("total_count") .last("limit 10")); return Result.success(list); }

说明:selectMaps返回List<Map>,每一行一个商品ID和它的售出总数,然后你在前端拿这个goods_id再去关联查询商品名称和价格,拼装成排行榜展示。注意selectMaps里 SQL 用了limit 10,这是 MySQL 的方言,如果你把数据库切到 PostgreSQL 就需要换成limit 10 offset 0语法,改动点很小。

5.2 大改思路:单模块重构带来差异化

如果时间充裕,建议对“库存管理”做一次模块级重构。源码里的库存可能只是一个简单的数字字段,你可以扩展成“库存预警”机制:在商品表增加stock_warn字段,当库存低于该阈值时,前端列表用红字或标记显示“库存不足”,并在登录后的首页增加一个“预警商品”的统计卡片。这个改造涉及数据库加字段、后端实体类加属性、前端加判断逻辑三个层面的改动,完整跑下来,你就能从头到尾理解一次“需求驱动开发”的流程。

验证改造是否成功的方法很简单:把库存数量改到一个极低值(比如1),刷新前端页面,观察列表页是否呈现预警状态;卖出一件商品后,再确认预警是否自动消失。整个链路只需要在源码上改不到20处代码,但对项目的理解深度完全不一样。

5.3 关于运维部署和个人化的额外提醒

如果这个项目要用于在线演示,一定要记得在后端application.yml里把 JWT 的SECRET和前端页面上的默认账号密码都改掉。很多同类项目的安全隐患是账号密码硬编码、默认密码未修改,答辩现场如果被评委用默认密码登进去,场面会很尴尬。从那以后我拿到任何新项目,第一件事就是全局搜索admin和123456,把硬编码的默认账号密码全局替换掉,再动手改业务。

另一个容易被忽略的细节是后端接口的统一返回格式。源码里如果定义了Result类(含code、msg、data三个字段),建议保持住这种风格,不要在这个类上做大的改动,因为前端 Axios 拦截器已经按照code字段做了统一分支处理,改返回结构会让所有前端的响应判断失效。如果一定要调整,前后端点要同步改,否则就是给自己挖坑。

希望这篇拆解能帮你把这套系统真正跑起来、改明白。如果你在启动时遇到某个具体报错,翻翻第3章的排查记录,多半能对号入座。祝答辩顺利。

本文还有配套的精品资源,点击获取