资讯详情

资讯详情

Spring Boot + Vue 实战:从零构建一套完整的超市管理系统

做过几个管理类项目之后我越来越觉得超市管理系统是练手价值极高的一类项目业务链条完整、功能边界清晰、既有常见的增删改查又包含库存扣减、订单结算、权限控制这些稍微需要动点脑子的模块。用 Spring Boot Vue 这套组合去实现一个超市管理系统正好能把前后端分离开发从“会写接口”推到“能设计整套业务”的程度。这篇博文我结合自己实际做过的项目把从需求拆解、数据库设计到前后端实现的关键环节以及踩过的几个坑完整梳理一遍。1. 项目到底要做什么需求拆解才是第一步很多人在拿到“超市管理系统”这个题目时第一反应是建表、写 CRUD。但真上手之后才发现需求边界不划清楚后面每一步都在返工。所以我建议先花半天时间把用户角色和业务场景理清楚。1.1 核心场景与用户角色超市管理系统的使用者粗分至少有三种角色收银员、仓库管理员、系统管理员。收银员关心的是结账快、操作简单仓库管理员关心的是商品出入库、库存余量、保质期预警管理员关心的是员工账号、商品上下架、销售报表和系统基础配置。还有一类容易被忽略的使用者是店长或老板本人。他们要看的不是某个订单而是“今天卖了多少”“哪些商品滞销”“库存资金占了多少”。所以销售统计和商品分析功能虽然不是日常高频操作却往往是这个系统真正的价值所在。我在设计功能清单时会先把角色代入场景收银台场景快速扫码、商品搜索、购物车、结算、抹零、小票打印仓库场景入库、出库、库存查询、库存预警、报损登记管理场景商品信息维护、分类管理、供应商管理、员工账号管理决策场景日/月销售报表、分类销售占比、库存周转情况这样拆完之后系统功能模块就非常清晰了不会出现做到一半突然发现“啊忘记做退货了”这种尴尬。1.2 功能模块边界划分合理的模块划分能让你在后端写 Service 时不至于绕来绕去。我当时把系统分成七个核心模块模块核心功能关联角色用户登录账号认证、Token签发、角色鉴权全部角色商品管理商品建档、上下架、分类维护、供应商管理管理员库存管理采购入库、退货出库、库存查询、预警设置仓库员销售管理购物车、订单结算、退货退款收银员会员管理会员建档、积分累积、折扣策略收银员报表统计销售日报、商品排行、利润概览管理员系统管理员工账号、角色权限、操作日志管理员模块边界划好之后后面写接口文档和分配开发任务都会轻松很多。尤其是自己做一个完整项目时这个清单能帮你随时提醒自己“当前功能属于哪个模块不要牵扯进无关逻辑”。2. 技术选型与架构设计这套组合到底好在哪2.1 后端为什么不选更重的框架Spring Boot 现在是 Java 后端开发事实上的标准尤其是对于这类中小型管理系统它的优势非常明显自动配置机制免去了大量 XML 配置内嵌 Tomcat 让部署变成一个 jar 包的事生态里对 MyBatis、Redis、Spring Security 的整合也相当成熟。搭配 Java 17 或 21 长期支持版本无论从代码稳定性还是从开发效率上看都比较稳妥。ORM 层我选的是 MyBatis Plus而不是纯 MyBatis 或 JPA。原因很简单项目里至少有七八张核心表每张表都要做分页查询和条件筛选MyBatis Plus 内置的BaseMapper和LambdaQueryWrapper能省掉大量重复的 XML 映射写法和条件拼接代码。遇到报表类复杂查询再用自定义 SQL 补充这样既保证了开发速度也留住了 MyBatis 在复杂 SQL 上的灵活性。权限认证用的 JWT Stateless 方案。超市管理系统的用户量不大没必要引入 Session Spring Security 的完整会话语义用 JWT 做登录态下发和接口鉴权配合拦截器校验简单直接。当然JWT 的缺点也明显无法主动失效所以我会在 Redis 里维护一份 Token 黑名单处理改密码和强制下线场景。2.2 前端为什么要用 Vue 3 Element PlusVue 3 的 Composition API 组合式写法在管理类页面里比 Options API 要顺手很多。比如一个商品列表页需要同时处理搜索条件、分页数据、选中状态、表单弹窗逻辑都集中在script setup里代码可读性和复用性都更好。组合式函数composables还能把“请求列表数据”“处理分页变化”这类逻辑抽取出来多个页面共用。Element Plus 是目前 Vue 3 生态里最完整的 UI 组件库表格、表单、对话框、消息提示、日期选择器开箱即用。管理类系统的 UI 风格偏功能导向不需要花太多精力做视觉设计Element Plus 的默认风格加上少量定制就能达到可用的程度。构建工具直接上 Vite开发环境热更新非常快每秒保存都能看到页面刷新。2.3 前后端分离架构下的部署形态整个系统采用前后端完全分离的架构后端模块按“分层”组织Controller层只做参数接收和响应封装Service层写业务逻辑Mapper层管数据访问。前端是独立的 Vue 3 项目通过 Axios 调用后端接口。开发环境下前端通过 Vite 代理解决跨域部署环境则用 Nginx 同时托管前端静态资源和反向代理/api路径。我比较推荐这种部署方式因为服务器只需要开 80/443 端口不暴露后端服务端口安全性更好。提示前后端分离不是把前端文件和 Java 代码放在同一个工程里就叫分离。真正的分层是前端只负责交互逻辑后端只提供数据接口两者通过约定好的 JSON 结构通信这样前端可以做独立构建和部署。3. 数据库设计把表建好项目就稳了一半数据库设计是这个项目里我花时间最多也最值得写一写的部分。表设计好了接口写起来行云流水表设计不合理后面每个查询都要带一堆 join 和条件判断。3.1 核心表结构与设计要点我最终设计的核心表有八张这里挑几张重点说的用户表sys_user字段类型说明idbigint主键usernamevarchar(50)登录账号唯一passwordvarchar(100)BCrypt 加密后的密码real_namevarchar(50)姓名rolevarchar(20)admin / cashier / stockstatustinyint1启用 0停用create_timedatetime创建时间密码字段我专门强调一下一定不要用明文也不要自创 MD5 拼接盐的方式直接用 Spring Security 的 BCryptPasswordEncoder或者用 Hutool 封装的 BCrypt 工具。虽然系统规模不大但从第一行代码起就把安全习惯养好是值得的。商品表product字段设计上除了常规的product_name、price、stock_quantity我额外加了barcode条码、category_id分类外键、supplier_id供应商和status上下架状态。条码是收银台扫码的关键依据要建唯一索引。价格用 decimal 类型避免浮点精度问题。销售订单表与订单明细表销售订单表存订单主信息和订单明细表分开设计这是很经典的“一父多子”结构。主表存总金额、收银员、支付方式、会员 ID、下单时间明细表存每个商品的单价、数量、小计金额通过order_id关联主表。查询某订单详情时先查主表再查明细表逻辑非常清晰。库存流水表stock_flow这张表是我觉得这个项目里最有价值的设计之一。每次入库、出库、报损都往这张表里写一条记录记录商品 ID、变动数量、变动类型、操作人、时间。有了它库存数量查商品表库存变动原因查流水表对账和排查问题都方便很多。不少同学省略这张表结果出现库存对不上时根本说不清楚数据是怎么变的。3.2 表关系与事务边界设计表关系相对简单商品表关联分类表和供应商表多对一订单明细表关联订单主表和商品表多对一库存流水表关联商品表。但表关系简单不代表事务边界可以随便定。我梳理的实际业务事务场景有三个创建订单 扣减库存 累加会员积分这是一个完整的强一致事务采购入库 增加库存 记录库存流水同样要在同一事务里商品退货 恢复库存也要保证一致性这三种场景都要用Transactional注解包住而且要注意事务方法必须通过 Spring 代理调用。很多人遇到的“事务没生效”问题基本都是因为在同一个类里内部方法调用绕过了代理。一个排查经验是在 Service 里注入自己的类或者把需要事务的方法放到独立 Service 中。4. 后端业务逻辑实现核心模块的代码思路后端代码结构我习惯按功能域分包而不是按技术分层乱堆。这里分享几个核心模块的实现思路。4.1 登录认证与权限控制流程不复杂用户提交用户名密码后端校验通过后生成 JWT Token其中在claims里带上用户 ID 和角色信息返回给前端。前端存储 Token后续每个请求在请求头里带上Authorization: Bearer xxx。后端拦截器会拦截所有/api/**请求先从请求头取 Token解析校验。校验通过后把用户信息放进ThreadLocal或请求属性方便后续获取当前用户。接着判断当前角色是否能访问这个接口管理员放行收银员只允许访问销售相关接口仓库员只允许访问库存相关接口。Controller 层我比较推荐配合自定义注解做权限控制例如定义RequireRole(admin)注解在拦截器里读取接口上的注解做角色判断。这样权限逻辑集中在拦截器里业务代码不用到处写判断。4.2 商品管理与库存预警商品管理的核心接口就四个分页查询、新增、编辑、上下架。分页查询我采用 MyBatis Plus 的Page对象加上LambdaQueryWrapper动态拼接查询条件。前端传keyword、categoryId、status后端通过if判断参数是否为null来决定是否加条件。库存预警我设置了两个阈值上限和下限。商品库存低于下限时列表标红提示“库存不足”高于上限时提示“库存积压”。这个阈值可以由管理员在系统配置里调整。每当入库或销售完成后后端顺手查一下该商品库存是否触及阈值如果触及就写一条预警记录前端登录后就能看到通知。4.3 销售结算与库存扣减的并发处理销售结算是最容易出问题的地方。我们设想收银台场景两个收银员同时卖最后一个库存为 1 的商品如果代码是先查库存、判断够不够、再更新库存那么很可能出现超卖。简单可靠的方案是使用数据库的乐观锁或原子更新。我在商品表加了一个version字段更新库存时带上WHERE id ? AND version ?如果更新影响行数为 0说明版本冲突重新查库存再尝试。这个方案实现成本低对这个场景已经完全够用。对于库存扣减我写的核心更新语句类似于UPDATE product SET stock_quantity stock_quantity - #{count}, version version 1 WHERE id #{productId} AND stock_quantity #{count} AND version #{version}注意条件里带了stock_quantity count这一步就把“库存不足时不允许扣减”的校验放到了数据库层面代码层面不需要先查再判安全很多。影响行数为 0 时直接在 Service 里抛出异常事务回滚订单也就创建失败。4.4 销售报表的 SQL 实现报表统计这里我踩过不少坑核心要理清“查询范围”和“聚合粒度”。比如销售日报按天统计营业额用GROUP BY DATE(order_time)聚合分类销售占比用商品表关联明细表再按分类聚合销售金额。一个实用经验是报表查询不适合用 MyBatis Plus 的通用方法硬拼直接在 Mapper XML 里写清晰的 SQL 更方便。当天销售统计的核心 SQL 大致是SELECT DATE_FORMAT(o.pay_time, %Y-%m-%d) AS day, COUNT(DISTINCT o.id) AS order_count, SUM(o.total_amount) AS total_sales, SUM(o.discount_amount) AS total_discount FROM orders o WHERE o.pay_time #{startDate} AND o.pay_time #{endDate} GROUP BY DATE_FORMAT(o.pay_time, %Y-%m-%d)这类统计接口要注意索引设计orders表的pay_time字段一定要建索引否则数据量到几万条后查询会明显变慢。5. 前端 Vue 页面与状态管理交互细节决定体验5.1 前端项目结构组织前端项目我按模块划分目录而不是单纯按技术类型分。结构大致是这样的src/ api/ # 每个模块一个请求文件 views/ # 页面组件 components/ # 通用组件 composables/ # 组合式函数 router/ # 路由配置 stores/ # Pinia 状态管理 utils/ # 请求封装、工具函数api目录里按照后端模块建文件比如product.js、order.js、stock.js每个文件导出一组封装好的接口函数。页面组件里不再直接写axios.post(/api/product/page)这类裸请求而是调用getProductPage(params)这样即使后端接口 URL 变化也只需要改一个文件。请求封装在utils/request.js里统一处理从 Pinia 读取 Token 并加到请求头响应拦截器统一判断 HTTP 状态码和业务码401 时自动清理本地登录态并跳转登录页。这个小细节能让你在开发时少写大量 if 判断。5.2 收银台页面的交互设计收银台是使用频率最高的页面交互设计直接决定收银效率。我的实现方案是页面左侧是商品搜索框和商品列表支持条码精确匹配和名称模糊匹配按回车或点击后加入右侧购物车。右侧购物车展示商品清单、数量、单价、小计支持修改数量、删除商品。底部显示应收合计点击“结算”弹出支付面板选择现金、微信、支付宝或会员卡带折扣时自动计算折后金额。这个页面不用弹窗装商品信息尽量把所有操作都放在一个页面里完成减少切换成本。结算成功后清空购物车和商品搜索框显示订单号和找零信息为下一位顾客做好准备。5.3 前后端联调的关键点联调阶段最常见的问题集中在字段命名和数据类型上。后端返回时间字段是LocalDateTime默认序列化成 ISO 字符串前端的日期组件可能直接显示成 UTC 格式需要统一设置 Jackson 的日期格式并配置时区。另一个关键点是分页参数的约定。我用 MyBatis Plus 的分页插件时默认参数名是current和size但前端习惯用pageNum和pageSize。解决方案很直接在后端写一层 VO 接收对象或者在全局配置里统一设置参数名。沟通好约定后把接口文档维护在像 Apifox 这样的工具里前后端各自照着文档联调能少不少扯皮。6. 踩坑记录与问题排查实录这部分内容来自实际开发中的真实教训值得仔细看看。6.1 跨域问题为什么会反复出现开发环境下后端启动在localhost:8081前端启动在localhost:5173浏览器默认会拦截跨域请求。我有段时间为了省事在后端写了一个全局 CORS 配置放行所有来源结果部署到服务器上之后发现接口虽然通但带 Token 的请求偶尔丢失。排查后发现问题在于预检请求。后端接口在 OAuth 等复杂场景下需要应对浏览器的 OPTIONS 预检请求如果拦截器拦截了 OPTIONS 请求会直接返回 401导致前端无法发起实际请求。解决办法很简单在拦截器里放行 OPTIONS 请求同时前端开发时通过 Vite 的proxy配置代理部署时由 Nginx 反向代理这样代码里根本不需要做跨域处理。6.2 BigDecimal 精度问题在金额计算里绝对不能妥协如果所有金额都按double算当计算折扣和找零时出现0.1 0.2 0.30000000000000004这样的结果是必然的。订单总价按分计算折扣优惠也按分计算这样从源头避免精度问题同时数据库字段用DECIMAL(10, 2)存金额。后端返回给前端时我用字符串序列化金额字段前端展示直接用字符串不转Number彻底避开浮点陷阱。6.3 库存对不上账之后的流水追踪项目上线测试阶段我遇到过一次库存数据和实际盘点对不上的情况。当时第一反应是去查订单表结果发现销售数据看起来没问题。后来把问题定位到是“退货”操作没有恢复库存或者恢复库存时没有检查当前商品是否已上下架。从那以后我在库存相关的每一个操作里都加入了stock_flow流水记录并新增了一个“库存对账”功能把某段时间内所有stock_flow变动量与商品当前库存做比对不一致时一条条追踪流水很快就能定位到是哪一笔操作漏了。6.4 前端权限控制不能只是“隐藏按钮”前端做权限控制时常见做法是根据角色判断要不要显示某个按钮或菜单。但真正可靠的权限控制必须在后端做前端隐藏按钮只是提升用户体验。我遇到过一个问题收银员角色明明没有商品新增权限前端也隐藏了按钮但有人拿着接口文档直接请求新增接口照样能创建商品。所以后来把所有写操作都加了后端角色校验前端只是配合展示。记住一个原则后端接口是所有安全控制的第一道也是最后一道防线。7. 实测体验与改进方向项目完成后我把整个系统部署到一台低配服务器上实测了一下前后端运行很稳定接口响应基本都在 30ms 左右。从实际使用角度来说这套系统的核心链路已经能支撑一个中小型超市的日常运营但真要长期落地还有几个方向值得继续做。商品批量导入导出功能值得加上。超市的商品数量通常上千每个商品条码、进价、售价、库存上限都手工录入效率太低。用 EasyExcel 封装一个批量导入模板仓库员填表后一键导入能节省大量时间。另一个值得扩展的是多门店支持。如果在商品表加一个store_id字段再调整库存和订单的查询逻辑系统就可以从一个单店版本平滑升级为连锁店版本。这一步的代码改动量不算大但设计层面需要提前预留扩展点。最后说一点实际体会做完这个项目之后我对“管理系统的本质是数据处理”这句话的理解比之前深了很多。它的难点不在于某个高深技术而在于你能否把现实中模糊的业务流程转化成精确、可靠的代码与数据结构。动手做之前多花点时间理清需求规划好数据表后面的开发会顺很多。这也是为什么我一直建议想练全栈的同学优先选这类业务完整、边界清晰的题目。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →