☰
Spring Boot水果销售系统毕设全攻略:从数据库设计到订单状态机
2026/10/18 12:27:50 网站建设 项目流程

刚带完一届毕业设计,好几个同学都选了电商类题目,其中"Spring Boot水果销售系统"这个题出现的频率相当高。说实话这类题目上手门槛不算高,但要做出区分度、顺利通过答辩,功夫全在细节上——订单状态怎么流转、库存扣减怎么保证不超卖、后台权限怎么做,这些才是评委真正会追问的地方。

这篇就按我平时带项目的完整思路来写,从功能拆解、技术选型、数据库设计,到核心模块实现、前后端联调,最后附上一批高频报错和答辩避坑清单。不管你是打算从零开始开发,还是已经买到一份源码准备二次开发,这篇文章都能帮你把整个系统彻底吃透。我踩过的坑、改过的 bug、答辩时被问倒的问题,全在这里面了。

1. 项目定位:一套水果销售系统到底要拆成哪些功能

1.1 前台商城的核心链路

先别急着写代码,拿到题目第一件事是画功能地图。对于基于Spring Boot的生鲜果品电商平台,前台的用户链路几乎是固定的:

用户注册登录 → 浏览首页轮播和分类 → 进入商品列表 → 搜索/筛选 → 点击查看商品详情 → 加入购物车 → 修改数量或删除 → 提交订单并选择收货地址 → 模拟支付 → 查看订单状态 → 确认收货。

这一条链路走完,系统的主要功能就覆盖了一大半。但"水果销售"和普通百货电商有一个明显差异:商品单位往往是"斤""份""箱",规格描述直接影响商品详情页的展示逻辑;另外生鲜商品有库存保鲜的压力,所以库存字段和上下架状态一定要单独设计,不能图省事写死。

1.2 后台管理的核心链路

后台是另一个半场。管理员登录后要能管理以下内容:

  • 商品管理:新增、编辑、上下架、调整库存和价格、上传图片;
  • 分类管理:一级分类(比如热带水果、进口水果、时令水果)的增删改;
  • 订单管理:查看全部订单、按状态筛选、发货操作(更新物流单号或标记已发货);
  • 用户管理:查看注册用户列表、启用/禁用账号;
  • 数据统计:销售总额、订单数量、top商品排行,用简单的图表展示即可。

很多初学者把精力全放在前台,后台只做了个"能登录、能查询"的壳子。我明确说一句:答辩时老师最爱看的就是后台,因为后台能直接体现你对业务流程的理解程度。订单管理里"发货"按钮点下去,订单状态从待发货变成待收货,这一下就能看出来你懂不懂状态机的概念。

1.3 选题价值与毕设加分点

做这个题目有一个天然优势:技术栈覆盖广。Spring Boot的核心特性(自动配置、依赖注入、事务管理)、持久层框架MyBatis-Plus、关系型数据库MySQL的表设计、缓存工具Redis的接入,再到前端页面渲染,全都能在同一个项目里体现。

如果你想拿高分,可以在基础功能之外再加一两个亮点模块。我见过做得好的同学加了积分签到、满减优惠券、销量排行、Excel批量导入商品、ECharts统计图表。加一个就够,不要贪多,保证能讲清楚原理比堆功能重要得多。这个思路也直接影响后面的数据库设计和代码结构,所以一开始就要想清楚。

2. 技术选型:为什么是Spring Boot这一套组合

2.1 技术栈清单与选型理由

毕业设计的技术选型,核心原则是"主流、稳妥、能讲清楚"。我这套组合是反复实践下来最稳的:

层级技术选型选型理由
后端框架Spring Boot 2.7.x稳定、生态成熟,3.x对JDK和部分依赖要求更高,不建议毕业设计阶段冒险
持久层MyBatis-Plus 3.5.x比原生MyBatis少写大量XML,内置分页插件,适合快速开发
数据库MySQL 8.0最常用关系型数据库,资料多、排错容易
缓存Redis 5.x存验证码、缓存热数据,让项目有亮点
安全方案JWT + 拦截器前后端分离场景下比Session更好讲,且无状态
前端Vue 3 + Element Plus组件化开发效率高,UI颜值在线;或者用Thymeleaf+Bootstrap(如果不想碰前后端分离)
构建工具Maven标准配置,课程里也常用

这套组合最大的好处是:每个技术点你都能在大学课程里找到对应章节,答辩时解释原理不心虚。比如Redis你只要说清楚"验证码设置了5分钟过期时间,用Redis的过期机制实现",老师就认。

2.2 开发环境准备与项目骨架

环境上建议统一用这些版本:JDK 1.8或11,IDEA社区版或旗舰版都行,MySQL 8.0配Navicat管理,Redis装Windows版或Linux版都可以。如果从来没装过Redis,一定要花半小时把它调通,因为很多同学卡在启动Redis这一步就直接放弃了。

项目骨架建议这样创建:直接用IDEA里的Spring Initializr生成,依赖勾选Spring Web、MySQL Driver、Lombok,然后手动加上MyBatis-Plus和JWT相关依赖。包结构保持简单清晰:

fruit-market/ ├── pom.xml └── src/main/java/com/example/fruit/ ├── FruitApplication.java 启动类 ├── config/ 配置类,包括拦截器、跨域、Redis配置 ├── controller/ 控制层,按模块分包 │ ├── admin/ 后台接口 │ └── user/ 前台接口 ├── service/ 业务层,接口+实现 ├── mapper/ MyBatis-Plus的Mapper接口 ├── entity/ 数据库实体类 ├── vo/ 给前端返回的视图对象 ├── common/ 统一返回结果、异常处理、常量定义 └── utils/ 工具类(JWT、MD5加密等)

这个结构看起来不起眼,但它是后面所有模块开发的地基。很多同学直接把所有类堆在controller里,后期改一个功能能改出三处bug,就是包结构混乱导致的。

2.3 前后端分离还是服务端渲染

这个问题我每次都会被问到。我的建议很直接:如果你对Vue有一定基础,用前后端分离,加分明显;如果你时间紧张、只想尽快跑通,用Thymeleaf或JSP服务端渲染。

前后端分离意味着你需要处理跨域、Token传递、异步请求这些额外问题,但这些都是毕业设计里的"安全加分题"——老师问起来你可以讲得头头是道。如果用Thymeleaf,开发速度确实快,页面直接在Controller里返回,但能展示的技术点就少了。

我个人推荐前后端分离,原因只有一个:它更接近真实企业开发形态。这篇后面的联调部分,也会按前后端分离的场景来讲。

3. 数据库设计:表结构决定业务复杂度上限

3.1 核心数据表一览

水果销售系统的核心表,经过我带过的所有项目验证,最少需要下面这些。不要嫌表多,"订单"和"订单明细"分开是电商设计的底线,因为一个订单里会有多个商品,这属于一对多关系。

表名用途关键字段
user注册用户id, username, password, nickname, phone, avatar, status, create_time
admin管理员id, username, password, role
category商品分类id, name, parent_id, sort
product商品id, name, category_id, price, stock, sales, image, unit, status, description
cart购物车id, user_id, product_id, quantity, checked
address收货地址id, user_id, receiver, phone, province, city, detail, is_default
orders订单主表id, order_no, user_id, address_id, total_price, status, pay_type, create_time, pay_time, ship_time
order_item订单明细id, order_id, product_id, product_name, product_image, price, quantity, subtotal

3.2 订单状态机:最容易翻车的设计点

订单状态是整个系统里最核心的设计之一。我第一次带人做的时候,有同学把订单状态存成字符串"已付款""已发货"直接写到前端下拉框里,这种做法做演示没问题,但一旦涉及数据统计和条件查询就非常痛苦。

正确的做法是用数字编码定义状态,并在代码里用常量或枚举表示:

0 待付款 1 待发货(已付款) 2 待收货(已发货) 3 已完成(确认收货) 4 已取消 5 退款中(可扩展)

状态流转只有固定几条路径:待付款→待发货(支付成功);待发货→待收货(商家发货);待收货→已完成(用户确认收货);待付款→已取消(用户主动取消或超时关闭)。你要在代码里控制住这个流转方向,不能让订单状态随意跳变。

数据库里建议直接加一行注释:status tinyint DEFAULT 0 COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消'。这个细节老师在查看SQL脚本的时候会注意到,属于印象分。

3.3 建表时的几个细节

建表这块我吃过不少亏,给你几个可复用的经验:

  • 金额一律用decimal(10,2),绝对不要用double。浮点数在金额计算上会有精度问题,答辩时你要是说"我用double存钱",被追问的概率极高;
  • 所有表都加create_time和update_time字段,配合MyBatis-Plus的自动填充功能,代码里就不用手动维护时间了;
  • 用户名、商品名这类字符串字段,统一用utf8mb4字符集,不然用户输入一个emoji表情直接报错;
  • 不要用外键约束。逻辑上保留关联关系就行,物理外键在后期删除数据时非常碍事,这也是企业开发的常见做法。

另外,把建表脚本单独放在项目根目录的sql/文件夹下,提前准备一些演示数据,特别是热门水果商品要配上清晰图片。演示数据充足与否直接决定答辩演示效果,这一步别看轻了。

4. 后端核心模块实现:从登录到下单的完整闭环

4.1 用户登录与拦截器设计

登录模块我推荐用JWT方案。思路是这样的:用户提交用户名密码,后端校验通过后生成一个包含用户ID的Token返回给前端;前端把Token存在浏览器里,每次请求接口时放在请求头的Authorization字段;后端写一个拦截器,拦截需要登录的接口,从请求头取出Token并解析出用户信息。

核心代码大致长这样:

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token)) { // 解析token,把用户信息放入ThreadLocal Long userId = JwtUtil.parseToken(token); UserContext.set(userId); return true; } // 未登录,返回401 response.setStatus(401); return false; } }

配置拦截器时只拦截需要登录的路径,比如/api/user/**和/api/admin/**,而/api/auth/**、前台商品查询接口、首页数据接口要全部放行,否则用户还没登录连商品都看不到。

这里有个实操细节:拦截器里解析完用户ID后,用ThreadLocal保存用户信息,请求结束必须记得调用UserContext.remove()清除,否则线程池复用时会串数据。这个点非常值得在项目里体现,属于真正踩过坑的人才会注意的细节。

4.2 商品分类与查询实现

商品模块是典型的CRUD,但查询要多花点心思。前台商品列表要支持三个维度的筛选:按分类查、按关键字搜索、按价格或销量排序。这些用MyBatis-Plus的分页插件很容易实现:

public Page<ProductVO> listProducts(Integer categoryId, String keyword, Integer pageNum, Integer pageSize) { LambdaQueryWrapper<Product> wrapper = Wrappers.<Product>lambdaQuery() .eq(Product::getStatus, 1) // 只查上架商品 .eq(categoryId != null, Product::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Product::getName, keyword) .orderByDesc(Product::getSales); return productMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }

注意这两点:一是前台永远只查询上架状态为1的商品和库存大于0的商品,这是业务边界;二是后端返回给前端的对象不要直接拿entity,要转成VO。为什么不直接返回实体?因为商品详情可能需要附带分类名称、销量排名等额外信息,直接在实体上加字段会造成结构混乱,所以单独建一个ProductVO放这些扩展字段。

4.3 购物车逻辑与订单生成流程

购物车我建议设计成独立的cart表,而不是存在Redis或Session里。道理很简单:用表存方便跨设备同步,而且能展示你对表关联设计的能力。

购物车加购逻辑要注意一个点:同一用户添加同一商品,不要新插入一条记录,而是把已有记录的数量加一。用数据库的方式就是先查再决定插入还是更新,或者用唯一索引(user_id, product_id)配合ON DUPLICATE KEY UPDATE。

订单生成是整个系统最考验事务能力的地方,因为要同时做好几件事:生成订单主表记录、批量生成订单明细、扣减库存、清空购物车。这四件事必须在一个事务里完成,任何一步失败都要全部回滚,否则就会出现"订单生成了但库存没扣"这类脏数据。核心写法:

@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateRequest request) { // 1. 从购物车查询选中商品 // 2. 计算总金额 // 3. 保存订单主表 orders,状态为0待付款 // 4. 批量保存 order_item 明细 // 5. 扣减库存:UPDATE product SET stock = stock - #{num}, sales = sales + #{num} WHERE id = #{pid} AND stock >= #{num} // 6. 清空已下单的购物车项 return orderId; }

第5步那个WHERE stock >= #{num}条件很多人不理解,这是防超卖的关键。我见过有人先查询库存,再判断够不够,然后更新库存,中间隔了几行代码,并发高一点就会超卖。虽然毕设并发量不高,但条件更新这个写法能在答辩时体现出你对并发安全的理解。

4.4 模拟支付与库存扣减的处理顺序

支付模块是另一个容易糊弄的地方。接入支付宝沙箱确实真实感强,但需要审核配置,流程稍有繁琐。更务实的做法是做"模拟支付":订单创建后状态是待付款,前端展示一个支付按钮,点击后调用后端/pay接口,后端把订单状态改为待发货,记录支付时间。

支付和库存扣减的顺序问题值得想清楚:是下单时扣库存,还是支付时扣库存?生鲜电商的现实做法是支付后才真正扣减库存,因为用户下单不付款会占住库存,影响其他顾客购买。但在毕设简化场景下,大多数实现是下单即扣库存,然后超时未支付取消订单时再返还库存。

不管你选哪种,逻辑要自洽。我个人推荐"下单扣库存、取消返库存"这种方案,因为状态流转完整,好讲清楚。取消订单需要回补库存,别漏了这个动作。很多同学做的系统取消订单后库存数字就少了,演示时被老师抓到就很尴尬。

5. 前端页面与前后端联调实操

5.1 页面结构与模板选择

如果你用Vue 3 + Element Plus,页面结构建议这样规划:

  • 前台移动端优先,因为水果商城主要面向手机用户。用Vue Router做路由跳转,页面包括:首页、商品列表页、商品详情页、购物车页、订单确认页、订单列表页、个人中心、登录注册页;
  • 后台单独一套布局,侧边栏放菜单,顶部是管理员信息,内容区放各管理页面:商品管理、分类管理、订单管理、用户管理、数据统计。

页面数量控制在10个以内,太多做不完,太少显得单薄。如果只用Thymeleaf,页面就是templates下的HTML模板,商品列表用th:each循环渲染,逻辑更直接,但体验上不如Vue丝滑。

做前端最省时间的办法是找一个后台模板先搭框架,前台页面自己写。不要把时间花在写CSS上,毕设演示看重的是完整流程通畅,而不是视觉细节多么精致。

5.2 接口联调的3个注意点

前后端联调是很多人的噩梦,其实坑来坑去就那几个:

第一,跨域问题。Vue开发服务器默认在5173端口,后端在8080端口,浏览器直接拒绝跨域请求。后端加一个全局CORS配置,允许指定前端来源,同时要允许携带请求头Authorization。不带这个配置,前端会一直报CORS错误,而且这个报错信息不太直观,容易排查半天。

第二,统一返回结构。后端所有接口返回统一的Result<T>格式,包含code、message、data三个字段。前端拿到后先判断code是否为200,再取data。这个约定可以写在一个文档里,前后端各自遵守,联调速度能提升一大截。

第三,日期格式化。Java返回的LocalDateTime默认是一个ISO字符串,前端解析没问题,但直接显示给用户很丑。后端全局配置Jackson,把日期统一格式化成yyyy-MM-dd HH:mm:ss。订单列表页的时间显示,这个小细节很影响成品观感。

6. 常见问题与毕设答辩避坑清单

6.1 高频报错映射表

这套系统做了无数遍之后,我整理了一份高频报错对照,基本能覆盖90%的情况:

现象大概率原因解决方向
启动报数据库连接失败MySQL没启动,或url里的时区参数不对在jdbc url后面加serverTimezone=Asia/Shanghai
启动后所有页面404访问了不存在路径,或遗漏了Controller的@RestController注解先确认接口路径和前端请求路径完全一致
前端报401Token未携带或已过期检查拦截器放行规则,确认前端请求头带了Authorization
MyBatis-Plus查询返回null实体字段与表字段命名不一致开启map-underscore-to-camel-case驼峰映射配置
中文乱码数据库字符集不是utf8mb4,或IDEA控制台编码问题建库时指定字符集,IDEA设置File Encodings为UTF-8
Redis连接失败Redis服务没启动,或密码配置错误启动Redis,检查spring.redis.host和password
下单后库存没变化SQL条件写错,或事务没生效检查更新SQL的WHERE条件,确认@Transactional是否被同级方法调用

有一个很多同学会忽略的问题:方法内部自己调用自己时,@Transactional会失效,因为Spring事务是通过代理对象生效的,内部调用不会走代理。如果订单创建方法里包含了事务注解,但控制层调的是另一个没加事务的方法,而这个方法内部又直接调用了带事务的同类方法,事务就不起作用了。

6.2 答辩前必查的5个点

最后这五分钟的检查,是我每次让学生答辩前必做的一套动作:

  1. 用一份干净的数据库重新初始化数据,确保sql脚本能一键执行成功,演示机器上不会出现缺表缺数据的问题;
  2. 把完整的购买流程从头到尾走三遍:注册、登录、加购、下单、支付、发货、确认收货,中途不报错;
  3. 检查管理员的权限:普通用户访问后台接口应该被拒绝,这是安全设计的基本要求;
  4. 准备好项目讲解逻辑:先讲业务需求,再讲表设计,然后演示核心接口,有时间再讲亮点(Redis、JWT、事务);
  5. 把application.yml里的数据库密码改成你自己机器上的实际密码,避免临时换环境后启动失败。

最后说一个我个人的小经验:毕业设计评审看的不只是功能是否齐全,更看重你对项目的理解深度。与其做十个自己都讲不清楚的功能,不如把一条主链路打磨到位,把每一处设计决策的原因想明白。我见过太多同学答辩时被老师问"为什么订单状态用数字不用字符串""为什么库存更新要加stock >= num条件"就卡壳,这些问题背后的原理,我在前面都逐一拆解过了。做系统的过程其实就是在不断回答"为什么"——这个项目做完,你能把每一步都说明白,这才是真正的收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询