ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Vue和Spring Boot的超市进销存系统实战:库存设计、单据闭环与部署要点

基于Vue和Spring Boot的超市进销存系统实战:库存设计、单据闭环与部署要点 做个基于Vue和Spring Boot的超市进销存系统其实最难的不是CRUD而是把“库存”这件事想清楚。我见过太多人一上来就写增删改查结果做到库存盘点、报表统计时发现数据对不上返工到崩溃。这篇博文从选型逻辑、表结构设计、后端业务闭环、前端模块划分到联调实战和答辩要点把整个项目拆开揉碎讲清楚适合正在做毕设或想完整走一遍进销存系统的同学参考。1. 为什么是Vue Spring Boot选型逻辑与系统边界1.1 这套技术栈解决了什么问题“vue基于springboot的企业超市产品仓库进销存管理系统”这个项目标题基本已经锁死了技术栈前端用Vue后端用Spring Boot做的是一个面向超市场景的仓库进销存管理系统。先说说为什么毕设场景普遍选这套组合。Spring Boot的价值在于它把后端开发的门槛压得很低。你不用像以前用SSH那样配一堆XML一个spring-boot-starter-web加进去写个RestController就能起接口。内置的Tomcat、默认的自动配置、application.yml里随手改端口这些特性对一个需要快速搭建后端的人来说非常友好。而Vue这边组件化开发和响应式数据绑定配合Element Plus这类现成的UI库做管理后台的效率比传统的JSP jQuery高出一大截。更重要的是前后端分离带来的开发便利。用Vue的npm run dev起前端开发服务器后端起Spring Boot的8080端口两边并行开发互不干扰。我见过很多同学把前后端代码混在一起改个样式都要重新编译整个后端工程那种体验真的很折磨。分离以后前端调接口、后端写接口出了Bug也容易定位。还有一点很多人忽略毕设答辩时老师特别看重“你是否理解为什么选这套技术”。你如果能说出“Vue负责视图层和交互、Spring Boot负责业务逻辑和接口暴露、MySQL负责持久化”这种分层认知比单纯说“我学了就拿来做”要加分得多。1.2 进销存系统的核心闭环进销存这个词听起来简单真正拆开就三条线进货采购入库、销货销售出库、存货库存查询与盘点。超市场景外加一个维度——商品是有保质期的部分商品还有批次管理需求。先看核心闭环。采购部门向供应商下采购单货到之后做入库操作库存增加。顾客在前台买走商品后台做销售出库库存减少。库管人员随时可以查某个商品当前还剩多少这就是实时库存。过一段时间还要盘点账实核对。这里最关键的认知是库存不是靠“改库存表”改出来的而是靠进出库单据“流水”推出来的。我给你打个比方进销存系统就像银行账户购物卡余额就是库存表充值记录、消费记录就是流水表。银行不会直接改余额这个字段——每次消费都是插入一条流水然后余额跟着变。进销存也一样每个入库单、出库单一旦审核就生成对应的库存流水库存表的数字是根据流水累计出来的结果。系统的边界也要提前划清楚。超市进销存一般做到商品管理、供应商/客户档案、采购入库、销售出库、库存查询、库存预警、盘点管理、基础报表这八个模块就够了。别贪多非要做多仓库调拨、委外加工、拆解组装这种生产型ERP的功能一是工作量膨胀二是你自己也很难讲清楚业务逻辑答辩容易翻车。2. 进销存系统的“数据地基”核心表结构设计2.1 从库存流水倒推表设计我给这个项目设计表结构的时候经验是倒着推先想清楚“最核心的查询是什么”再反推表怎么建。进销存系统里最高频的查询是什么——某个商品现在有多少库存。第二高频的查询是——某个商品这段时间入了多少、出了多少。所以表结构必须能同时回答这两个问题。核心表差不多有六张我给你列个表看职责表名核心字段作用productproduct_id, product_name, spec, unit, category商品档案超市里每种商品一行suppliersupplier_id, name, contact, phone供应商信息进货的来源customercustomer_id, name, phone客户信息销售的对象stockstock_id, product_id, quantity, warehouse_id当前实时库存多少在库stock_flowflow_id, product_id, change_type, change_qty, before_qty, after_qty, order_no, create_time库存流水每一笔出入库的痕迹orders含主表和明细表order_no, order_type, supplier_id/customer_id, total_amount, status采购入库单和销售出库单商品表不必说。供应商表、客户表是基础档案。重点讲stock_flow这张表。它的字段设计直接决定了系统的可追溯性change_type记录是“入库”还是“出库”change_qty记录数量变化最关键的是before_qty和after_qty说白了就是变更前的库存数和变更后的库存数。有了这两个字段你在任何时间点都能回答“这笔操作对公司库存的影响到底是什么”。举个例子商品A原来库存是10件一张销售单卖出去3件那么流水里记的就是before_qty10, change_qty-3, after_qty7。有了这种流水库存一旦对不上就能一条一条往回翻找出是哪张单据出了问题。orders表做主从结构是这一块的实操约定俗成。主表存单据的汇总信息单号、类型、往来单位、总金额、状态明细表存每一行商品商品、数量、单价、金额。这样设计的好处是单据以“头”为单位审核以“行”为单位操作库存而且报表统计的时候两种粒度都能支持。2.2 冗余字段与单据编号的取舍表设计里最容易让新手纠结的是要不要冗余字段。我直接说结论该冗余的必须冗余不该省的别省。什么叫该冗余的比如订单明细表里建议直接把product_name和spec存进去。因为商品档案是可以被修改的万一改了名历史订单里如果只存product_id查询时显示的名称就全变了。冗余一份快照字段虽然查出来可能和当前商品档案不一致但这才符合“单据是某时刻的客观事实”这个原则——你买的时候商品就叫“农夫山泉550ml”后来它改名了你的历史单据里它就该还是“农夫山泉550ml”。单价、金额也是同理。订单明细里必须存下单那一刻的成交价而不是现查现在的零售价。这是进销存系统里一个很隐蔽但至关重要的点。再讲单据编号。很多人直接用一个自增主键当单号这在Demo里能用但到了答辩或真实环境会被老师追问。主流做法是“前缀 日期 流水号”比如CG20250612001表示采购入库单、2025年6月12日、第一单。用Java生成也不难查当天最大单据号取后缀加一再拼上日期前缀。这里有个小坑如果用了雪花ID或UUID做主键别把它直接当单据号展示给用户一长串数字没有辨识度。正确姿势是“主键用自增或雪花ID单据号用业务规则单独生成”两者互不干扰。3. 后端Spring Boot核心库存变更的业务闭环3.1 入库单、出库单、库存表三者的关系很多人做进销存会直接把“增加库存”写成“给stock表update一下quantity”。这是最大的设计隐患。正确业务闭环应该是这样的单据驱动用户在前端填写采购入库单或者销售出库单此时单据状态是“待审核”或者说“未过账”。系统这个时候只保存单据本身绝不碰库存。等审核通过过账的那一刻才触发库存变更逻辑。审核操作的Service层逻辑核心就是三步校验单据状态防止重复审核。逐行处理单据明细每一行都去更新stock表。每更新一行插入一条stock_flow流水。我用一段伪代码把审核入库的逻辑写出来你感受一下Transactional public void auditOrder(OrderMain order) { if (order.getStatus() ! 0) { throw new BusinessException(单据已过账不能重复操作); } for (OrderItem item : order.getItems()) { Stock stock stockMapper.selectByProduct(item.getProductId()); int beforeQty stock.getQuantity(); int afterQty beforeQty item.getQuantity(); // 入库加 stock.setQuantity(afterQty); stockMapper.updateById(stock); StockFlow flow new StockFlow(); flow.setProductId(item.getProductId()); flow.setChangeType(order.getOrderType()); // 1入库 2出库 flow.setChangeQty(item.getQuantity()); flow.setBeforeQty(beforeQty); flow.setAfterQty(afterQty); flow.setOrderNo(order.getOrderNo()); stockFlowMapper.insert(flow); } order.setStatus(1); orderMapper.updateById(order); }注意那段Transactional。审核操作必须在事务里执行——要么全部成功要么全部回滚。试想一下一张采购单有三行明细第一行库存加成功了第二行插入流水时数据库报错如果不在一个事务里库存就被改了一半账就永远对不上了。这种问题在联调时极难排查等发现库存不对数据早就回不到审核前的状态了。出库单的逻辑完全对称只是afterQty beforeQty - item.getQuantity()。但出库多一个校验减完之后不能是负数。这个判断必须在事务内完成而且要考虑并发。3.2 库存扣减的并发问题并发问题是我强烈建议你在论文里写、在答辩时主动提的加分点。超市的收银台同时在打销售单两张单子同时扣同一个商品的库存如果不做控制最后库存数可能被覆盖错。假设库存是10件同时来了两个出库单各扣5件。如果不加锁它们都读到before10都算成after5然后都写回5件最后库存变成5而不是正确的0。这就是典型的丢失更新。解决方式有三个层次从基础到高级方案一数据库行锁。在查询库存时加上悲观锁SELECT * FROM stock WHERE product_id ? FOR UPDATE这个FOR UPDATE会把该商品的库存行锁住第二个事务必须等第一个提交后才能继续读。我局促地说这是在单机部署场景下最稳的解法代码改动也小。方案二乐观锁。给stock表加一个version字段更新时带上版本号UPDATE stock SET quantity ?, version version 1 WHERE product_id ? AND version ?如果更新影响行数为0说明版本对不上别人已经改了回去重试或者报错。这种方式性能好但需要引入重试机制实现稍复杂。方案三Redis分布式锁。如果以后系统要拆多实例部署数据库行锁就撑不住了得用分布式锁。毕设阶段没必要上这个但答辩时被问到“以后并发量大了怎么办”你能答出Redis分布式锁的思路就已经胜过了大部分同学。我的建议是毕设用方案一论文和答辩里把三种方案做个对比专门讲一版“为什么选数据库行锁它解决的是什么问题后续如果扩展可以怎么做”。这个深度会让导师觉得你真的在思考系统设计而不是在堆代码。3.3 库存预警的两种实现方式超市场景里“库存预警”几乎是必须做的模块而且它是你区别于“普通CRUD系统”的重要亮点。什么叫预警两个方向库存存量低于下限要提示补货高于上限要考虑促销。部分商品还有保质期预警。实现方式有两种各有应用场景。一种是查询时即时判断。每次查库存列表的时候SQL里直接带上条件判断当前库存是否小于安全库存阈值。这种方式实现简单数据也是实时的但每次都要计算查询负担略重。另一种是定时任务轮询。用Spring Boot自带的Scheduled注解每天跑一次扫描低于阈值的商品把预警记录写进一张预警表里前端登录后能看到。这种方式适合要推送通知的场景但时效性差一点。我倾向于两者结合列表页的库存状态用即时判断因为查询当场就能算出来后台首页的预警提醒比如“库存低于预警线的商品有18种”用定时任务预警表这样首页打开不卡数据也够用。定时任务代码也不长Component public class StockWarnTask { Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void checkStockWarn() { // 扫描stock表quantity safety_stock的商品批量写入stock_warn表 } }注意Scheduled默认是单线程串行的如果以后预警逻辑变复杂要记得配线程池。毕设阶段每天跑一次完全够用不会出问题。4. 前端Vue的模块划分页面、状态与组件4.1 页面路由与菜单设计Vue端我习惯先用路由把整体页面框架搭出来。超市进销存系统的页面结构按角色和业务划分基本这样/login登录页/dashboard首页看板库存总览、预警提示、近期出入库动态/product商品管理列表、新增、编辑支持按分类筛选/supplier供应商管理/customer客户管理/purchase采购入库单据录入、单据列表、审核操作/sale销售出库单据录入、单据列表、审核操作/stock库存查询实时库存列表、库存流水/report统计报表进销存趋势图、商品排行/user用户与角色管理路由配置用vue-router记得把菜单结构和路由表对应起来。Element Plus的侧边栏菜单组件一般用el-menu配el-sub-menu点击菜单router.push跳转。这里说一个实操细节操作类页面比如新增采购单我建议用弹窗或者独立页面承载不要和列表页混在一起。以采购入库为例合理的交互流程是库存管理页面点“入库”弹出一个单据编辑面板选择供应商、添加商品行、填数量单价提交后跳回列表页。面包屑和路由保持简单清晰答辩演示时操作路线也很顺畅。4.2 状态管理Vuex还是PiniaVue生态下管理登录用户信息、菜单权限这种全局状态老项目多用Vuex新项目推荐Pinia。如果你从零开始做这个毕设直接选Vue3 PiniaAPI更简洁TypeScript支持也更好。Pinia的store写法相当直白。我用它来存登录用户信息和权限标识在main.js初始化时从localStorage恢复登录态export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , username: , role: }), actions: { setLoginInfo(data) { this.token data.token this.username data.username this.role data.role localStorage.setItem(token, data.token) }, logout() { this.token this.username this.role localStorage.removeItem(token) } } })store里不要放重的东西。像商品列表、订单明细这种业务数据正常通过接口拉、存在组件的响应式变量里就够了全堆进store会让状态管理变成大泥潭反而不如不用。4.3 库存看板与报表的可视化看板和报表是前端部分的加分大头。建议用ECharts它和Vue配合很顺。常见的图就是这三种折线图近30天入库/出库数量趋势柱状图各分类商品的库存分布饼图库存金额按供应商占比先看折线图。我从后端拿数据时接口可以直接返回两个数组dateList近30天日期和inboundList/outboundList每日进出数量。前端渲染就很简单const chart echarts.init(document.getElementById(stockChart)) chart.setOption({ xAxis: { type: category, data: dateList }, yAxis: { type: value }, series: [ { name: 入库量, type: line, data: inboundList }, { name: 出库量, type: line, data: outboundList } ] })有个关键的坑页面初始化时图表容器可能还没渲染完成init会拿到宽高为0的DOM图显示不出来。解决方法是等nextTick再初始化或者在mounted钩子里延迟一下。我建议监听窗口大小变化时调用chart.resize()否则缩放浏览器后图表会变形。看盘刷新这一块也要考虑。新增一单后重新拉数据图表通过setOption更新即可不需要销毁重建性能也可以接受。5. 前后端联调中真实踩过的坑5.1 vue打包放进Spring Bootdist目录的归宿热搜词里“vue打包放进springboot中”热度不低说明这是很多人的痛点。联调开发的最后一步就是要把前端工程和后端工程合到一起打出一个可执行的Jar包给别人看到的效果就是“一个后台管理系统”。步骤本身不复杂前端执行npm run build生成dist目录把dist里的文件复制到Spring Boot的src/main/resources/static下重新打包后端即可。但这里有一连串的坑我一个一个说。第一个坑vue-router用了history模式时前端路由在Spring Boot里刷新页面会404。比如你访问/stockSpring Boot处理不了这个前端路由直接返回404。解决方案有两种一是改成hash模式URL里带#虽然不好看但省事二是后端写一个WebMvcConfigurer把所有非静态资源的路径流转到index.html。我在项目里用的是第二种代码大概这样Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }第二种方案更优雅但要注意别把后端接口路径也拦了接口路径通常带/api前缀正则里已经排除了带点号的路径所以不会冲突。第二个坑dist更新不及时。每次改了前端都要重新npm run build再复制最容易出现“改了前端代码但部署还是旧效果”。我的习惯是写一个启动脚本先构建前端再清理static目录、复制dist、最后用Maven打包一条命令跑完避免手动复制漏文件。第三个坑接口前缀不统一。前端axios请求若直接写/api/stock/list而Spring Boot的RequestMapping没用前缀两边就得约定好。我的习惯是后端统一加一个/api前缀这样在代理转发或部署到同一端口时都可以轻松区分前后端路由。5.2 跨域与端口配置的真实操作开发环境下前端跑在8081Vue默认端口后端跑在8080浏览器访问前端地址时去请求后端接口会跨域。解决跨域的常规方案是配置Vite代理。在vite.config.js里server: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端页面里的/api请求会自动转发到后端8080浏览器的角度看到的是“同源请求”跨域问题直接消掉。上次热搜里有人问“idea 2026 怎么配置springboot服务编辑配置数据比如启动端口”其实Spring Boot改端口就是改application.yml里的server.port改完启动时看日志确认就行不需要在IDEA里额外配置。IDEA的Edit Configurations里只要确保选对了Main class就行。需要注意的是代理配置只作用于开发环境。打包部署时前端dist和后端jar在同一个端口不存在跨域所以axios的baseURL我建议写成相对路径/api不要写死成http://localhost:8080否则打包后照样跨域。5.3 依赖版本与“卡死”的Module问题热搜词里有“springboot版本太高”和“springboot版本”相关这真的是个高频翻车点。Spring Boot版本太高经常出现和旧教程不匹配的问题。最典型的是Spring Boot 3.0以上把javax包迁到了jakarta新手抄旧代码import javax.servlet.*直接编译不过。我给的建议很实际别追新。做毕设和落地项目选Spring Boot 2.7.x这个成熟版本最稳网上资料最多碰到问题一搜就有答案。Vue这边也一样Vue 3 Element Plus是当前主流但如果你的组件库习惯用Element UI那就要配合Vue 2.7别混用。还有“vue安装依赖”的问题。当你收到一个别人给的Vue项目源码通常要执行npm install安装依赖。如果装得很慢或者报错先看npm源是不是默认源切换成国内镜像会快很多。如果node_modules已经存在推荐先删掉再重新npm install避免依赖残留导致的各种灵异问题。给项目之前记得把本地数据库的建表脚本单独导出一个sql文件放进来application.yml里的数据库连接按环境改好。否则源码发给别人对方连数据库配置都找不到项目根本跑不起来。这正好对应热搜里“vue项目源码怎么发给别人”的疑问——发项目包的关键就是把运行环境相关的东西讲清楚包括JDK版本、Node版本、MySQL版本、Redis有没有依赖都要写进README。6. 超越“能跑”项目答辩与高分要点6.1 答辩时的加分功能怎么加如果你的系统只是“能跑”那答辩分数基本是及格线水平。想拿高分我建议在三个功能点上做得比别人深一点点。第一个是库存预警。别只做个“库存低于X显示红色”这种静态判断可以做成动态设置的安全库存。每个商品单独配置预警阈值后台定时扫描后生成预警列表。答辩时你就说“我对超市实际业务做了需求分析像矿泉水这种快消品安全库存要高像高档红酒进货周期长但销量低阈值可以按商品配置。”这个细节一抛出来老师立刻能感受到你做了业务思考。第二个是单据反审核。真实超市运营里单据录错是常有的事。做个“反审核”功能单据审核后还能再撤销撤销时自动回补/扣减库存并写一条反向流水。这个功能虽然增加了一点工作量但它让系统的进销存在逻辑上形成闭环在答辩时能极具辨识度地证明你理解了库存流水的核心。第三个是操作日志。谁在什么时间审核了什么单据、修改了什么商品信息都记录进日志表。不用做得多复杂一张operation_log表加一个AOP切面就行。这个功能说明你有了“系统安全性和可追溯性”的意识是甲方和老师都喜欢看到的东西。6.2 答辩现场的高频问题与应对思路整理几个我当年和周围同学被高频追问的问题提前想好答案别现场组织语言问题一为什么用前后端分离好处是什么答前端专注交互和数据展示后端专注业务逻辑和接口服务分工明确可以并行开发未来前端可以换小程序、App后端接口不用重写部署也更灵活前端静态资源可以用Nginx托管后端只提供服务。问题二如果两张销售单同时扣同一个商品的库存怎么办这就是前面讲过的并发问题。你把数据库行锁的原理说清楚再提一句“如果后续系统扩展成多实例可以引入Redis分布式锁”这个问题就答得扎实了。问题三数据库里的库存字段和历史流水不一致怎么办这其实是“以流水为准”还是“以库存字段为准”的问题。你回答说系统每次出入库都同时更新库存和流水并且流水里存了before和after一旦对不上账可以从库存数值反推检查每一笔流水定位是哪张单据出了问题。这就是可追溯性设计的价值。问题四报表数据怎么来的答报表数据在产生入库/出库流水时同步更新到统计表或者查询时用SUM函数由流水表实时汇总。前者快但实时性差后者慢但数据准。超市场景数据量不大实时汇总即可以后数据量大再引入缓存或定时统计。6.3 演示脚本三分钟讲完你的系统最后给一份答辩演示脚本照着走一遍整个系统就讲清楚了打开首页看板先指给大家看库存总览和预警数量——体现系统的“仪表盘”价值。进入商品管理演示新增一个商品设置安全库存——体现基础档案管理。点击采购入库新增一张采购单选择供应商、添加商品、提交并审核——然后马上切到库存查询页面指出刚才的商品库存增加了再切到流水页面指出新增了一条入库流水。再做一次销售出库同样步骤后回看库存和流水指出数字的变化。打开报表页面展示最近出入库趋势图和商品销量排名——体现数据可视化的价值。这个流程环环相扣每一步都验证了上一步的操作结果逻辑非常清晰。老师跟着你的演示走一遍就完全明白你系统的工作原理了。最后再分享一个小体会做完这个项目最大的收获不是学会了Vue的某个API或者Spring Boot的某个注解而是理解了“业务系统为什么要这么设计”。为什么要有流水表、为什么要事务、为什么单据要审核——这些从书本上看到会觉得很抽象的东西在做完一个真实项目后就全通了。做进销存系统代码只是载体真正的功夫在业务逻辑的梳理上。你能把“进—存—销”这条链路的前因后果想明白这个项目就从“完成”变成了“优秀”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进