
简介面向毕业设计场景的SpringBootVue超市管理系统源码包适合计算机专业学生、Java全栈初学者及需要完整项目案例的开发者用于完成商品库存、销售、会员、促销与供应链等模块的设计与实现也可作为前后端分离项目的参考模板。压缩包共2000个文件、约23.17MB包含61个Java后端类、43个Vue组件以及大量js、css、xml、json、sql和配置文件分别对应接口逻辑、页面样式、组件视图、依赖描述、数据库初始化与运行参数目录组织较完整。已有87人学习说明该案例具备一定参考价值读者可据此梳理SpringBoot自动配置、Vue组件化开发与前后端联调思路并借助SQL脚本和项目结构理解超市业务模块划分、员工权限管理、库存实时监控、销售报表统计及系统参数设置等功能的实现方式适合对照搭建、二次改造或撰写毕业设计文档。1. 需求与边界一套超市管理系统到底要接住哪些柜台动作带过几届毕业设计之后我发现最容易在答辩现场被问住的不是框架版本号而是“你这套系统到底替谁省了哪一步”。超市管理系统的麻烦之处就在这题目听着简单真正动手时需求会从“商品增删改查”膨胀成收银、库存、会员、供应商、报表五摊事每摊都能单独做一篇。所以这套基于 SpringBoot Vue 的超市管理系统一开始就得把边界划清楚——它服务的是一家中型社区超市三到五个收银台、两三千个 SKU、每天几百单流水核心目标只有一句让“卖出去多少”和“库里还剩多少”这两件事始终对得上。它适合拿来做毕业设计或课程设计的完整交付也适合想练一遍前后端分离全流程的人照着改。下面按架构、库表、后端、前端、避坑、验收的顺序把这份方案展开讲。2. 总体架构与技术选型前后端分离的四层结构与六大模块职责架构这一章最容易写成名词解释我一般会直接跳过“什么是 MVC”从“一个请求进来之后经过哪些文件”讲起。这套系统走标准前后端分离浏览器加载 Vue 打包出的静态资源页面里的请求打到/api/**SpringBoot 侧用 JWT 校验身份后进入 ControllerController 调 ServiceService 调 MapperMapper 落到 MySQL。中间没有任何模板渲染也没有 Session——这是整套方案后面所有设计的前提包括权限、跨域、登录状态续期。2.1 分层结构Controller / Service / Mapper / Entity 各管什么Java 侧严格四层职责不能串。Controller 只做参数接收、校验和统一响应包装Service 承载业务规则和事务边界Mapper 只写 SQL不写 if-elseEntity 与 DTO、VO 分离实体类不直接返回给前端。RestController RequestMapping(/api/product) public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService productService; } // 分页查询size 做上限保护防止前端传 size99999 把连接池打满 GetMapping(/page) public ResultPageResultProductVO page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { size Math.min(size, 100); return Result.ok(productService.pageQuery(page, size, keyword, categoryId)); } // 新增商品Valid 触发 DTO 上的校验注解失败由全局异常处理器统一兜住 PostMapping public ResultLong save(RequestBody Valid ProductDTO dto) { return Result.ok(productService.saveProduct(dto)); } }Result是统一响应包装固定 code / message / data 三个字段前端 axios 拦截器只认这三个键。ProductVO和ProductDTO分开的意义在于字段可见性商品表里有成本价收银员角色不该看到VO 里就没有这个字段而不是靠前端隐藏。参数上keyword同时匹配商品名和条码categoryId用来配合左侧分类树联动size的 100 上限是硬编码在 Controller 里的属于那种“加上去只要一行、不加以后一定出事”的保护。提示不要在 Controller 里写业务判断哪怕只有一行“库存是否为 0 就返回错误”。这一行以后会长成二十行而且会出现在三个不同的 Controller 里。2.2 技术选型每一层为什么是它选型不追求新追求“毕设周期内能写完、答辩时讲得清、出问题查得到”。这套组合的实际考虑如下层次选型选它的实际理由后端框架SpringBoot内嵌容器一条命令起服务事务与拦截器生态完整持久层MyBatis-Plus单表 CRUD 自动生成复杂报表仍可手写 SQL不用和 ORM 的隐式查询较劲数据库MySQL行锁和事务语义明确库存扣减要靠它保证原子性前端框架Vue3 Vite组合式 API 写收银台这种高频交互页面更顺Vite 冷启动快UI 组件Element Plus表格、表单、弹窗、分页开箱可用省掉大量样式工作量状态管理Pinia存登录用户、购物车、字典缓存比全局变量可控鉴权JWT前后端分离下无状态服务端不用维护会话真正需要解释的是 MyBatis-Plus 这一项。很多同学一上来选 JPA单表查询很爽但报表模块要写“按日聚合销售额、按品类算毛利、按供应商统计入库量”这类 SQL 用 JPA 要么写原生查询要么拼 Criteria反而更绕。MyBatis-Plus 的定位是简单表用BaseMapper省事复杂统计直接写在 XML 里两套并存不打架。2.3 模块划分六大业务模块与单向依赖模块按业务动作切不按技术层切。基础数据模块管商品、分类、供应商库存模块管入库、出库、盘点、库存流水收银模块管购物车、结算、挂单、退货会员模块管等级、积分、储值报表模块管日销、品类、毛利系统模块管用户、角色、菜单、操作日志。依赖方向必须单向收银依赖库存和商品报表只读依赖收银和库存系统模块被所有人依赖但不能反向依赖任何业务模块。这条规则听起来抽象落到代码上就是——ReportService里可以注入OrderMapper但OrderService里绝不能注入ReportService。一旦出现反向依赖后面加个“结算后刷新报表缓存”就会变成循环调用。2.4 前端工程结构目录组织与路由分层前端目录按职责分不按页面堆src/ api/ 按后端模块拆分的请求函数一个模块一个文件 router/ 静态路由登录、404 动态路由按角色注入 store/ Piniauser登录态、cart购物车、dict字典缓存 layout/ 侧边栏、顶栏、面包屑、标签页 views/ 页面组件 utils/ request.jsaxios 封装、auth.jstoken 读写路由这一层有两个设计点值得说清楚静态路由只放登录页和错误页业务路由全部由后端返回的菜单树动态注入每条路由的meta.roles只用于前端菜单过滤不作为安全边界。// src/router/index.js export const constantRoutes [ { path: /login, component: () import(/views/login/index.vue) }, { path: /, component: Layout, children: [ // 收银台管理员和收银员都能进退货按钮在组件内部再按角色判断 { path: cashier, name: Cashier, component: () import(/views/cashier/index.vue), meta: { title: 收银台, roles: [ADMIN, CASHIER] } } ] } ]meta.roles的作用是“看不见”后端PreAuthorize的作用才是“拦得住”。这两件事必须分开做只做前者就是权限越界后面第 5 章会专门讲这个坑。3. 数据库设计商品、库存、订单、会员、供应商、用户六条主线的表结构与字段取舍库表设计是这套系统里最不该省事的部分。毕设常见做法是“一个商品表塞所有字段”写到收银模块就会发现库存扣不动、订单金额对不上。这里的划分原则是凡是会随时间变化、需要留痕的数据一律单独建表。3.1 商品与分类条码唯一成本价单独收口product表存商品主体category表用parent_id做自关联形成分类树一般两层够用大类 小类。商品表的关键字段取舍字段类型说明idbigint主键barcodevarchar(32)条码唯一索引收银台扫码就靠它namevarchar(128)商品名普通索引支持模糊查category_idbigint分类外键purchase_pricedecimal(10,2)进价仅管理员可见sale_pricedecimal(10,2)售价unitvarchar(16)单位瓶、袋、斤statustinyint1 上架 0 下架deletedtinyint逻辑删除标记金额一律用decimal不用double。这不是讲究是必修课——double累加几百条明细之后会出现 0.01 的偏差对账时找不到原因很容易变成说不清的问题。3.2 库存表为什么不在商品表加一个 stock 字段很多人的第一版就是product.stock写到“入库、出库、盘点、退货”四类动作时就崩了库存变了但不知道是谁、什么时候、因为哪一单变的。正确做法是两张表——stock存当前余量商品 ID 仓库 ID 唯一stock_log存每一次变动流水变动类型、变动数量、变动前后值、关联业务单号。-- 库存流水表所有库存变动必须落一条退货、盘点、报损都不例外 CREATE TABLE stock_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT 1入库 2销售出库 3退货入库 4盘点调整 5报损, change_qty INT NOT NULL COMMENT 正数增加负数减少, before_qty INT NOT NULL, after_qty INT NOT NULL, biz_no VARCHAR(32) DEFAULT NULL COMMENT 关联单号销售出库时存订单号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;before_qty和after_qty这两个字段看起来冗余但它们是排查问题时唯一能提供依据的数据。库存对不上时沿着流水一条条看哪一步错的立刻清楚只有change_qty的话只能从最新值往前倒推出了错就是难查。3.3 订单与明细金额快照字段一个都不能省order存订单头订单号、会员 ID、收银员 ID、应收、实收、优惠、支付方式、状态order_item存明细商品 ID、商品名、单价、数量、小计。这里的关键设计是快照明细表里的product_name和unit_price是下单那一刻复制过来的不是关联查询出来的。原因很直接——商品明天可能改名下周可能调价但三个月前的订单必须还能还原成当时的样子。同理订单头里的total_amount、discount_amount、pay_amount都是计算后落库的不在查询时现算否则一旦促销规则改了历史订单金额会跟着变。3.4 会员、供应商、用户三张表不要合并新手容易把“会员”和“系统用户”合成一张表理由是都有手机号、都要登录。这两类对象的生命周期完全不同会员是消费主体有积分、等级、储值余额可能一辈子不登录系统用户是操作主体有角色、菜单权限、登录日志。合并之后收银员 A 同时是会员 B 这种情况就没法表达。供应商表同理它是采购侧的主数据和商品是多对多一个供应商供多种商品中间用supplier_product关联表记录供货价和起订量。三张表分开字段各管各的后面加“供应商结算”功能时不用改表结构。4. 后端核心实现商品维护、库存扣减、收银结算与报表统计4.1 商品维护接口分页、条件查询与逻辑删除商品列表页是使用频率最高的管理页要同时支持关键字模糊查、分类筛选、状态筛选、分页排序。这一块用 MyBatis-Plus 的LambdaQueryWrapper拼条件即可重点是逻辑删除的配置——TableLogic注解加上之后deleteById自动变成UPDATE ... SET deleted 1所有查询自动带deleted 0。Service public class ProductServiceImpl extends ServiceImplProductMapper, Product implements ProductService { Override public PageResultProductVO pageQuery(Integer page, Integer size, String keyword, Long categoryId) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // keyword 为空时不拼条件避免 like %% 全表扫描 wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .or(StringUtils.hasText(keyword)) .like(StringUtils.hasText(keyword), Product::getBarcode, keyword); wrapper.eq(categoryId ! null, Product::getCategoryId, categoryId) .eq(Product::getStatus, 1) .orderByDesc(Product::getCreateTime); PageProduct result this.page(new Page(page, size), wrapper); return PageResult.of(result, ProductVO::from); } }这里有个细节like(condition, column, value)的第一个参数是布尔条件为 false 时整段不拼进 SQL。这是 MyBatis-Plus 里最实用也最容易被忽略的写法用好了能省掉一大堆 if 判断。分类筛选为空时不拼category_id条件是因为“全部商品”页传的就是 null。4.2 库存扣减一条带条件的 UPDATE 解决超卖这是整套系统里唯一必须认真对待并发的地方。收银台结账扣库存正确写法是把判断塞进 SQL 的 WHERE 里靠数据库的行锁保证原子性!-- ProductMapper.xml -- update iddeductStock UPDATE product SET stock stock - #{qty}, update_time NOW() WHERE id #{productId} AND stock #{qty} AND deleted 0 /updateService public class StockService { private final ProductMapper productMapper; private final StockLogMapper stockLogMapper; /** * 扣减库存返回 false 表示库存不足 * 流水写入和扣减必须在同一事务否则出错时查不到账 */ Transactional(rollbackFor Exception.class) public boolean deduct(Long productId, Integer qty, String bizNo) { int rows productMapper.deductStock(productId, qty); if (rows 0) { return false; } stockLogMapper.insert(StockLog.out(productId, qty, bizNo)); return true; } }WHERE stock qty是整个方案里最值钱的一行。如果写成“先select查库存、判断够不够、再update扣减”两个收银台同时结账时两边都查到库存 1都判断够都执行扣减最后库存变成 -1。这种问题本地单线程测试永远复现不了上线第一天就出事属实是血泪经验。Transactional上的rollbackFor Exception.class也不能省。Spring 默认只对RuntimeException回滚如果业务里抛了受检异常比如某些 IO 相关异常事务不会回滚库存扣了订单没生成账就对不上了。4.3 收银结算一个事务写五张表的顺序一次结算涉及写订单头、写订单明细、扣库存、写库存流水、更新会员积分。这五步必须在一个事务里而且顺序有讲究——先做最容易失败的失败早回滚锁持有时间最短。Service public class OrderService { Transactional(rollbackFor Exception.class) public String checkout(CheckoutDTO dto, Long cashierId) { // 1. 先扣库存库存不足直接抛异常后面的表一张都不用写 for (CheckoutItem item : dto.getItems()) { boolean ok stockService.deduct(item.getProductId(), item.getQty(), dto.getOrderNo()); if (!ok) { throw new BizException(商品【 item.getProductName() 】库存不足); } } // 2. 写订单头金额由后端重算不信任前端传来的合计 Order order buildOrder(dto, cashierId); orderMapper.insert(order); // 3. 写明细价格与名称取当前商品快照 for (CheckoutItem item : dto.getItems()) { orderItemMapper.insert(buildItem(order.getId(), item)); } // 4. 会员积分只有绑定了会员的订单才更新 if (dto.getMemberId() ! null) { memberService.addPoints(dto.getMemberId(), order.getPayAmount().intValue()); } return order.getOrderNo(); } }要点有三个。第一金额由后端重算前端传什么价格都不认否则改一下浏览器内存就能一块钱买走一车货。第二扣库存放在最前面因为它是唯一会“预期失败”的步骤早失败早回滚不会白写三张表。第三订单号用“日期 收银台号 序列”生成不要用自增主键当订单号——自增主键能反推出一天的订单量属于不该暴露的信息。4.4 报表统计按日聚合的 SQL 与索引配合报表模块是整个系统里唯一需要手写复杂 SQL 的地方。日销报表按天聚合订单数、销售额、客单价-- 注意 order 是 MySQL 保留字必须加反引号 SELECT DATE(create_time) AS day, COUNT(*) AS order_cnt, SUM(pay_amount) AS amount, ROUND(SUM(pay_amount) / COUNT(*), 2) AS avg_amount FROM order WHERE status 1 AND create_time #{startTime} AND create_time #{endTime} GROUP BY DATE(create_time) ORDER BY day;这条 SQL 能不能跑得快全看order表上有没有(status, create_time)联合索引。有索引走范围扫描几千条数据毫秒级返回没索引就是全表扫描加临时表分组数据攒到几万条之后打开报表要等十几秒演示时非常尴尬。品类销售占比是另一条思路从明细表出发关联商品表SELECT c.name AS category_name, SUM(i.qty) AS total_qty, SUM(i.subtotal) AS total_amount FROM order_item i JOIN order o ON o.id i.order_id AND o.status 1 JOIN product p ON p.id i.product_id JOIN category c ON c.id p.category_id WHERE o.create_time #{startTime} AND o.create_time #{endTime} GROUP BY c.id, c.name ORDER BY total_amount DESC;这里用的是明细表里存的product_id反查分类而不是明细表直接存分类 ID。原因是分类可能调整商品从“饮料”挪到“酒水”用当前分类归属来统计更符合管理者的理解如果想要“下单当时”的分类就得在明细表加category_id快照字段两者都合理取决于业务怎么定义。5. 踩坑与排查并发超卖、跨域联调、权限越界、报表卡顿这一章是我认为比前面四章都更值得逐条看的部分。下面四个问题几乎每一届做这类系统的同学都会撞上至少两个。5.1 现象两个人同时结账库存变成 -1现象本地测试一切正常找同学帮忙模拟两个收银台同时点结算最后一件商品卖出了两次库存显示 -1。刷新页面数据还是错的数据库里确实存了 -1。原因典型的“查询—判断—更新”三步走写法两步之间存在时间窗。两个请求都读到stock 1都判断1 1成立都执行stock 1 - 1最后写回 0不对如果两个请求都写 0库存不会变负。真正变成 -1 的是这种写法stock stock - qty但判断在 Java 层做——第一个请求扣成 0第二个请求的 Java 判断用的是它自己之前读到的旧值 1照样执行减法于是变成 -1。解决把判断搬进 SQL用UPDATE ... SET stock stock - #{qty} WHERE id ? AND stock #{qty}靠 InnoDB 行锁串行化两个请求第二个请求的rows返回 0业务层抛“库存不足”。同时给stock_log加一条记录方便事后核对。如果业务需要处理同一商品的批量扣减还要在 Service 层对商品 ID 排序后再依次扣避免两个事务交叉持有行锁造成死锁。5.2 现象后端接口 Postman 能通前端一调就 401 或跨域报错现象登录接口在 Postman 里返回 200 和 token前端页面点登录却报Access-Control-Allow-Origin好不容易跨域通了刷新页面又跳回登录页陷入循环。原因跨域和 401 是两个独立问题经常被混在一起。跨域是因为开发阶段前端跑在 5173 端口、后端跑在 8080 端口浏览器同源策略拦截401 循环是因为 axios 响应拦截器里对“token 失效”和“登录接口本身返回 401”没做区分登录失败也触发跳转跳转又发请求又失败。解决跨域优先用 Vite 的server.proxy转发而不是在后端堆CrossOrigin——代理方案下浏览器看到的始终是同源生产环境换成 Nginx 反代也是同一套逻辑不会出现“开发能跑、部署报错”。// vite.config.js export default defineConfig({ server: { proxy: { // 所有 /api 开头的请求转发到后端浏览器视角下始终同源 /api: { target: http://localhost:8080, changeOrigin: true } } } })401 循环的修法是在响应拦截器里排除登录接口// src/utils/request.js service.interceptors.response.use( res res.data, error { const url error.config?.url || // 登录接口返回 401 是账号密码错不是登录过期不能跳转 if (error.response?.status 401 !url.includes(/auth/login)) { userStore.clearToken() router.replace(/login) } return Promise.reject(error) } )5.3 现象普通收银员能看到并点到“删除商品”按钮接口还真删了现象前端菜单按角色过滤了收银员登录看不到“商品管理”菜单但用浏览器直接输入/product路径能进页面更严重的是绕过页面直接调删除接口后端也返回成功。原因只做了前端菜单过滤没做路由守卫更没做后端接口鉴权。前端的meta.roles只是渲染依据任何人打开控制台都能改后端接口如果只校验“是否登录”而不校验“是否有权限”等于没设防。解决三层一起做。路由守卫在beforeEach里比对目标路由的meta.roles与当前用户角色不匹配跳 403后端在方法上加PreAuthorize(hasRole(ADMIN))并在配置类开启EnableGlobalMethodSecurity同时把菜单也改成后端下发前端只负责渲染这样增删角色权限不用改前端代码。另外逻辑删除的数据要检查TableLogic是否真的生效——见过不止一次因为字段名写错删除标记没起作用被删的商品又出现在列表里。5.4 现象销售报表打开要等十几秒偶尔还超时现象演示前一天导入了半年的模拟数据订单表三万多条点开“销售统计”页面转圈十几秒才出来切个日期范围又要重新等。原因三个问题叠加。一是order表的create_time没有索引日期范围查询全表扫二是报表 Service 里用循环查每个订单的明细典型 N1三是每次请求都实时聚合没做任何缓存。解决先加联合索引(status, create_time)单这一条就能把查询从十几秒降到几十毫秒再把循环查询改成一条JOIN聚合 SQL最后对“近 7 天”“本月”这类固定区间加一层本地缓存用Cacheable或简单的ConcurrentHashMap加过期时间都行报表数据延迟一分钟完全可以接受。排查这类问题的通用手法是打开 MySQL 慢查询日志把long_query_time调成 1 秒跑一遍报表慢在哪条 SQL 上一目了然比猜快得多。6. 验收与答辩一份能照着演示的功能清单和演示脚本到这一步代码写完了最怕的是答辩现场点哪个按钮哪个报错。我一般会提前做一张验收清单逐项手动走一遍走不通的当场记下来不留到现场。模块验收动作通过标准登录鉴权管理员、收银员分别登录菜单不同直接输 URL 访问越权页被拦到 403商品管理新增、改价、下架、删除列表刷新正确被删商品不再出现订单里历史商品名不变库存入库 100 件收银卖出 3 件库存显示 97库存流水有两条记录前后值连贯收银结算购物车加 3 件商品绑定会员结账订单生成库存同步扣减会员积分增加金额与手工计算一致并发两个浏览器同时结账最后 1 件商品一个成功一个提示库存不足库存为 0 不为负退货对已支付订单退货库存回滚流水新增退货记录订单状态变更报表查近 7 天销售统计3 秒内返回数字与订单列表手工汇总一致演示脚本我建议按“一条业务主线”走而不是按模块走登录收银员 → 扫码加购三件商品 → 绑定会员 → 结账 → 切管理员登录 → 看库存减少 → 看订单列表 → 看报表数字变化 → 对订单退货 → 再看库存回补。这条线走完等于把商品、库存、收银、会员、报表、权限六个模块全串了一遍评委看到的是一个闭环系统而不是六个互不相干的增删改查页面。答辩提问里高频的三个问题提前准备好答案一是“并发怎么处理的”答带条件的 UPDATE 加行锁能说出为什么不能先查后改二是“金额为什么由后端算”答前端数据不可信三是“权限怎么做的”答前端控制可见性、后端控制可执行性两层都要有。这三个问题答得利落基本就稳了。从那以后我每次带这类系统都强制自己在上交前一天按验收清单从头点一遍哪怕是看着最不可能出错的“修改商品价格”——真有一次就是改价接口少传了一个字段列表页价格显示成 null现场演示直接卡住。提前十分钟走一遍比事后解释半小时有用得多。希望帮到你。本文还有配套的精品资源点击获取