资讯动态

Spring Boot+Vue批发商城毕设实战:订单状态机与库存防超卖

发布时间:2026/10/13 10:55:37 来源:尧图企业网站定制
02575这个编号熟悉毕业设计这套流程的同学应该一眼就懂——这是我们学院选题目录里的一个独立项目编号对应的课题就是“泉州男装批发服务平台”。我当时在目录里翻了很久最终敲定它的理由很简单它不是那种纯教学性质的CRUD模板而是一个带着真实业务背景、能讲清楚完整交易链路的系统。从采购商登录浏览、加购下单到商家接单发货、后台数据统计一整条闭环跑下来无论是写代码还是最后答辩心里都有底。这篇文章我就用做项目时踩坑复盘的口吻从需求拆解、功能设计、数据库建模、源码实现到答辩准备把这个平台从头到尾拆给你看。如果你正在做电商批发类毕设或者只是想搞清楚一套批发业务系统是怎么搭出来的这篇应该能帮你少走不少弯路。1. 项目整体拆解这个平台到底在解决什么问题1.1 需求源头批发行业的现状与毕业设计的选题价值先说说这个课题的业务背景。泉州在国内男装产业带里是有分量的大量中小服装厂、档口、批发商聚集在这一带线下批发市场非常成熟。但传统批发模式有个很典型的问题采购商要拿货得跑到市场去一家家档口看款、比价、下单信息不透明效率也低。尤其是外地客户要了解款式、库存、批发价成本很高。线上批发平台要解决的就是把“拿货”这件事数字化商家在平台上发布商品、设置阶梯批发价、管理库存采购商注册成会员后浏览商品、按批发规则下单、跟踪订单状态。这个定位和普通零售电商有明显差异——批发更强调批量、会员价、起批量约束而不是零售那种单件购买、促销凑单的逻辑。从毕业设计选题的角度看这个项目有几个先天优势业务场景真实需求边界清晰不像“学生管理系统”那样千篇一律功能模块多但彼此关联有商品、有订单、有权限、有统计能完整展示开发能力商业模式上有“多商家入驻”的概念可以顺势做出不同角色的权限控制这是答辩时的加分点。说白了选一个能“讲出业务故事”的课题比堆砌十个互不相干的功能模块有用得多。1.2 角色与核心功能盘点这个平台我拆成了三个核心角色系统管理员、批发商家、采购商会员。每个角色看到的界面、能操作的功能是完全不一样的这也是权限设计的主要依据。角色核心职责主要功能模块管理员平台运营与监管商家审核、用户管理、商品审核、分类管理、订单监管、数据统计批发商家商品供给与订单履约商品发布与上下架、库存管理、订单发货、价格设置、店铺信息管理采购商批发采购商品浏览/搜索、购物车、提交订单、订单支付/确认收货、收货地址管理整个系统的核心业务流其实是一条主线商家发布商品 - 采购商浏览商品并加购 - 按批发规则结算下单 - 商家接单发货 - 采购商确认收货 - 管理员全程监控。我在做完这个项目之后最大的体会是一个毕设系统有没有完整度就看你能否把这条主线从头到尾走通。很多同学做电商类项目只做到了“下单”就停了后面发货、确认收货、订单状态流转全部砍掉那答辩时被问“订单状态怎么变化”就很被动。这个项目把状态流转做全了这也是它能作为案例分析课代表的原因之一。1.3 为什么选择这套技术栈技术选型这块我直接说结论然后用一段话解释为什么这么选。后端用的是 Spring Boot MyBatis-Plus MySQL Redis前端是 Vue 2 Element UI Axios鉴权统一用 JWT。Spring Boot 是目前毕业设计里的绝对主流生态成熟、资料多、遇到问题随便搜就有答案。MyBatis-Plus 的好处是单表 CRUD 几乎不用手写 SQL能让你的精力集中在订单、库存这类复杂业务上而不是反复写 mapper 的增删改查。MySQL 存业务数据Redis 做缓存和临时数据存储比如验证码、首页轮播、商品热度统计这些在实际项目里很常见写在简历上也不虚。前端用 Vue 2 Element UI组件化开发界面做出来比较专业老师一看就觉得像个正经系统。有人可能会纠结要不要上 Spring Cloud 微服务或者前后端分离加网关。我的建议是除非你基础非常好而且时间充裕否则毕业设计别硬上微服务。单体应用加上模块化分层已经足够表达你的工程能力。反而是一套你能完全驾驭的简单架构在答辩时更有底气。评阅老师更关心的是你的代码逻辑清不清楚、业务有没有闭环、数据库设计是否合理而不是你有没有用上最新版的框架。1.4 系统架构与部署形态这套系统的部署架构并不复杂属于典型的前后端分离单体应用浏览器 - 前端工程Vue开发端口 8081 - 后端接口Spring Boot端口 8080 - MySQL业务数据 - Redis缓存/临时数据本地开发时后端直接用 IDEA 启动前端用npm run serve跑起来。生产打包的话后端打 Jar 包前端npm run build之后可以丢到 Nginx 里托管同时用 Nginx 做接口代理解决跨域问题。不过毕设阶段跑到开发环境就够了准备好一键启动说明就行。整个工程目录大致是这样分工的backend/ src/main/java/com/campus/menswear/ controller/ # 接口层 service/ # 业务逻辑层 mapper/ # 数据访问层 entity/ # 数据库实体 dto/vo/ # 入参出参对象 config/ # 配置类 util/ # 工具类 src/main/resources/ mapper/ # MyBatis XML 文件 application.yml # 配置文件 frontend/ src/ api/ # 接口请求封装 views/ # 页面 components/ # 公共组件 router/ # 路由 store/ # 状态管理源码在手的时候先按这个结构找到入口然后顺着“登录 - 商品列表 - 下单”这条路径读代码比从头到尾硬啃效率高得多。2. 核心功能模块的设计与实操要点2.1 商品管理与多级分类批发SKU的处理方式商品管理是批发平台的重头戏也是和零售系统差异最大的地方。批发市场的衣服通常不是一件件卖的而是按“一手”“一打”“整箱”来出货并且有颜色、尺码多个维度。数据库层面如果只用一张商品表存“名称价格库存”后面做订单的时候会非常痛苦。我的做法是拆成商品 SPU 和规格 SKU 两层SPU 表示一个款式的抽象信息比如“商务休闲修身衬衫”包含标题、主图、详情描述、分类SKU 则是这个款式下的具体规格组合比如“黑色 / 42码 / 库存 50件”每个 SKU 有自己的库存、批发价和商品编码。这样设计的好处是前端商品详情页可以灵活选择颜色尺码后端处理订单时能精确锁定到某个 SKU而不是笼统扣商品总库存。批发价格也需要单独考虑。零售一般就是一个售价但批发客户买得多单价理应便宜。所以数据库里我加了一张价格区间表按购买数量设定多个档位数量区间单价1-9 件128 元10-49 件108 元50-99 件98 元100 件以上88 元结算时系统根据购物车中某个 SKU 的总数量自动匹配对应档位计算价格。这一步看似简单但实际上是个很容易被忽略的隐藏需求。如果评委老师问“你的批发价是怎么算的”你能直接说出这个阶梯逻辑是会加分的。商品图片上传也是一块容易出问题的环节。项目里我用的本地磁盘存储方案上传接口把图片保存到指定目录然后返回可访问的 URL 写入数据库。开发时注意后端要配置静态资源映射否则前端img标签访问不到图片。例如 Spring Boot 里这样映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); } }如果你准备用云存储那也是可以的但需要额外引入 SDK并且要小心跨域和防盗链问题。本地存储对毕设来说完全够用。2.2 订单流转与库存扣减答辩必问的难点订单模块是整个系统业务最密集、答辨时最容易深挖的部分。我在设计订单状态时直接沿用了电商领域的标准状态机待付款 - 待发货 - 已发货 - 已完成 \- 已取消付款前可取消超时自动取消 \- 退款中 - 退款完成发货前可申请退款订单表的status字段存的是一个数字枚举前端根据枚举值显示对应的按钮和文案。这样设计的好处是状态机清晰后端逻辑不会乱套。库存扣减这里有个很经典的坑不能在下单时才去更新库存也不能每次查询都直接读数据库。更合理的做法是“预扣库存”或者“下单锁库存”。也就是说采购商提交订单时系统先把对应 SKU 的库存扣掉一部分作为锁定如果订单超时未支付再把库存释放回来。代码层面要注意并发安全。最简单的方案是使用乐观锁也就是在更新库存时带上库存条件UPDATE sku SET stock stock - #{count} WHERE id #{skuId} AND stock #{count}这条 SQL 能保证不会出现超卖——如果库存不足影响行数为 0业务层捕获后提示用户库存不足。如果要更进一步可以用 Redis 的原子操作来做库存预扣但毕设阶段用数据库乐观锁已经足够而且这段代码是很好的答辩素材“你是如何处理并发超卖的”答案就在这个 UPDATE 语句里。订单提交的完整业务逻辑我放在了submitOrder这个事务方法里下面这一段是核心缩影Transactional public OrderVO submitOrder(OrderSubmitDTO dto) { // 1. 遍历购物车明细逐个校验商品上下架状态、SKU是否存在 // 2. 按数量匹配阶梯价格计算每个SKU的小计 // 3. 预扣库存UPDATE sku SET stock stock - ? WHERE id ? AND stock ? // 4. 生成订单主表记录状态为待付款 // 5. 批量插入订单明细表保存SKU快照信息 // 6. 删除对应的购物车记录 // 7. 返回订单号、总金额给前端 }有一点要特别提醒订单明细表里除了 SKU ID一定要冗余保存商品名称、图片、单价、规格快照。为什么因为商家后面可能改商品名称、调价格甚至下架商品但用户的订单历史不能被影响。快照的本质是“订单生成那一刻的商品信息拷贝”这是订单系统设计的常识也是很多自学的同学容易漏掉的地方。2.3 购物车与结算批量下单的规则校验购物车在批发场景里和零售最大的不同就是批量校验逻辑。零售购物车基本是选中即买但批发购物车要处理起批量、阶梯价、跨店结算等问题。我在前端购物车列表里对每一行加了“起批量”标识如果商品要求最低 5 件起批而用户只加了 2 件提交订单时后端会直接拦截并返回提示。这个校验前端要做后端更要重复做一遍因为接口是可以被外部直接调用的不能只靠页面限制。阶梯价格的计算也在结算页实时反映。用户修改某个 SKU 的购买数量前端会重新请求后端的价格计算接口把最新的数量和价格回到界面上。这样用户能直观看到“买 10 件比买 5 件每件便宜 20 块”的激励效果很符合批发场景的心理预期。结算页还要包含收货地址的默认选中、买家留言备注、支付方式选择。支付这块毕设通常不会真接第三方支付渠道我的做法是模拟支付提交订单后生成一个待付款订单点击“去支付”按钮后模拟支付成功状态流转到待发货。答辩时主动说明“这里是模拟支付真实环境中接入微信/支付宝易支付接口即可”老师基本都能理解。2.4 商家端权限与数据隔离多商家入驻是批发平台区别于普通商城的重要设计。这里涉及两层权限登录鉴权和数据隔离。鉴权我用的是 JWT。用户登录成功后后端签发一个带用户ID和角色标识的 token前端存到 localStorage每次请求放在请求头Authorization里。后端用一个拦截器统一从 token 解析出当前用户信息然后写进ThreadLocal或直接放入请求上下文这样所有 Controller 都能方便地拿到“当前是谁在操作”。数据隔离则要更谨慎。商家登录后商品列表、订单列表都只能看到自己的数据。最粗暴的写法是每个查询手动加user_id ?条件但这容易漏一旦漏了就是严重的安全漏洞。我在项目里用了一个更工程化的方案在 MyBatis-Plus 里配置数据权限拦截器在 SQL 执行前自动拼上商家隔离条件。如果你们用的也是 MyBatis-Plus可以参考官方文档里的多租户插件把商家 ID 当作租户 ID 来隔离数据效果非常好而且在答辩时讲出来会很亮眼。管理员端的功能相对常规商家入驻审核、商品上架审核、会员管理、全平台订单查看、首页数据看板等。我在管理端的首页放了几张 ECharts 图表展示最近一周订单量、销售额排行、商品分类占比数据是从订单和商品表里汇总出来的视觉效果和专业度都很不错。3. 数据库设计与关键实现3.1 数据表设计与关联从ER到落地数据库设计是我认为这套项目里最值得反复打磨的部分。表设计得好后端的代码能少写一半设计得乱后面每个查询都要在 JOIN 里挣扎。我整理出来的核心表有 12 张这里挑最重要的说明一下表名用途关键字段user用户表id、username、password、role角色、phone、statusseller商家信息表id、user_id、shop_name、license、intro、statuscategory商品分类表id、parent_id支持多级、name、sortgoods_spu商品主表id、seller_id、category_id、title、main_image、detail、statusgoods_sku规格库存表id、spu_id、color、size、price、stock、salesprice_policy阶梯价格表id、sku_id、min_num、max_num、pricecart购物车表id、user_id、sku_id、quantity、checkedorder订单主表id、order_no、user_id、seller_id、total_amount、status、create_timeorder_item订单明细表id、order_id、sku_id、goods_title、sku_snapshot、price、quantityaddress收货地址表id、user_id、receiver、phone、province、city、detailbanner轮播图表id、image、link、sortoperation_log操作日志表id、user_id、action、detail、create_time你要注意几个设计细节金额字段我用的是decimal(10,2)而不是double避免浮点数导致的价格精度问题。所有表都有create_time和update_timeMyBatis-Plus 的自动填充功能可以统一处理不用在业务代码里每次手动 set。订单号order_no不是自增 ID而是程序中生成的唯一编号。我用的是时间戳加随机数的组合同时加唯一索引保证并发下不重复。外键关系在表设计里存在但在 MySQL 层面我没有强制建物理外键而是通过索引和应用层来维护引用关系。理由是性能更好而且毕设项目里逻辑外键已经足够清晰。3.2 核心接口与业务逻辑下单事务的完整写法一个电商系统的核心接口无非就那么几个登录、商品列表、商品详情、购物车、下单、订单列表。我这里挑出下单接口做一个完整展示因为这个接口最复杂也最能体现工程能力。下单入参用 DTO 接收包含收货地址 ID 和一组购物车明细 ID。后端拿到参数后在一个事务里完成前面说的几步操作。下面这段代码是简化后的骨架但关键逻辑都在Transactional(rollbackFor Exception.class) public SubmitOrderResult submitOrder(OrderSubmitDTO dto, Long userId) { ListCartItemVO items cartService.getCheckedItems(dto.getCartItemIds(), userId); if (items.isEmpty()) { throw new BizException(没有选中的商品); } Long sellerId null; BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItemVO item : items) { // 校验SKU状态和库存 GoodsSku sku skuMapper.selectById(item.getSkuId()); if (sku null || sku.getStatus() ! 1) { throw new BizException(商品不存在或已下架 item.getGoodsTitle()); } // 数量必须满足起批量 if (item.getQuantity() sku.getMinOrderQuantity()) { throw new BizException(item.getGoodsTitle() 未达到起批量); } // 预扣库存依赖 SQL 条件防止超卖 int affected skuMapper.reduceStock(item.getSkuId(), item.getQuantity()); if (affected 0) { throw new BizException(库存不足 item.getGoodsTitle()); } // 根据数量匹配阶梯价格 BigDecimal unitPrice pricePolicyService.matchPrice(item.getSkuId(), item.getQuantity()); BigDecimal subtotal unitPrice.multiply(BigDecimal.valueOf(item.getQuantity())); OrderItem orderItem new OrderItem(); orderItem.setSkuId(sku.getId()); orderItem.setGoodsTitle(item.getGoodsTitle()); orderItem.setSkuSnapshot(sku.getColor() / sku.getSize()); orderItem.setPrice(unitPrice); orderItem.setQuantity(item.getQuantity()); orderItems.add(orderItem); totalAmount totalAmount.add(subtotal); sellerId item.getSellerId(); // 同一家店结算简化场景 } // 创建订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSellerId(sellerId); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.WAIT_PAY.getCode()); orderMapper.insert(order); // 批量插入订单明细 for (OrderItem oi : orderItems) { oi.setOrderId(order.getId()); orderItemMapper.insert(oi); } // 清空购物车中已结算的项 cartService.removeCheckedItems(dto.getCartItemIds(), userId); return new SubmitOrderResult(order.getOrderNo(), totalAmount); }这段逻辑中最核心的是Transactional注解和reduceStock的乐观锁条件。前者保证如果后面任何一步失败前面预扣的库存会自动回滚后者保证高并发下不会超卖。两件事缺一不可我建议你在自己的项目里把这套思想吃透而不是把代码背下来。前端调用接口的封装也很重要。我在src/api/order.js里统一封装请求配合 Axios 拦截器自动附加 tokenimport request from /utils/request export function submitOrder(data) { return request({ url: /api/order/submit, method: post, data }) }拦截器里统一处理 401 跳转登录、错误消息提示这样业务页面只需要关心成功后的跳转逻辑代码干净不少。3.3 源码结构解读拿到一套毕设源码后怎么快速上手很多人拿到一套源码后第一反应是直接运行结果不断报错。我先说结论源码的运行永远不是第一步第一步应是“读配置”。先打开后端的application.yml把这三样核对清楚数据源配置数据库名、用户名、密码是否和本地环境一致Redis 配置地址、端口、密码是否有默认值本地 Redis 是否启动文件上传路径配置的目录在本机是否存在要预先创建。再看前端的.env文件或src/utils/request.js确认接口请求的基础地址是否指向后端端口const service axios.create({ baseURL: process.env.VUE_APP_BASE_API || http://localhost:8080, timeout: 10000 })然后执行导入 SQL 脚本按顺序启动 Redis、后端、前端。正常情况下登录接口能通商品列表能出来这条链路就算通了。源码里最有学习价值的部分我建议按这个顺序阅读先读entity看数据结构再读controller看接口清单然后挑一个完整业务链路读service比如下单、订单支付、商品审核。不要一上来就钻进某个 XML 文件里研究一条复杂 SQL那样容易迷路。4. 从源码到答辩我踩过的坑与复盘建议4.1 常见问题排查五个高频坑做这个项目时我前后帮几个同学排查过启动和运行问题最常遇到的五个坑汇总在下面的表里问题现象常见原因解决办法后端启动报数据库连接失败MySQL 未启动、账号密码不一致、库名不存在检查 application.yml确认数据库已导入且用户有权限前端页面打开但接口 404前后端路由前缀不一致或后端未启动确认后端启动成功检查 baseURL 和 Controller 映射地址接口报跨域错误前后端端口不同未配置跨域后端加CrossOrigin或全局跨域配置类图片上传后无法访问静态映射未配置或上传目录不存在添加资源映射创建对应目录检查上传路径绝对路径登录成功但后续请求 401token 没带、过期或后端校验失败检查 Axios 请求拦截器确认 token 存取的 key 和后端解析一致跨域是我第一次跑前后端分离项目卡最久的问题。记住一个原则跨域问题要在后端解决而不是在前端改请求头。一个简单的全局配置类就能搞定Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }我还有一个建议开发时尽量把后端接口日志打印得详细一些。用一个拦截器打印每个请求的 URL、参数、耗时排查问题的时候会顺手很多。这个能力在答辩演示时也会让老师觉得你的工程素养不错。4.2 答辩讲解思路用业务故事串流程答辩时最忌讳的就是照着代码从头讲到尾老师完全听不出重点。我的建议是用一条完整的业务故事线来做演示第一步以管理员身份登录展示商家入驻审核说明平台怎么控制入驻商家质量第二步切换商家账号发布一款男装商品设置阶梯价格和库存说明批发价的业务逻辑第三步切换采购商账号浏览商品、加入购物车、修改数量触发价格档位变化第四步提交订单模拟支付订单状态变为待发货第五步切回商家账号看到新订单执行发货订单状态变为已发货第六步采购商确认收货整套订单状态闭环完成最后回到管理端展示数据看板说明平台的全局运营视角。这套演示逻辑把三个角色串了起来每一步都对应一个业务功能而不是零散的功能点。老师如果中途问“这个状态是怎么变化的”你正好把订单状态机讲一遍如果问“怎么防止超卖”你把乐观锁和事务讲一遍。整个答辩的节奏就掌握在你自己手里。4.3 进一步扩功能低成本让项目加分的方向如果你时间还有富余我建议在这个基础上做三个低成本、高展示度的扩展第一个是数据看板。管理端首页用 ECharts 展示订单趋势、商品销量 Top10、分类占比。数据来源可以从订单表和订单明细表里聚合查询不需要引入额外的报表工具。视觉效果一出来整套系统的完成度会明显上一个档次。第二个是订单导出。给商家端增加一个“导出 Excel”按钮后端用 EasyExcel 把订单列表导出成文件。这个功能代码量不大但很贴实际业务需求答辩时讲“方便商家对账”非常合情合理。第三个是物流状态模拟。在订单发货后增加一个简易的物流轨迹字段比如“已出库”“运输中”“已签收”用一个定时任务按时间更新状态。如果你想顺便提一下消息队列也可以讲“后续可以用队列做异步状态通知”点到为止即可不用真的把 RabbitMQ 装进来。我始终认为毕业设计的核心不是“功能多”而是“逻辑通”。在有限时间内把一条业务主线做到极致胜过堆砌十个只做了一半的模块。这个泉州男装批发服务平台做完之后我自己复盘了很久。最大的收获不是 Spring Boot 的注解背熟了多少而是真正理解了一个订单从购物车到完成收货中间要经过多少次校验、多少次状态流转数据又是怎么在十几张表里关联起来的。如果你也是第一次做这种完整项目别怕按“业务主线优先、细节逐步补齐”的节奏推进踏踏实实跑通一条链路你已经赢了大多数人。源码和设计文档我都整理得比较规整目录结构、注释、SQL 脚本都齐了供参考学习用。加油。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑