
每年的毕业设计选题季Java 方向总会出现一批XX管理系统。我当初在换过好几个选题之后最终选定做基于 Spring Boot 的植物销售管理系统做完之后的体会是这个题比表面看起来要有营养得多。植物销售本质上是一个轻量级电商系统它涉及用户、商品、库存、购物车、订单、营销这些完整链路而 Spring Boot 又是 Java 后端最主流的脚手架二者结合非常适合用来练手也适合作为毕设或课设的项目。这篇文章就把我从需求分析到数据库设计、从接口实现到部署实测的完整过程梳理一遍。内容主要参考这类系统在毕设场景下的常见做法实际项目文档如果和我的设计有出入也可以对号入座地替换。另外提醒一句下面的篇幅会比较长因为我想尽量把每个关键选择背后的逻辑也讲清楚而不是只贴代码。1. 这个项目到底解决了什么问题植物销售的业务画像1.1 植物商品和普通商品的差异很多人在做管理系统时第一版数据库设计很容易照搬商品表 订单表的通用模板。但植物销售这个场景一旦落到实际业务中会发现植物商品有几个明显特性非标品。同样是绿萝可能有土培、水培、大盆、小盆之分多肉植物更是不同品种形态差异极大。所以要维护种类、规格、养护说明、适宜温湿度等属性。状态敏感。植物在售状态会随季节变化夏季很多品种不适合长途运输需要季节下架或调整库存。库存波动大。植物进货有损耗需要及时修正库存数量。售后特殊。物流过程中可能损坏用户需要养护指导和售后说明。这部分直接决定了数据库不能简简单单设置一个商品名 价格 库存就完事。我在设计时把植物信息拆成基础信息和扩展信息基础信息存通用的分类、价格、图片扩展信息存养护说明、光照需求、季节状态等。这样既保证列表页的加载效率又给详情页留足展示空间。1.2 从使用角色梳理功能清单一个常规的植物销售管理系统通常面向三类角色普通用户、管理员、系统本身。我在具体做功能清单时习惯先列角色再列每个角色需要做什么然后再转成功能模块。用户端主要功能包括注册登录、浏览商品与分类筛选、查看商品详情、加入购物车、下单、支付模拟、查看订单、签收确认、个人资料与签到积分。管理端主要功能包括商品管理增删改查、上下架、库存修改、分类管理、订单处理发货、查看详情、轮播图管理、公告管理、用户管理。这些功能模块别急着直接写代码先画一个简单的用例图或者列一个表格标清楚哪些是核心模块哪些是加分模块。我自己的划分是核心模块为商品、购物车、订单加分模块为签到积分、轮播图、公告。加分模块的作用是让系统看起来更完整答辩时也能多聊几句但它们的实现难度都不大属于性价比很高的功能点。1.3 项目的核心难点数据一致性功能清单看着不复杂但存在几个需要花时间想清楚的设计点库存扣减下单时扣还是支付时扣扣多了会超卖扣少了用户无法下单。订单状态迁移待付款、待发货、待收货、已完成、已取消状态之间如何流转。商品与库存关系一张商品表直接放库存字段还是单独拆一张库存表这些都是需要在设计文档阶段就回答的问题。我当时的方案是商品表只存基础信息库存单独建立一张表并且在下单时使用乐观锁扣减库存。后面会详细展开这里先记住一个结论库存扣减是整个系统的关键路径数据库层面的原子操作远比代码层面的 if 判断可靠。2. 技术选型与工程结构Spring Boot 这套组合怎么搭2.1 前后端分离的取舍既然是毕设级项目我建议优先考虑 Spring Boot 做后端接口Vue 3 Element Plus 做后台管理页面。前后端分离除了是当前企业级开发的主流形态还有两个实际好处一是在系统演示时可以先跑前端页面视觉效果好二是答辩时可以把跨域处理接口设计前端路由守卫这些分离开发才会遇到的问题讲出来证明项目是真实的工程而不只是 CRUD 演示。当然如果时间非常紧或者对前端不熟悉用 Thymeleaf 模板也是可行方案毕竟 Spring Boot 官方对 Thymeleaf 支持很完善。但凭我自己做完的经验Vue 的前期学习成本换来的是更顺滑的展示效果非常值得。特别是后台管理页面用 Element Plus 的表格、表单、弹窗组件能省下大量写 CSS 的时间。2.2 后端分层与包结构后端我采用的是一套非常经典的分层结构com.yourproject ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config ├── common │ ├── result │ └── exception └── utils每个包的责任边界要明确Controller 只做参数接收和返回Service 里写业务逻辑和事务控制Mapper 负责数据库访问。这里特别想提醒的一点是entity、dto、vo 三种对象千万别混用。entity 对应数据库字段dto 接收前端传来的参数vo 返回给前端展示。很多新手图省事直接把 entity 返回到前端结果某些敏感字段没被忽略掉比如密码字段。养成用 vo 的好习惯后面会少踩很多坑代码也更容易维护。2.3 版本选择一上来就在版本上翻车Spring Boot 的版本选择真的会影响体验。我第一次用 Spring Boot 3.2 JDK 21结果代码里写 javax.servlet 包名总是报红后来才知道 Spring Boot 3.x 已经把 javax 换成了 jakarta。如果你用 Spring Boot 3.x就不要再复制网上的 2.x 依赖很多包名的前缀都不一样。对毕设项目来说我的建议是本地 JDK 用 1.8 或 11Spring Boot 选 2.7.x这个组合最稳定相关教程也最多。如果电脑装的是新版 JDK比如 21也完全可以安装一个低版本 JDK 然后切换项目 SDK。IDEA 里 File - Project Structure - Project SDK 里改即可。如果遇到 Maven 编译版本不对还要检查 pom.xml 里的 maven.compiler.source 和 targetproperties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties还有一个搜索热词里经常出现的东西是 Spring Boot Banner。官方其实支持在 resources 目录下放一个 banner.txt启动时控制台会打印你自定义的图案。很多人会用它生成一个植物主题或者学校名字的启动图案演示的时候也算一个小小的记忆点。这个功能本质上只是 Spring Boot 启动阶段的一个展示机制不影响业务但能让项目看起来更亲历亲为。3. 数据库设计表结构、库存扣减与订单状态机3.1 核心表结构全景数据库是整个项目的底盘表结构设计得好后面实现会很顺利。我在这个项目里最后落定的核心表包括user用户表字段包括用户名、密码加密存储、昵称、手机号、头像、角色、注册时间category分类表父分类字段用于实现两级分类plant植物商品表存储名称、简介、价格、原价、主图、详情、养护说明、光照需求、是否上架inventory库存表plant_id 唯一关联存总库存和剩余库存cart购物车表用户 id、植物 id、数量order订单主表订单编号、用户 id、订单状态、总金额、收货人、手机、地址、创建时间、支付时间order_item订单明细表订单 id、植物 id、单价、数量、小计sign_in签到记录表用户 id、签到日期、连续天数、获得积分这里有一个细节值得单独提为什么订单要拆成 order 和 order_item 两张表因为一个好的订单系统需要让订单头和订单明细分离。头记录整体信息明细记录当时购买的商品快照。商品价格和商品名称必须是快照不能在读订单的时候再去关联实时的商品表——否则你后面改了商品价格用户的历史订单金额也会跟着变这是不合理的。3.2 库存扣减乐观锁解决超卖库存是植物销售系统最容易出错的地方。最原始的写法是SELECT stock FROM inventory WHERE plant_id ?; -- 如果 stock 0 则 UPDATE inventory SET stock stock - 1 WHERE plant_id ?;两条语句之间如果并发访问就会出现两个用户同时读到 stock1然后都去扣库存最后把库存扣成负数。解决超卖最常用的方式是在 UPDATE 语句上做条件判断UPDATE inventory SET stock stock - 1 WHERE plant_id ? AND stock 1;这条语句是原子操作受影响行数大于 0 才表示扣减成功。配合一张 version 字段可以做更完善的乐观锁实现。对毕设项目来说这种写法已经足够而且面试时被问到怎么解决超卖也能说出一套完整思路。3.3 订单状态机与超时未支付处理订单状态我固定为0-待付款、1-待发货、2-待收货、3-已完成、4-已取消。每个状态之间的流转要清晰待付款可以在超时后取消也可以由用户主动取消待发货是支付成功之后的状态管理员发货之后进入待收货用户确认收货进入已完成。超时未支付订单的处理在简单实现里我用的是定时任务每分钟扫描一次待付款订单如果创建时间超出 30 分钟且仍未支付就更新订单状态为已取消同时把冻结的库存释放回来。这里要强调一个原则订单取消后库存必须回补否则用户反复下单却不付款库存就被吞掉了。这个道理很朴素但往往容易被忽略。4. 核心代码实现从登录鉴权到签到积分4.1 JWT 登录鉴权与统一响应用户模块是整个系统的入口。我使用的是 JWTJSON Web Token方式生成登录凭证。登录成功后后端把用户 id 和角色写入 token前端每次请求在 header 里带上 token后端通过拦截器解析并放入 ThreadLocal 中方便后续取当前登录用户。拦截器注册用 Spring Boot 的 WebMvcConfigurer 实现只拦截需要登录的接口放行登录接口和商品浏览接口。这里有一个小技巧拦截器只能拿到 HandlerMethod 上声明的注解所以配合自定义注解 RequireLogin 和 RequireAdmin 会更灵活比如某几个管理接口只需要管理员访问直接在方法上加 RequireAdmin 即可不用每个接口都手写一遍角色判断。统一响应体我定义了一个 Result 类code 表示业务状态码message 表示提示信息data 表示数据。配合 RestControllerAdvice 写的全局异常处理器后端接口的错误信息能统一返回而不是到处 try-catch。全局异常处理还有个额外的好处数据库层抛出的异常不会直接暴露给前端而是统一包装成业务错误信息对演示体验和安全性都有帮助。4.2 商品模块图片上传与分类检索商品模块看起来简单实际要处理的问题包括分类查询、关键字搜索、上下架状态过滤、图片上传等。图片上传我采用本地磁盘存储上传接口用 MultipartFile 接收保存到配置好的目录再把虚拟路径存到数据库。如果部署到服务器前端的图片目录可以通过 Nginx 映射出去这样就不需要专门的对象存储服务系统架构保持简单。分类检索如果只有一级分类很好做但如果想要二级分类建议在 category 表设计时加一个 parent_id 字段查询时先用 parent_id 查出父分类再根据父分类 id 关联查商品或者直接在商品表里冗余一个分类路径字段加快查询速度。毕设项目用前者就够答辩时还能解释一句为什么用 parent_id 而不是固定层级体现出对数据模型的理解。4.3 订单完整链路事务、状态与库存联动创建订单的核心逻辑是校验购物车 - 校验库存 - 生成订单主表和明细 - 扣除库存 - 清空购物车 - 返回订单编号。这里面必须使用 Transactional 注解保证事务任何一个环节异常全部回滚。如果事务不回滚会出现订单创建成功但库存没扣或者库存扣了但订单失败的情况。这里有一个容易忽略的并发问题如果校验库存和扣减库存是两步操作即使事务保证一致性并发情况下也可能超卖。所以必须在 Service 里把库存扣减的 UPDATE SQL 作为整个操作的仲裁点也就是先尝试扣减扣减失败就终止创建订单。代码可以写成这样int rows inventoryMapper.deductStock(plantId, quantity); if (rows 0) { throw new BusinessException(库存不足); }这个 deductStock 就是上面那句带条件的 UPDATE。先扣再判断比先查再扣更安全。我在第一次实现时其实也写了先查询再判断的逻辑后来用两个浏览器同时点击下单才发现库存可以被扣成负数改成这种写法之后才真正解决问题。4.4 签到积分模块小功能也能讲出花签到功能不算核心模块却是答辩时的亮点。我的实现非常简单sign_in 表存三个字段user_id、sign_date、continuous_days。每次签到先判断当天是否已签如果已签返回今天已经签过啦未签则插入一条记录。连续天数根据昨天是否有记录来计算昨天有签连续天数 1昨天没签连续天数重置为 1。这里要注意日期格式的统一数据库存 date 类型判断时用 LocalDate.now()不要用带时分秒的 Timestamp否则当天已签的判断很容易出 bug。积分逻辑是最容易讲出层次的部分基础签到加 10 分连续签到满 7 天额外加 50 分。积分可以存放在 user 表的总分数字段里这样查询积分排行榜也方便。如果有余力把用户表当天的签到状态用 Redis 缓存一天避免每次请求都查一次数据库会显得更有工程意识。当然毕设阶段不做缓存也完全没毛病只要你能讲清楚为什么不做。5. 部署与实测记录从本地到可演示的状态5.1 环境与配置文件管理项目用 Maven 管理依赖application.yml 里区分 dev 和 prod 两套环境。开发者模式就用本地的 MySQL数据库连接池、Redis 连接等写在 dev 配置里部署时通过启动参数指定 prod 配置。这样一个简单的 profile 机制能让项目在开发和部署之间顺利切换。数据库初始化也不要全靠手敲直接在项目里放一个 sql 脚本包含建库、建表、插入演示数据。演示数据很重要尤其是植物商品图片空数据库和没有图片的页面演示效果会大打折扣。建议提前准备好 10 到 20 张植物图片用本地路径或图床引用。配置文件里还有一个容易忽略的点如果数据库是 MySQL 8.0要注意驱动类名和时区配置。比较稳妥的数据库连接配置是spring: datasource: url: jdbc:mysql://localhost:3306/plant_sales?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver如果按这个配置连接不上多半是驱动版本或时区设置的问题先检查 pom 里 MySQL 驱动的版本再检查数据库 URL 的时区参数。5.2 前端打包与常见部署方式前端项目执行 npm run build 后生成 dist 目录。部署时最省事的方案是把后端打成一个 jar 包直接运行前端 dist 里的静态文件交给 Nginx 或者直接丢到后端 resources/static 目录下。二者选一即可我最终用的是 Nginx 方式因为更接近真实项目的部署习惯。如果使用前后端分离部署nginx 配置是server { listen 80; location / { root /data/dist; index index.html; } location /api/ { proxy_pass http://localhost:8080/api/; } }这样前端的 API 请求通过 /api 前缀反向代理到后端不需要在前端代码里写死 IP 地址也顺便解决了部署环境的跨域问题。如果你在本地开发时已经配置过跨域但部署到 nginx 之后还是报跨域错误多半是前后端请求路径不匹配先检查前端发起请求时用的 baseURL 是 /api 还是 8080 地址。5.3 实测阶段踩过的几个典型问题CORS 跨域报错开发阶段前端端口是 5173后端是 8080必须配置跨域过滤器。我配置了 corsConfigurer并允许携带凭证否则每次请求都会因为 preflight 请求失败而看不到数据。LocalDateTime 的 JSON 序列化默认返回格式带有 T看着很丑。我在全局配置中统一加了 jackson 的日期格式化让接口返回 yyyy-MM-dd HH:mm:ss 这种常见格式。文件上传过大报错Spring Boot 默认上传大小限制是 1MB需要配置文件里调大同时 nginx 的 client_max_body_size 也要相应调整。MyBatis 的 mapper xml 路径扫描不到接口和 xml 路径要匹配或者在 application.yml 里指定 mapper-locations。这个问题非常隐蔽因为编译不报错只有运行时报 Invalid bound statement 错误。这些坑很小网上都能搜到但自己实测一遍之后再记住解决办法比死记硬背面试题有效得多。尤其 LocalDateTime 格式化和跨域问题几乎每个前后端分离项目都会遇到属于必须要会的技能点。6. 从毕设到面试这套系统能讲出的技术深度6.1 Spring Boot 自动装配必须能讲清楚面试官看到项目里有 Spring Boot大概率会问自动装配原理。最简单的回答路径是Spring Boot 启动类上的 SpringBootApplication 由 EnableAutoConfiguration 引入后者通过 spring.factories 或 AutoConfiguration.imports 加载大量自动配置类再配合 ConditionalOnClass、ConditionalOnProperty 等条件注解按需生效。拿项目举例引入 spring-boot-starter-web 之后Spring Boot 会自动配置内嵌的 Tomcat、DispatcherServlet、Jackson 序列化器。引入 spring-boot-starter-data-redis 后RedisAutoConfiguration 会自动装配 RedisTemplate。理解这条链路比背十道八股都有用。如果被问为什么 Spring Boot 能自动配置你甚至可以从 META-INF/spring.factories 文件开始讲说明自动装配其实不是魔法而是一套约定好的 SPI 机制。6.2 项目的扩展方向让系统继续生长做完基础版本之后如果还有时间可以考虑加几个横向扩展加入 Redis 做轮播图和商品列表缓存减轻数据库压力用 EventListener 事件机制在订单创建后解耦发送短信或邮件通知多数据源配置把用户统计数据和业务数据分离属于进阶玩法引入 RabbitMQ 或 RocketMQ 做订单超时延迟消息替代定时扫描用 Undertow 替换默认的 Tomcat 内嵌容器体验一把内嵌容器可插拔的特性。每个扩展背后都对应一个面试高频话题。比如用了 EventListener就能顺便聊聊解耦和事件驱动架构用了 Undertow就能聊聊为什么不同内嵌容器的性能和适用场景不一样。做完一个扩展简历上就多一个可以深入聊的点。6.3 我对这个项目的最终建议从选题到答辩我最大的感受是这类系统能不能做出区分度不在于功能数量而在于细节是否经得起追问。比如库存为什么用乐观锁、订单为什么分两张表、为什么用 vo 而不是 entity 返回给前端。这些细节平时看起来不起眼但被问到的时候能答上来就说明项目真的是你亲手做的。如果按照这套路线走下来最后你会有一个能正常演示的植物销售管理系统还能对 Spring Boot、MySQL、Vue 之间的关系建立起完整的认知。我至今仍认为这是性价比很高的一类毕设题目既有电商系统的完整链路又没有支付、推荐、物流这些非常复杂的域非常适合用来完成一次独立的全栈实践。