每到毕业季,总有人在毕设选题上反复横跳:写管理系统吧怕显得太水,写算法吧又担心做不完。如果你手里正握着“基于 SpringBoot 的酒水销售系统”这个题目,或者在几个电商类毕设里犹豫,我建议你认真看完这篇。酒水销售系统本质上是一个垂直品类的电商平台:用户能浏览商品、加购、下单、模拟支付,管理员能维护商品、处理订单、看统计报表。用 Java 生态里的 SpringBoot 来做,工程量介于“学生管理系统”和“大型电商”之间,既有完整业务闭环,又不会做到中途崩溃,是投入产出比很高的毕设方向。这篇博文适合正在选题的同学,也适合工作几年想快速搭一套小电商练手的开发者。下面我会把整个项目的设计思路、数据库表、核心代码、环境搭建和踩坑经验一次说清楚。
1. 项目整体设计:先看思路,再看代码
1.1 为什么酒水销售系统是“高性价比”的毕设选题
选毕设题目,本质上是选“业务模型的复杂度”。太简单的题目,比如图书管理、学生信息管理,功能就一张表的 CRUD,做完了没东西可讲,答辩老师问几句就冷场;太复杂的题目,比如秒杀系统、完整电商中台,涉及高并发、分布式事务、消息队列,一个人短期内根本写不完。
酒水销售系统正好卡在中间。它首先是“销售系统”,必须包含商品、购物车、订单、支付、发货、收货这一整条电商链路;它又是“酒水”这个垂直品类,要有分类、品牌、规格(容量、酒精度)、多级分类、库存批次等细节。如果标题里再强调“销售与订单一体化”,那基本就是“前台商城 + 后台管理”双层结构:
- 前台用户端:注册登录、浏览商品、分类筛选、商品详情、加购、下单、支付、查看订单。
- 后台管理端:用户管理、分类管理、商品管理、库存管理、订单管理、销售统计。
两个端共用同一套后端服务和数据库,但权限角色不同,这本身就是一个很好的展示点。答辩时你可以说:这不是一个单页面管理系统,而是一个多角色的业务闭环系统。这句话一出来,整个项目的层次就不一样了。
另外,酒水类商品天然适合做“规格差异化”,比如同一款酒有 500ml 和 750ml、有单瓶和整箱装,这会迫使你在商品表和购物车表里考虑规格和单位问题,比普通的图书管理系统要有说服力得多。“为什么这么设计”在答辩时就是你最有话说的部分。
1.2 技术选型:版本钉死,才能少踩坑
技术选型这件事,我见过太多同学栽跟头。视频教程里用的是 SpringBoot 2.x,官网初始化出来的却是 3.x;代码里写 javax.servlet,新版本要改成 jakarta.servlet;Lombok 突然不生效;Maven 下载依赖卡半天。这些问题 90% 都出在“版本矩阵”没钉死。
毕设项目不需要追求最新,只需要“确定能跑通”的组合。我推荐下面这套:
| 层次 | 推荐选型 | 选型理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.18 | 兼容 JDK 1.8,教程最多,生态最稳 |
| ORM | MyBatis-Plus 3.5.5 | 单表 CRUD 零 SQL,分页/逻辑删除内置,企业用得也多 |
| 数据库 | MySQL 5.7 / 8.0 | 免费成熟,网上案例丰富,Navicat 直接连 |
| 认证方案 | JWT + 拦截器 | 比 Spring Security 轻,容易讲清楚无状态认证原理 |
| 前端 | Vue 2 + Element UI | Element UI 的后台组件非常全,很快能出界面 |
| 工具库 | Lombok、Hutool、Maven | Lombok 简化代码,Hutool 提供雪花ID、时间工具等现成方法 |
为什么不用 Spring Security?不是它不好,而是对毕设来说太重了。Spring Security 的过滤器链、UserDetailsService、密码加密器等整套配置,学习成本很高,调试起来也费时间。毕设里用 JWT + 拦截器实现登录认证,代码你完全能控制,答辩时还能把“token 生成、校验、无状态接口”讲得明明白白。这已经能拿到不错的分。
数据库选 MySQL 5.7 还是 8.0?如果本机之前装过哪个就用哪个,两者差别不大。但要注意驱动:Spring Boot 2.7 默认配 com.mysql.cj.jdbc.Driver,连接 URL 里一定要加 serverTimezone=Asia/Shanghai,否则查时间字段会报时区错误。
1.3 分层架构与功能模块划分
后端代码结构尽量用标准的三层架构:Controller 接收请求、校验参数,Service 写业务逻辑,Mapper 访问数据库。这是最不容易出错的结构,也是答辩老师最认可的结构。不要搞那种所有代码全堆在 Controller 里的写法,虽然功能能跑,但一问你“分层怎么设计的”就露馅了。
具体模块我建议这样切:
- 用户模块:注册、登录、获取用户信息、修改资料,用 JWT 管理登录态。
- 商品模块:商品列表、分类列表、商品详情、关键词搜索、销量排序。
- 购物车模块:加购、修改数量、勾选、删除、批量结算。
- 订单模块:下单、订单列表、订单详情、取消订单、确认收货。
- 支付模块:模拟支付或沙箱支付、支付回调处理。
- 后台管理模块:用户管理、商品上架/下架、库存调整、订单发货。
- 统计模块:按天统计销售额、热销商品 Top10。
每一个模块都对应独立包名,比如 controller、service、mapper、entity、vo、common。还要有一个统一返回体 Result<T>,统一返回 { code, msg, data },前端的 Axios 拦截器直接根据 code 判断业务成功或失败。这套东西看起来简单,但代表了“工程化意识”,在答辩时是明显的加分项。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:表结构建好,系统就成功了一半
2.1 核心表与字段设计
很多同学拿到题目就开建表,用户表、商品表、订单表,三张表搞定,写到后面发现订单明细没法存、库存变动记录不了、地址没地方放,只能翻工改表,改到崩溃。酒水销售系统建议至少设计以下 8 张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户与管理员账号 | id, username, password, phone, role, status, create_time |
| category | 商品分类,支持多级 | id, parent_id, name, sort, status |
| product | 酒水商品 | id, category_id, name, brand, capacity, alcohol, price, stock, sales, image, status |
| cart | 购物车 | id, user_id, product_id, quantity, checked, create_time |
| address | 收货地址 | id, user_id, receiver_name, receiver_phone, province, city, district, detail, is_default |
| orders | 订单主表 | id, order_no, user_id, total_amount, pay_amount, status, address_id, create_time, pay_time, deliver_time, finish_time |
| order_item | 订单明细表 | id, order_id, product_id, product_name, product_image, price, quantity, total_price |
| stock_log | 库存变动日志 | id, product_id, change_type, change_num, before_stock, after_stock, create_time |
user 表里要加 role 字段,区分普通用户和管理员。password 字段建议长度至少 60,因为要用 BCrypt 加密,加密后的字符串很长,用 varchar(50) 后面存不下。status 字段表示账号是否可用,被管理员禁用后无法登录,这是后台管理里常被问到的一个点。
product 表里的 capacity 和 alcohol 是酒水商品特有的属性,一个是容量,一个是酒精度。把它们作为独立字段存下来,商品列表就可以做“按酒精度筛选”这种需求,属于垂直品类特色。
2.2 商品表和订单表为什么必须拆主表/明细表
订单表拆成 orders 和 order_item 两张表,是你必须理解的设计,也是答辩老师大概率会问的题。原因是:一个订单会包含多件商品,如果所有商品都塞在一条记录里,要么用逗号拼接商品ID,要么加一堆冗余列,查询和统计都会很难受。
拆成两张表后,orders 表存一笔订单的公共信息,比如订单号、总金额、支付状态、收货地址;order_item 表存这笔订单里的每一件商品,比如商品ID、下单时的商品名、单价、数量、小计。一个订单对应多条明细,这就是典型的一对多关系。
这里有个重要细节:order_item 表里的 product_name 和 price 是“快照字段”,不是冗余设计。因为下单之后,商品可能改名、调价甚至被下架删除,但订单明细必须保留用户下单那一刻的商品名称和价格。这个点讲出来,评委老师会觉得你考虑到了真实业务,而不是只会照抄表结构。
库存变动日志 stock_log 也是容易被忽略的表。它用来记录每次库存变化的原因,比如“手动调整”“用户下单扣减”“取消订单回补”。有了这张表,后台可以查“某商品为什么库存变了”,排查问题非常方便。
2.3 字段类型、索引与关联设计的坑
建表时的几个细节,直接影响后续开发和答辩质量。
金额字段必须用 DECIMAL(10, 2),不要用 float 或 double。浮点数在计算金额时会有精度损失,比如 0.1 + 0.2 可能等于 0.30000000000000004,用户看着都难受。Java 端对应使用 BigDecimal 做计算,这是标准做法。
数量字段用 INT,库存字段也要用整数。库存扣减时不要用程序先查再减,而是要在一条 SQL 里做条件更新,这个后面章节会详细讲。
索引怎么建?orders.order_no 建唯一索引,因为订单号绝对不能重复;orders.user_id 建普通索引,因为用户查询自己订单很频繁;order_item.order_id 建普通索引,因为要从订单查明细。如果商品支持按分类筛选,product.category_id 也要建索引。
我不建议用物理外键,也就是数据库层面的 FOREIGN KEY。原因有两点:第一,毕设项目里你会手动给分类排序、给商品做逻辑删除,物理外键的级联删除会很碍事;第二,实际开发中很多团队确实禁用物理外键,靠应用层保证数据一致性,用逻辑外键(只是普通字段 + 索引)更方便。答辩时可以主动说:“我没有使用外键约束,而是通过应用层事务保证一致性,这样后续扩展更灵活。”效果很好。
所有表都建议加两个公共字段:create_time 和 deleted。create_time 用 datetime DEFAULT CURRENT_TIMESTAMP,插入时不用管;deleted 是逻辑删除标记,通过 MyBatis-Plus 的 @TableLogic 自动拼上 WHERE deleted = 0,这样删除操作都变成 update,数据不会真正消失。
3. 订单核心流程与关键代码实现
3.1 下单主流程拆解
酒水销售系统的核心链路是:用户从购物车勾选商品 -> 点击结算 -> 后端校验商品和库存 -> 创建订单 -> 扣减库存 -> 清空购物车 -> 跳转支付。这个流程看着简单,但每一步都有讲究。
首先是商品价格校验。商品价格不能直接信任前端传过来的数字,前端传的只是商品 ID 和数量,后端必须根据商品 ID 重新从数据库查出最新价格和库存,再计算总金额。否则有人改一下前端参数就能把价格改成 0 元下单,这是很明显的安全漏洞。
订单状态先设置为待支付,然后扣减库存。注意顺序:先扣库存,再插订单主表,再插订单明细。如果中间任何一步失败,整个方法要回滚,库存不能扣了但订单没建成,否则就是“幽灵扣库存”。
创建订单时还要生成一个唯一订单号。我这里直接用 Hutool 的雪花 ID 生成器,一行代码搞定,格式好看且不会重复:
java复制String orderNo = IdUtil.getSnowflakeNextIdStr();
整个下单方法要用事务包起来,类上或方法上加上 @Transactional(rollbackFor = Exception.class)。注意一定要写 rollbackFor = Exception.class,因为 Spring 默认只在抛出 RuntimeException 时才回滚,如果代码里抛的是受检异常,事务不会回滚,这个坑很多人踩过。
3.2 库存扣减的并发处理
说到扣库存,就绕不开“超卖”问题。所谓超卖,就是两个人同时买最后一件商品,系统却把库存变成了负数。错误的写法是:
java复制Product product = productMapper.selectById(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productMapper.updateById(product);
}
问题很明显:两个请求同时查出来库存为 1,都判断足够,然后都执行减 1,库存变成 -1。这不是代码逻辑笨,而是并发条件下查和改不是原子的。
正确做法是用一条 SQL 带条件扣减,把“判断库存足够”和“修改库存”放在一条语句里完成:
java复制@Update("update product set stock = stock - #{num}, sales = sales + #{num} where id = #{productId} and stock >= #{num}")
int deductStock(@Param("productId") Long productId, @Param("num") Integer num);
调用的时候看返回的行数,返回 1 表示扣减成功,返回 0 表示库存不足或者商品不存在,直接抛业务异常即可。因为数据库的行锁保证了同一时刻只有一个事务能更新这条商品记录,所以不会超卖。
这段代码要配合事务使用。扣库存、插订单主表、插订单明细都在同一个事务里,任何一步失败都能整体回滚。答辩时老师问“怎么防止超卖”,你直接把这条 SQL 拿出来,讲清楚“条件更新”和“行锁”这两个关键词,这个回答是能在评委那里拿分的。
3.3 支付回调与订单状态机
支付是另一个容易出问题的环节。毕设如果不想申请支付宝沙箱,可以先做一个“模拟支付”按钮,点一下直接调用后端的 paySuccess 接口。但如果时间允许,我建议试一下支付宝沙箱,配置网关、应用 ID、密钥后,用沙箱账号就能走真实的支付回调链路,演示效果完全不输真实支付。
支付回调的核心逻辑是:根据订单号更新订单状态。这里要处理“重复回调”的问题,比如支付平台因为网络问题回调了两次,你的接口不能把同一笔订单从待支付改成已支付再改成已支付,这样会重复处理。解决方案是使用条件更新:
java复制public void handlePaySuccess(String orderNo) {
int rows = orderMapper.updateStatusByOrderNo(orderNo, OrderStatus.PAID, OrderStatus.UNPAID);
if (rows == 1) {
// 更新成功,才执行后续逻辑:记录支付时间、发送通知等
}
}
SQL 里带上“当前状态必须是待支付”这个条件,这样第二次回调来了也只会更新 0 行,不会重复处理。这就叫幂等,虽然不是最复杂的方案,但在毕设里足够用,而且很好讲。
订单状态建议用常量类或枚举管理,不要散落一堆魔法数字。状态流转要像下面这样保持单向:
code复制待支付 -> 已支付 -> 已发货 -> 已收货/已完成
待支付 -> 已取消
已支付 -> 已退款(售后场景,可选)
不要出现“已收货”又变回“已支付”这种状态倒流。所有状态更新都带上“当前状态”作为条件,这是最稳的做法。
3.4 超时未支付订单与定时任务
如果用户下单后一直不支付,订单就会一直躺在“待支付”状态,库存也会被一直占用。所以需要一个定时任务,把超过 30 分钟的待支付订单自动取消,同时把库存回补回去。
SpringBoot 自带的 @Scheduled 就能实现,不需要引入 Quartz。启动类上加 @EnableScheduling,然后写一个任务类:
java复制@Component
public class OrderTimeoutTask {
@Scheduled(cron = "0 */5 * * * ?")
public void cancelTimeoutOrders() {
LocalDateTime deadline = LocalDateTime.now().minusMinutes(30);
List<Order> orders = orderMapper.selectList(new LambdaQueryWrapper<Order>()
.eq(Order::getStatus, OrderStatus.UNPAID)
.lt(Order::getCreateTime, deadline));
for (Order order : orders) {
orderService.cancelOrder(order.getId());
}
}
}
cron 表达式表示每 5 分钟扫描一次。取消订单的方法内部也要用事务,既要更新订单状态,也要把库存回补回去,还要写一条库存变动日志。这里要做的判断是:回补库存时不能简单地把数量加回去,而是要考虑在这 30 分钟内商品可能被重新上架、价格变动等场景,不过毕设里只要重新计算库存并记日志就够了。
4. 从零搭建项目:环境选择与关键配置
4.1 JDK 和 SpringBoot 版本怎么选
这里要专门说“版本过高”的问题
