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、status |
| seller | 商家信息表 | id、user_id、shop_name、license、intro、status |
| category | 商品分类表 | id、parent_id(支持多级)、name、sort |
| goods_spu | 商品主表 | id、seller_id、category_id、title、main_image、detail、status |
| goods_sku | 规格库存表 | id、spu_id、color、size、price、stock、sales |
| price_policy | 阶梯价格表 | id、sku_id、min_num、max_num、price |
| cart | 购物车表 | id、user_id、sku_id、quantity、checked |
| order | 订单主表 | id、order_no、user_id、seller_id、total_amount、status、create_time |
| order_item | 订单明细表 | id、order_id、sku_id、goods_title、sku_snapshot、price、quantity |
| address | 收货地址表 | id、user_id、receiver、phone、province、city、detail |
| banner | 轮播图表 | id、image、link、sort |
| operation_log | 操作日志表 | id、user_id、action、detail、create_time |
你要注意几个设计细节:
- 金额字段我用的是
decimal(10,2),而不是double,避免浮点数导致的价格精度问题。 - 所有表都有
create_time和update_time,MyBatis-Plus 的自动填充功能可以统一处理,不用在业务代码里每次手动 set。 - 订单号
order_no不是自增 ID,而是程序中生成的唯一编号。我用的是时间戳加随机数的组合,同时加唯一索引,保证并发下不重复。 - 外键关系在表设计里存在,但在 MySQL 层面我没有强制建物理外键,而是通过索引和应用层来维护引用关系。理由是性能更好,而且毕设项目里逻辑外键已经足够清晰。
3.2 核心接口与业务逻辑:下单事务的完整写法
一个电商系统的核心接口无非就那么几个:登录、商品列表、商品详情、购物车、下单、订单列表。我这里挑出下单接口做一个完整展示,因为这个接口最复杂,也最能体现工程能力。
下单入参用 DTO 接收,包含收货地址 ID 和一组购物车明细 ID。后端拿到参数后,在一个事务里完成前面说的几步操作。下面这段代码是简化后的骨架,但关键逻辑都在:
@Transactional(rollbackFor = Exception.class) public SubmitOrderResult submitOrder(OrderSubmitDTO dto, Long userId) { List<CartItemVO> items = cartService.getCheckedItems(dto.getCartItemIds(), userId); if (items.isEmpty()) { throw new BizException("没有选中的商品"); } Long sellerId = null; BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> 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 拦截器自动附加 token:
import 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或全局跨域配置类 |
| 图片上传后无法访问 | 静态映射未配置,或上传目录不存在 | 添加资源映射,创建对应目录,检查上传路径绝对路径 |
| 登录成功但后续请求 401 | token 没带、过期或后端校验失败 | 检查 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 脚本都齐了,供参考学习用。加油。