简介:基于Java+Spring Boot+Vue+MySQL的超市进销存系统是一套完整的高分毕业设计项目,面向计算机相关专业学生,可用于毕业设计、课程设计或期末大作业。系统围绕超市日常运营,覆盖商品管理、库存监控、销售管理、采购管理和报表统计等核心功能,界面简洁、操作直观,并充分考虑系统的稳定性与可扩展性。资源包共480个文件,压缩后约17.91MB,其中147个Java文件承担后端业务逻辑,53个Vue文件组成前端界面,161个SVG与多张PNG/JPG用于界面素材,SQL脚本完成数据库初始化,另有XML/Properties配置、JS逻辑、CSS样式、Maven脚本及文档资料,整体结构清晰。项目采用前后端分离开发,通过AJAX交互,下载后使用IDEA、Navicat等工具即可运行;源码、数据库脚本、软件工具与论文相关文档一并包含,已经严格调试,无需修改即可部署。目前已有55人学习使用,适合需要快速搭建可运行项目或参考高分毕设做法的学习者。
1. 超市进销存系统:一个能直接跑起来的Spring Boot+Vue毕业设计
如果你正在找一份“Java + Spring Boot + Vue + MySQL”的完整项目作为毕业设计或课程设计,最怕的就是下载下来缺文件、跑不起来、代码结构混乱。这份超市进销存系统源码包恰好避开了这些坑:前后端代码齐全,附带MySQL数据库脚本和开发工具,解压后按照脚本顺序执行就能启动。它不是那种只有增删改查的玩具项目,而是覆盖了商品管理、库存监控、销售流、采购订单和报表统计五个核心模块,并且采用了前后端分离模式,后端用Spring Boot提供RESTful接口,前端用Vue + Axios做数据交互。对于想学习企业级开发流程的人,或者需要快速交付一个可演示系统的学生,这份源码的参考价值都很直接。
2. 数据库建模与进销存核心表结构设计
2.1 业务模块如何映射到数据库表
超市进销存系统的业务闭环是“进货—入库—销售—出库—盘点—统计”。在设计表结构时,我建议把“商品信息”作为主数据,围绕它展开库存流水、销售订单和采购订单。这套源码里的表结构大致分为五类:
| 模块 | 核心表 | 关键字段 | 关联关系 |
|---|---|---|---|
| 商品管理 | product | id,name,category_id,price,cost | category_id关联分类表 |
| 库存管理 | stock | id,product_id,quantity,warning_value | 与product一对一 |
| 采购管理 | purchase_order | id,supplier_id,order_no,status | 明细表存商品 |
| 销售管理 | sale_order | id,customer_name,total_amount,create_time | 明细表存商品 |
| 报表统计 | 聚合查询 | 按日/月汇总销售额、毛利 | 依赖销售明细表 |
实际建表时,采购和销售需要拆分成主表和明细表,否则商品数量无法关联。比如销售单sale_order只需要记录订单总额和客户信息,而sale_order_item记录每个商品的单价、数量和小计。这样设计的好处是统计“某段时间内哪个商品卖得好”时,直接对明细表做GROUP BY就很高效。
2.2 建表脚本中的关键 SQL 片段
下载包里附带db_supermarket.sql,在 MySQL 5.7 以上版本执行即可。核心的商品表和库存流水表脚本如下:
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `category_id` int(11) DEFAULT NULL COMMENT '商品分类ID', `price` decimal(10,2) NOT NULL COMMENT '销售价', `cost` decimal(10,2) NOT NULL COMMENT '成本价', `status` tinyint(1) DEFAULT '1' COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `stock_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL, `change_type` tinyint(1) NOT NULL COMMENT '1入库 2出库 3盘点调整', `quantity` int(11) NOT NULL COMMENT '变动数量(正数增加,负数减少)', `before_stock` int(11) NOT NULL, `after_stock` int(11) NOT NULL, `remark` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stock_record是库存管理的核心。每次入库或销售出库时,先更新商品对应的库存数量,再插入一条流水记录。通过before_stock和after_stock可以回溯任意时刻的库存变化,这也为后续做报表统计提供了原始数据。注意change_type用tinyint而不是字符串,是为了节省空间,查询时再通过 Java 枚举翻译成中文。
2.3 权限相关的用户表与角色表
系统集成登录认证时,用户表不能只存一个密码字段。源码里使用sys_user、sys_role、sys_user_role三表关联,支持管理员和收银员两种角色。密码存储用的是BCryptPasswordEncoder加密后的哈希值,而不是明文。如果你要在面试中讲解这个项目,可以强调这一点:登录时校验password字段,用 Spring Security 的BCryptPasswordEncoder.matches()方法比对,而不是简单地equals()。
3. Spring Boot 后端接口开发与事务控制
3.1 分层结构与核心依赖
后端采用标准的 Controller-Service-Mapper 三层架构,包结构如下:
com.supermarket ├── controller # REST API 入口 ├── service # 业务逻辑,事务边界 ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库实体 ├── dto # 前端接收参数对象 ├── config # 安全配置、跨域配置 └── common # 统一返回结果、异常处理pom.xml里引入的关键依赖有spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java和spring-boot-starter-security。其中 MyBatis-Plus 让我省掉了大量 XML 写 SQL 的时间,分页查询直接调Page对象即可。对于这个体量的系统,用 MyBatis-Plus 比 JPA 更直观,因为生成的 SQL 可控,且学生更容易向导师解释。
3.2 登录认证与 JWT 令牌实现
登录接口采用 JWT(JSON Web Token)无状态认证。用户成功登录后,后端签发一个 Token,前端在请求头里带上Authorization: Bearer <token>,后端通过拦截器解析用户身份。核心代码如下:
@Service public class UserService { // 注入密码加密器 private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); public String login(String username, String password) { SysUser user = userMapper.findByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 生成 JWT,有效期 24 小时 String token = Jwts.builder() .setSubject(user.getUsername()) .claim("role", user.getRoleId()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); return token; } }上述代码里有两个关键点。第一,passwordEncoder.matches()是比对明文密码和哈希密码的唯一正确方式,千万不能重新加密明文再比对,因为 BCrypt 每次生成的盐不同。第二,JWT 的secretKey需要放在配置文件里,不要硬编码在类中。实际生产环境还会用jjwt或auth0库,但原理一致。
3.3 进货入库与销售出库的事务边界
库存调整必须保证操作前后的一致性。比如销售出库时,需要同时完成“插入销售订单”“插入销售明细”“扣减库存”“插入库存流水”四个操作,任何一个失败都应该回滚。在 Spring Boot 中直接使用@Transactional注解:
@Transactional(rollbackFor = Exception.class) public void createSaleOrder(SaleOrderDTO dto) { // 1. 生成销售单,计算总金额 SaleOrder saleOrder = new SaleOrder(); saleOrder.setCustomerName(dto.getCustomerName()); saleOrder.setCreateTime(new Date()); saleOrderMapper.insert(saleOrder); // 2. 记录销售明细并扣减库存 for (SaleItemDTO item : dto.getItems()) { Product product = productMapper.selectById(item.getProductId()); if (product == null) { throw new BusinessException("商品不存在"); } // 明细记录 SaleOrderItem saleItem = new SaleOrderItem(); saleItem.setOrderId(saleOrder.getId()); saleItem.setProductId(product.getId()); saleItem.setPrice(product.getPrice()); saleItem.setQuantity(item.getQuantity()); saleItemMapper.insert(saleItem); // 3. 扣减库存 Stock stock = stockMapper.findByProductId(product.getId()); int afterStock = stock.getQuantity() - item.getQuantity(); if (afterStock < 0) { throw new BusinessException("库存不足:" + product.getName()); } // 更新库存 stock.setQuantity(afterStock); stockMapper.updateById(stock); // 4. 写入库存流水 stockRecordMapper.insert(buildStockRecord(product.getId(), item.getQuantity(), 2, stock.getQuantity(), "销售出库")); } }这段代码的rollbackFor = Exception.class很关键,因为 Spring 默认只在 RuntimeException 时回滚,而这里自定义的BusinessException通常继承自 RuntimeException,所以能触发回滚。如果你在项目里看到Exception没有指定rollbackFor,记得追问一句,这是很多面试官喜欢挖的地方。
3.4 报表统计的 SQL 实现
报表模块不需要额外建表,可以用聚合查询完成每日销售额和毛利:
SELECT DATE(create_time) AS report_date, COUNT(*) AS order_count, SUM(total_amount) AS sales_amount, SUM(total_amount - cost_amount) AS gross_profit FROM sale_order WHERE create_time >= #{startDate} AND create_time < #{endDate} GROUP BY DATE(create_time) ORDER BY report_date DESC;这里cost_amount可以在生成销售单时通过商品成本价乘以数量计算好,冗余存储在销售主表里。虽然违反三范式,但在业务系统里这种冗余能大幅提升查询速度,因为报表查询不需要再关联明细表去实时计算成本。
4. Vue 前端页面实现与路由传参细节
4.1 前端项目结构与运行脚本
前端代码在frontend目录下,使用 Vue CLI 生成,组件结构如下:
src ├── api # 按模块封装的请求接口 ├── assets ├── components # 公共组件(侧边栏、头部、面包屑) ├── router # 路由配置 ├── store # Vuex 状态管理 ├── views # 页面 │ ├── product # 商品管理 │ ├── stock # 库存管理 │ ├── sale # 销售管理 │ └── dashboard # 报表中心 ├── App.vue └── main.js下载包里的IndexMain.vue.bak、IndexHeader.vue.bak这些.bak文件,是开发者调试时备份的旧版本。它们不影响运行,但建议清理掉,避免被后端代码扫描工具误判。真正的前端入口在main.js,路由配置在router/index.js。
4.2 路由配置中的动态传参
商品管理和库存管理页面之间经常需要传递商品ID。Vue 路由传参有两种方式,这里使用query方式,因为页面刷新后参数不会丢失:
// 在商品列表页跳转到库存详情 this.$router.push({ path: '/stock/detail', query: { productId: row.id, productName: row.name } }); // 库存详情页接收参数 this.$route.query.productIdquery传参适合不敏感且需要可收藏的URL。而如果参数是订单号、用户名这类不适合暴露在地址栏的信息,建议用params配合name路由跳转。很多 Vue 面试题会问这两种方式的区别,掌握了这个场景就很好答。
4.3 Axios 请求封装与后端联调
前后端分离开发时,跨域问题是第一个坑。后端已经在config里配置了 CORS 策略,允许所有来源。前端的api/request.js简单封装如下:
import axios from 'axios'; const request = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 10000, withCredentials: true }); // 请求拦截器:自动在头信息附加 token request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:统一处理 code 和 HTTP 错误 request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { alert(res.msg || '请求失败'); return Promise.reject(new Error(res.msg)); } return res.data; }, error => { if (error.response.status === 401) { this.$router.push('/login'); } return Promise.reject(error); } );注意baseURL要与后端配置文件里的server.servlet.context-path保持一致。后端接口前缀是/api,所以前端请求写成/product/list,拼接后就是http://localhost:8080/api/product/list,这样不会出现少一层路径的问题。
4.4 商品管理页面的表格与表单联调
商品管理视图是典型的“搜索表格 + 弹窗表单”结构。表格使用el-table,表单使用el-dialog嵌套el-form。添加商品时,表单数据通过POST请求发送到后端;修改时通过PUT请求。关键代码如下:
<el-table :data="tableData" v-loading="loading"> <el-table-column prop="name" label="商品名称" width="180"></el-table-column> <el-table-column prop="price" label="销售价"></el-table-column> <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table>async handleEdit(row) { // 打开弹窗之前先回填数据 this.form = { ...row }; this.dialogVisible = true; }删除操作要提示确认框,避免误删。一个实用的做法是删除前先检查该商品是否存在于未完成的采购单或销售单,后端对应接口会拦截并返回提示。前端只需要把message展示出来即可。
5. 本地部署与运行:安装顺序、报错排查和参数调整
5.1 三批启动脚本的执行逻辑
下载解压后,根目录下有1-install.bat、2-run.bat、3-build.bat。它们的执行顺序不能乱:
# 1-install.bat:安装后端依赖并初始化数据库 mvn clean install -DskipTests mysql -uroot -p123456 < db_supermarket.sql # 2-run.bat:启动后端服务 mvn spring-boot:run # 3-build.bat:构建前端并启动开发服务器 cd frontend npm install npm run serve1-install.bat里的mysql命令依赖系统环境变量。如果你没有配置 MySQL 的bin目录到 PATH,脚本会报“mysql 不是内部或外部命令”。解决办法是显式指定 MySQL 安装路径,例如:
"C:\Program Files\MySQL\MySQL Server 5.7\bin\mysql.exe" -uroot -p123456 < db_supermarket.sql5.2 最常见的三个启动失败原因
第一,端口被占用。后端默认使用8080端口,前端使用8081。如果启动时提示Web server failed to start. Port 8080 was already in use,在application.yml里换一个端口:
server: port: 9090注意前端request.js里的baseURL也要同步改成http://localhost:9090/api。
第二,MySQL 连接失败。检查application.yml中的数据库密码是否为123456,如果不是,统一修改。Spring Boot 2.4 之后,spring.datasource.url需要加上serverTimezone=Asia/Shanghai和useSSL=false,否则会报时区错误:
spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false第三,前端 npm 安装依赖卡死。如果npm install速度慢或报错,切换到淘宝镜像:
npm config set registry https://registry.npmmirror.com5.3 让项目在演示时更出彩的小改动
这个系统默认的管理员账号是admin,密码123456。演示前,可以先把部分商品分类的icon字段替换成 emoji 或图片链接,前端侧边栏会显示对应图标,视觉上比纯文字更专业。再一个技巧是调整库存预警阈值:在stock表里把warning_value设为 10,然后打开首页的库存预警统计,当某个商品库存低于这个值时,仪表盘会出现醒目的红色标记,这样能直观展示业务流。
如果愿意深入一点,可以在报表模块加一个导出 Excel 功能。后端用EasyExcel写一个GET /report/export接口,前端通过window.open直接下载。这个功能在毕设答辩时属于加分项,而且实现成本很低。最后强调一个容易被忽略的点:.bak文件是备份,不要把它们提交到 Git,最好在.gitignore里加上*.bak,避免仓库混乱。
本文还有配套的精品资源,点击获取