十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot3+Vue3的连锁超市库存订单系统设计与实现

基于SpringBoot3+Vue3的连锁超市库存订单系统设计与实现 简介项目编号25338面向毕业设计/课程设计人群提供大型连锁超市库存与订单管理系统完整源码与配套资料。系统采用SpringBoot3Vue.js3前后端分离架构涵盖管理后台与用户前台结合MySQL8数据库实现库存、订单、商品等核心业务模块适合需要学习现代Web开发全流程的Java学习者。压缩包内共4个文件包含源码压缩包、MySQL数据库脚本、需求文档以及演示录屏整体大小45.1MB结构清晰便于快速部署与二次开发。已有110人学习下载配套启动教程视频可帮助使用者快速上手。读者可获得完整可运行的工程源码、数据库建表脚本、需求分析文档和操作演示视频既能作为毕业设计答辩的支撑材料也可用于课程设计或项目实训帮助理解前后端分离架构的真实落地方式。1. 连锁超市库存订单系统的整体架构与前后端分离选型接触过传统单体进销存系统的同学应该对那类“一个Tomcat跑完所有页面和接口”的架构不陌生。这类系统改一处模块就要重新编译整个Web应用库存和订单的并发控制更是靠synchronized硬撑连锁超市这种多门店、多SKU、频繁出入库的场景很快会到瓶颈。这套毕业设计项目并没有走老路而是选了SpringBoot3做后端纯接口服务Vue.js3分别构建管理后台与用户前台MySQL8承担数据持久化属于典型的分离式Web应用。它的价值不止于完成任务书上的功能点更重要的是展示了一条从数据库设计到接口联调、再到前端部署的完整链路。适合正在做Java课程设计或毕业设计的读者也适合想快速搭一套可演示的进销存原型的开发者。2. SpringBoot3 后端从实体映射到库存扣减接口的实现2.1 数据库表结构与JPA映射的取舍这套系统的核心表主要有商品表含库存字段、门店表、订单主表、订单明细表、库存变动流水表。在SpringBoot3项目里如果用Spring Data JPA实体类通过注解映射到MySQL8的表结构开发效率很高如果对SQL有掌控力MyBatis-Plus的BaseMapper也能快速完成单表操作。我一般建议毕业设计优先用MyBatis-Plus因为它的ServiceImpl自带常用方法调试时能看到完整SQL和MySQL8的JSON字段配合也顺。以商品表为例核心字段应包含total_stock总库存、locked_stock锁定库存和available_stock可用库存。总库存减去锁定库存才是真正可售的数量。这个设计在连锁超市场景下尤其重要因为订单创建后系统需要先在锁定库存里占住货等支付成功再扣减总库存否则用户下单后其他门店或渠道可能把货卖超。Entity Table(name product) public class Product { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name product_name, nullable false, length 128) private String productName; Column(name total_stock, nullable false) private Integer totalStock; Column(name locked_stock, nullable false) private Integer lockedStock; Version private Integer version; public Integer getAvailableStock() { return totalStock - lockedStock; } }上面加了Version字段这是JPA的乐观锁写法。每次更新库存时Hibernate会自动在UPDATE语句后拼接WHERE version ?如果版本不一致则更新失败。这个字段在源码中未必存在但如果你要处理订单并发扣减建议在后端补上。getAvailableStock()没有映射到数据库列它属于派生属性在接口返回时能直接暴露给前端展示避免每次都在SQL里手动做减法。2.2 订单创建接口的事务边界与幂等性订单系统的核心难点不是CRUD而是创建订单时同时操作订单主表、明细表和库存表的原子性。常见的错误是把saveOrder、saveOrderItem、reduceStock三个方法分开写然后在Controller里依次调用任何一步失败都会造成脏数据。正确做法是把它们收敛到一个Service方法内加上Transactional。Service RequiredArgsConstructor public class OrderService { private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; private final ProductMapper productMapper; Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateRequest request) { // 1. 幂等性校验根据业务单号判断是否已处理 Order order new Order(); order.setOrderNo(request.getOrderNo()); order.setStoreId(request.getStoreId()); order.setStatus(1); orderMapper.insert(order); int totalAmount 0; for (OrderItemRequest item : request.getItems()) { // 2. 乐观锁扣减可用库存更新行数1才算成功 int rows productMapper.reduceAvailableStock( item.getProductId(), item.getQuantity()); if (rows 0) { throw new BusinessException(库存不足或版本冲突); } OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(item.getPrice()); orderItemMapper.insert(orderItem); totalAmount item.getPrice() * item.getQuantity(); } order.setTotalAmount(totalAmount); orderMapper.updateById(order); return order.getId(); } }这段代码的逻辑是先创建订单主记录然后遍历购物车明细每件商品调用一次reduceAvailableStock该方法生成的是UPDATE product SET locked_stock locked_stock #{quantity} WHERE id #{productId} AND total_stock - locked_stock #{quantity}。数据库行锁会保证同一商品在同一时刻只有一个订单能成功更新配合Version可以拦截重复提交。rollbackFor Exception.class很关键Spring默认只在RuntimeException时回滚如果自定义的业务异常继承Exception而你没指定事务不会回滚库存会被白扣。同时要注意createOrder的幂等性。前端在用户点击“提交订单”时如果网络抖动又重试了一次就会生成两单。在我的习惯里order_no字段必须有唯一索引第一次请求正常插入第二次请求插入时触发DuplicateKeyException捕获后直接返回第一次的订单号。这样既不需要引入Redis分布式锁也能挡住大部分重复提交。3. Vue.js3 管理后台SPA路由、状态管理与响应式列表3.1 Vite 工程结构与路由懒加载前端部分用Vue.js3作为核心框架配合Vite构建管理后台和用户前台是两个独立的SPA工程。这样设计的好处是管理员端和C端用户在权限模型、路由表、样式体系上完全不同拆分后可以单独打包互不干扰。每个工程内部的结构建议按views、components、api、router、store五个目录组织和常见的Vue3后台模板保持一致。路由配置中库存管理和订单管理这类页面适合做懒加载。所谓懒加载就是用户第一次访问该路由时才去请求对应的js文件。连锁超市后台的商品类目和门店列表往往很长如果首屏全量加载白屏时间会让人难以接受。Vue Router的component: () import(...)写法足够应付。const routes [ { path: /inventory, name: Inventory, component: () import(/views/inventory/InventoryList.vue), meta: { requiresAuth: true } }, { path: /orders, name: Orders, component: () import(/views/order/OrderList.vue), meta: { requiresAuth: true } } ]meta.requiresAuth用于在全局前置守卫里做登录态检查比如通过localStorage读取token不存在就重定向到/login。这和SpringBoot后端的HandlerInterceptor是对应关系前端守卫解决“看不到页面”的问题后端拦截器解决“接口不可调”的问题。实际项目中前后端两层校验缺一不可因为用户可以直接在浏览器控制台调用fetch接口绕开路由守卫。3.2 使用 axios 封装请求并渲染库存预警列表管理后台最重要的页面之一是库存预警。超市运营每天要看哪些SKU库存低于安全线。这个功能的前端实现并不复杂但请求封装的质量直接关系到整个系统的维护成本。我把axios实例统一放在api/request.js里设置基础URL为/api并添加请求拦截器附带token、响应拦截器统一解包code字段。这样业务页面里只需要关心data部分遇到401则跳转登录页。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response error.response.status 401) { router.push(/login) } return Promise.reject(error) } )在库存预警页面我通常会在onMounted中调用一个fetchInventoryWarningList方法把后端返回的列表数据用ref包起来模板里通过v-for渲染。这里要注意的是timeout: 10000只用于防止请求长时间挂起不能替代后端对慢SQL的优化。如果库存数据量到了十万级别前端列表还需要做分页或虚拟滚动否则DOM节点太多页面会明显卡顿。另外供应链场景里库存预警的阈值往往不是一个固定数字而是根据最近7天日均销量动态计算这部分逻辑一般放在后端做前端只负责把阈值字段显示出来。4. 订单管理实战超卖防护与库存流水对账4.1 连锁超市的并发扣减场景大型连锁超市的订单并发量虽然比不上秒杀系统但在促销日或门店App开放领券时某款鸡蛋或牛奶可能在几分钟内被几千个用户同时下单。如果代码写成SELECT total_stock FROM product WHERE id ?判断大于0后UPDATE那就必然出现超卖。上面第2章已经给出了乐观锁方案但乐观锁在高冲突下会让大量请求失败用户体验差。这时可以考虑升级为数据库悲观锁SELECT ... FOR UPDATE会锁住商品行直到事务提交。在SpringBoot3中通过Lock(LockModeType.PESSIMISTIC_WRITE)可以让JPA生成带FOR UPDATE的SQL。但悲观锁和事务要配合好否则库存锁会持有很久。我的习惯是创建订单的Service方法里只保留扣减库存和生成订单这两个最必要的操作把优惠券计算、会员积分累加等逻辑放到订单创建成功后通过TransactionalEventListener异步执行。这样事务短锁释放快MySQL8的InnoDB行锁压力也会小很多。4.2 库存流水表与对账SQL光有库存被正确扣减还不够出问题时要能定位“哪笔订单扣了哪个商品当时库存从多少变成多少”。因此系统里必须有一张stock_change_log表记录每次库存变动的完整上下文。这张表在演示时可能显得不起眼但在实际排错时价值极高。字段名类型说明idbigint主键product_idbigint商品IDchange_typetinyint1-下单锁定2-支付扣减3-取消解锁4-入库增加change_quantityint变动数量正数增加负数减少before_stockint变动前可用库存after_stockint变动后可用库存order_novarchar(64)关联订单号create_timedatetime创建时间每次扣减库存时应该在同事务里插入一条流水记录。before_stock和after_stock可以在应用层计算也可以直接在SQL里通过RETURNING子句拿回更新后的值。MySQL8虽然不支持RETURNING但可以用UPDATE后紧接着SELECT获取只要在同一个事务中数据就是一致的。-- 找出所有库存小于安全阈值且存在未完成订单的商品一次性列出明细 SELECT p.product_name, p.total_stock, p.locked_stock, p.total_stock - p.locked_stock AS available_stock, o.order_no, oi.quantity FROM product p JOIN order_item oi ON p.id oi.product_id JOIN orders o ON oi.order_id o.id WHERE p.total_stock - p.locked_stock p.warn_stock AND o.status IN (1, 2) ORDER BY available_stock ASC;这条SQL用于“哪些订单占着库存但还没支付完成”的排查。如果发现大量status1的订单待支付占用了locked_stock就需要业务侧设置超时未支付自动取消订单并调用库存解锁逻辑。否则用户付款时发现库存被那些“僵尸订单”锁死了体验会非常糟糕。我通常还会写一段月度对账的SQL按order_no分组汇总order_item的数量和金额再和orders.total_amount比对任何不一致都说明订单创建逻辑或定时任务补发逻辑有缺陷。5. IDEA 创建 SpringBoot3 项目的启动细节与 MySQL8 时区坑5.1 SpringBoot3 初始化步骤上手这套系统时很多人卡在第一步用IDEA创建SpringBoot3项目。如果你使用的IDEA版本自带的Spring Initializr默认生成SpringBoot2.x可以手动在pom.xml里把版本改为3.x或者在start.spring.io生成后导入。SpringBoot3要求JDK17及以上这也是一个常见起点错误。启动类通常长这样package com.supermarket; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class SupermarketApplication { public static void main(String[] args) { SpringApplication.run(SupermarketApplication.class, args); } }SpringBootApplication包含ComponentScan、EnableAutoConfiguration和Configuration。它生效后mapper包下的接口需要额外加MapperScan或者每个接口标注Mapper。在我看到的很多SpringBoot3项目里新建工程时容易忽略这段配置导致运行时报NoSuchBeanDefinitionException。启动前还需要确认application.yml中的数据库连接参数与MySQL8实际环境匹配。5.2 MySQL8 时区与驱动问题Java和MySQL8对时区的处理方式与早期MySQL5.x不同。连接串里必须显式指定时区否则会出现“服务器时区值未被识别”的异常。推荐使用serverTimezoneAsia/Shanghai并配合连接属性useSSLfalse和allowPublicKeyRetrievaltrue。后者在MySQL8用户采用caching_sha2_password认证时是必须的否则启动后第一次访问数据库就会报Public Key Retrieval is not allowed。spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这段配置里驱动类名使用的是com.mysql.cj.jdbc.Driver这是MySQL8后的新驱动位置。如果你复制了老项目的com.mysql.jdbc.Driver在SpringBoot3里会直接启动失败。characterEncodingutf8建议保留它能避免中文字段乱码但前提是数据库表本身也是utf8mb4字符集。MySQL8默认字符集已经是utf8mb4所以建表SQL里不需要刻意加DEFAULT CHARSET。5.3 通过录屏与日志定位联调问题项目压缩包里如果带了演示录屏别急着快进看完就关。我一般会对照录屏里的操作顺序检查自己的环境差异。比如录屏里用的是npm run dev启动前端并访问http://localhost:5173那你本地Vite端口被占用时需要在vite.config.js里改server.port或使用--port参数。后端接口默认8080时如果前端请求返回404先看Vite的代理配置是否把/api转发到了正确地址这个比检查后端代码更省时间。在IDEA里启动SpringBoot3后如果出现APPLICATION FAILED TO START可以直接看控制台最前面的Description段。多数问题是数据库连不上、端口占用、Mapper未被扫描三类。端口占用可以用server.port8081临时规避但更推荐用netstat -ano | findstr :8080找出占用进程。前后端联调阶段按F12打开浏览器开发者工具在Network面板里查看“库存列表”请求的响应体如果code字段是500再去后端控制台找异常堆栈定位效率会提高很多。本文还有配套的精品资源点击获取
返回列表