做毕设选题那会儿,我翻遍了各类“Java 毕业设计论文题目参考”,发现十个里七个是图书馆管理系统、在线商城、学生选课系统,剩下三个是“XX管理系统”的变体。不是说这些题目不行,而是答辩时评委看多了,提问自然会往刁钻方向走。最后我选了“基于 Spring Boot 的宠物咖啡馆平台的设计与实现”,一方面是因为宠物咖啡馆这个业态近几年在线下确实火,另一方面是它的业务复杂度刚好够用——不再是单纯的增删改查,而是把宠物档案、饮品商品、座位预约、领养审核这些非标场景揉在了一起,可以顺理成章地用到缓存、状态机、并发控制、权限隔离这些技术点。
这篇文章我会完整复盘整个项目的设计思路、技术选型、数据库建模、核心代码实现,以及我在实际开发中踩过的坑。如果你是正在做同类毕设的本科生,或者打算把“宠物主题门店系统”作为练习项目的开发者,这篇文章应该能帮你少走不少弯路。整体内容我会按我的实战顺序来写,从选题定调到上线答辩,全程都是可落地的干货。
1. 选题定调:宠物咖啡馆平台的真实业务边界在哪里
1.1 宠物和餐饮的复合业态决定了系统形态
宠物咖啡馆跟普通咖啡馆最大的区别是:它的“SKU”里不仅有咖啡、甜品,还有活体宠物和相关服务。走进一家正经的宠物咖啡馆,消费者的动线大概是这样的:门口先看今日在店宠物列表,挑一只合眼缘的预约互动时段;进店后点一杯咖啡、买一包宠物零食;互动过程中如果对某只猫或狗特别有感情,可以现场提交领养申请;门店员工除了做咖啡,还要记录每只宠物的喂食、清洁、健康状态;店长则要盯着库存、订单、预约情况和领养审核。
从这个动线能提炼出系统的几个核心角色:
- 普通用户(C端):注册登录、浏览宠物、点单、预约座位、提交领养申请、查看订单
- 门店员工:处理订单、核销预约、维护宠物日常档案、补充商品库存
- 管理员:宠物档案管理、商品管理、订单总览、领养审核、用户管理、数据统计
这样划分后,系统就不是单薄的一层“商城”了,而是“宠物档案 + 预约 + 零售 + 审核”四条业务线并行。这个形态恰好能支撑起一个完整的 Web 应用该有的模块设计,也方便在答辩时讲清楚“每个角色能做什么、数据是怎么流转的”。
1.2 毕设的展示策略:先想清楚评委想看什么
做毕设和做商业项目有个显著区别:商业项目追求快速上线,毕设追求的是“思路完整 + 技术点可证明”。我自己把评委最常关注的几个问题提前列了出来:
- 系统解决了什么实际问题?有哪些角色,每个角色完成什么闭环?
- 数据库设计是否合理?表之间的关联和状态流转是否有逻辑?
- 核心模块有没有技术含量?比如并发控制、权限校验、缓存设计。
- 项目是不是你自己写的?现在很多评委喜欢问代码细节,比如某张表的字段为什么这么定。
所以我在设计的时候就刻意增加了几个“可讲点”:宠物领养的状态机约束、预约座位的防并发方案、订单模块的乐观锁扣库存、基于 JWT + Redis 的登录态管理。这些点未必有多高深,但足够在答辩时把一个普通 CRUD 项目讲出“系统设计感”。
2. 技术选型的取舍逻辑:为什么这个组合最稳妥
2.1 Spring Boot 版本与持久层框架的搭配
技术选型我纠结过一段时间,核心是版本搭配问题。最初想直接用 Spring Boot 3.x,毕竟它已经是很成熟的版本了,但后来发现很多教学资料和已有开源项目还停留在 Boot 2.x 时代,加上部分第三方 starter 的兼容性在 Boot 3 下有变化,最后我选了Spring Boot 2.7.x + JDK 8/11的组合。理由很现实:毕设时间有限,遇到兼容性问题排查成本太高,Boot 2.7 是 2.x 系列的最终维护版本,稳定性和资料丰富度都最理想。
持久层我选的是 MyBatis Plus 3.5.x,而不是原生 MyBatis 或 Spring Data JPA。MyBatis Plus 在毕设场景下的优势非常突出:内置LambdaQueryWrapper和LambdaUpdateWrapper,写条件查询不用拼字符串;提供代码生成器,从数据库表一键生成实体、Mapper、Service、Controller 的骨架代码。这意味着你能把更多时间留给核心业务逻辑,而不是浪费在重复的 CRUD 代码上。
需要注意的是,用 MyBatis Plus 不等于抛弃 SQL 能力。比如订单统计、报表查询这类复杂聚合,我会在 Mapper 里写自定义 XML SQL,因为QueryWrapper处理 group by 的代码可读性实在一般,不如原生的SELECT DATE(create_time), COUNT(*) ... GROUP BY直观。
2.2 单体应用还是微服务:多端对接时的接口边界
技术选型时还有一个高频追问:既然宠物咖啡馆以后可能有小程序、App、门店收银端,那接口要不要拆成微服务?我的答案是明确不做微服务,整个系统保持“一个 Spring Boot 工程 + 前端分离”的单体架构。
这里要解释一下单体与微服务的边界。毕设项目规模在几千行代码量级,数据表大概十几张,微服务的拆分只会带来几个问题:服务间调用需要引入 Feign、注册中心要搭 Nacos 或 Eureka、分布式事务几乎无法规避、部署环境变复杂。而 Spring Boot 的单体应用,启动就是一个进程,所有模块共享同一个数据源,事务控制由 Spring 统一管理,开发体验和调试效率都高得多。
至于“对外提供接口给第三方时应该放在哪里”——我当时的做法是在单体内部按业务域拆包,比如controller/user/、controller/admin/、controller/open/。把面向第三方或未来端侧的接口单独放在open包下,统一加上/open/api前缀,走独立的鉴权逻辑(比如 Access Key 签名),这样即使以后要拆服务,这部分接口也能整体搬走。这个思路可以在答辩时展开聊,比“我用了微服务”更能体现架构意识。
2.3 鉴权、缓存、接口文档与监控的配套选型
配套工具这块我列一个清单,是我实际使用的方案:
| 功能 | 选型 | 理由 |
|---|---|---|
| 登录鉴权 | JWT + Spring Boot Starter Redis | 无状态、多端共享登录态、可手动登出 |
| 缓存 | Redis | 首页热点数据、token 黑名单、预约时段计数 |
| 接口文档 | Knife4j(Swagger 增强版) | 自动生成接口页面,演示时方便快速查看 |
| 监控 | Spring Boot Admin | 可视化查看服务状态、内存和日志级别调整 |
| 工具库 | Hutool | 日期转换、Bean 拷贝、生成随机文件名等常用能力 |
| 数据库连接 | Druid 连接池 | 官方监控页面可看 SQL 执行情况 |
为什么缓存我坚持用 Redis 而不是 ConcurrentHashMap?虽然本地 Map 也能缓存,但要处理过期时间、并发淘汰逻辑,而且重启就丢。Redis 做缓存是面试和答辩加分项,它还顺便解决了 JWT 登出时的 token 失效问题——登录后把 token 存进 Redis 并设置过期时间,登出时删掉即可。
3. 数据库设计:订单、宠物档案与状态机的核心逻辑
3.1 核心表结构与字段设计
数据库设计是整个项目最花时间、也最值得好好写的地方。我把核心表列出来,配套说明每张表的用途:
| 表名 | 用途 | 关键字段 |
|---|---|---|
user | 用户表 | id、phone、password、nickname、avatar、role(USER/EMPLOYEE/ADMIN)、status |
pet | 宠物档案表 | id、name、species(猫/狗等)、breed、age、gender、description、status、cover_image |
pet_health_record | 宠物健康记录表 | id、pet_id、record_date、temperature、vaccine_date、physical_condition、remark |
product | 商品表(咖啡/宠物零食/周边) | id、name、category、price、stock、image、status |
cart_item | 购物车表 | id、user_id、product_id、quantity |
orders | 订单表 | id、order_no、user_id、total_amount、status、pay_time |
order_item | 订单明细表 | id、order_id、product_id、product_name、product_image、price、quantity |
reservation | 座位/宠物互动预约表 | id、user_id、pet_id、reserve_date、start_time、end_time、status |
adoption_apply | 领养申请表 | id、user_id、pet_id、reason、experience、status、review_remark |
announcement | 公告表 | id、title、content、create_time |
这里有一个细节值得注意:订单表为什么要加order_no唯一索引?因为订单号在对外展示、支付回调、线下核销时都要用,不能让同一个订单号出现两次。我生成的规则是yyyyMMddHHmmss + 用户ID后四位 + 4位随机数,虽然极端情况下可能重复,但加了唯一索引兜底后,插入冲突时重试一次即可,保证百分百不重复。
3.2 宠物状态流转:用状态机约束业务边界
宠物状态是整个系统里最有“逻辑味道”的部分,因为领养、下架、暂停互动这些操作不是随意发生的。我定义了彩票级的业务状态集合,说实话后期在整个业务里起的效果比预想的大:
宠物的status字段我设计为:
0:在店休养中(新到店或身体不适,暂不开放互动)1:互动中(正常开放用户预约和展览)2:领养申请中(已有用户提交申请,宠物暂时锁定)3:已领养(宠物已离开咖啡店)
状态流转规则我放在了 Service 层统一封装,不允许 Controller 里直接改pet.status。比如用户提交领养申请时,会先执行一个update pet set status = 2 where id = ? and status = 1,如果影响行数为 0,说明宠物已被锁定或不在互动状态,直接提示“该宠物暂不可申请领养”。管理员审核通过后,状态才变更为 3,同时把申请人的 ID 回写到宠物的adopted_by字段,记录归属。
这个设计在答辩时可以强调:你并没有把所有业务规则散落在各个接口里,而是收敛到状态机层,避免出现“订单都支付了,商品库存还是扣少了”这类状态错乱问题。
3.3 订单流程的字段设计与金额处理
订单模块我借鉴了常规电商的简化流程,状态字段:
0待支付1已支付/制作中2已完成(已取餐)3已取消4退款
金额字段必须用BigDecimal,千万不要用double或float。道理很简单:0.1 + 0.2在浮点运算里并不精确等于0.3,涉及钱哪怕差一分都是bug。我在数据库里用的是DECIMAL(10,2),Java 实体里用BigDecimal,计算时用setScale(2, RoundingMode.HALF_UP)做四舍五入。购物车结算也全部走事务:先校验库存,再扣减,然后生成订单和明细,最后清空购物车。
订单创建这个操作必须加@Transactional,因为涉及多张表的写入,任何一步失败都要整体回滚,否则会出现“订单明细有了,订单总表还是空的”这种脏数据。我在开发初期就掉进过这个坑,当时少写了一个try-catch回滚,直接导致测试库出现了孤儿订单。
4. 核心功能实现:从用户端到管理端的闭环流程
4.1 登录鉴权与角色权限控制
登录模块我用的是 JWT + Redis 双保险方案。流程是:用户提交手机号和密码,后端校验成功后生成 token(把用户 ID 和角色放进载荷),同时把 token 存进 Redis,设置过期时间为 24 小时。之后所有请求携带Authorization: Bearer <token>,由拦截器统一解析。
这里我加了两个实用的细节:
第一是角色权限注解。自定义一个@RequireRole注解,支持ADMIN、EMPLOYEE、USER三种角色,拦截器中读取 token 里的角色做匹配,不匹配就返回 403。比如领养审核接口就标了@RequireRole("ADMIN"),普通用户即使能猜到接口地址也调用不了。
第二是登出逻辑。前端登出时除了删除本地 token,还要请求后端把 token 从 Redis 删除,这样可以让 token 立即失效。如果没有这个步骤,用户在有效期内的 token 都是可用的,那 JWT 的“无状态”就变成了安全隐患。
代码骨架大概是这样的:
public boolean logout(String token) { // token 形如 "Bearer xxxxx" String realToken = token.replace("Bearer ", ""); String key = "login:token:" + realToken; // 直接删除,后续访问拦截器会读取不到 key 而拒绝 return redisTemplate.delete(key); }4.2 点单、预约与管理端接口的设计逻辑
用户端下单流程是标准操作,我简化成五个步骤:加入购物车 → 确认结算 → 创建订单(状态为待支付)→ 模拟支付→ 扣减库存并更新状态。因为是毕设,不会真的接微信支付或支付宝,我用了“模拟支付”方式:页面调后端/pay/mock接口,后端把订单状态从 0 更新为 1,同时执行库存扣减。如果有精力,也可以接支付宝沙箱环境,能增加的亮点是:支付回调 + 验签逻辑。但实测沙箱文档对新手不算友好,完全可以用模拟支付代替,不影响整体评分。
预约模块的核心是防止同一个座位或同一只宠物在同一时间段被重复预约。最简单的方案是对reservation表加“唯一索引 + 插入前检查”,但更稳妥的是在创建预约的方法上加@Transactional,先用select ... for update锁定目标宠物记录,确认当前状态可预约及时间不重叠后再插入。我在实现时用的是 MyBatis Plus 加自定义 SQL:
SELECT * FROM reservation WHERE pet_id = #{petId} AND reserve_date = #{date} AND status IN (1, 2) FOR UPDATE锁定后如果查到冲突预约,直接抛出业务异常回滚。这样即使两个用户同时提交,同一只宠物也不会被重复预约。
管理端这一侧,我按“宠物管理、商品管理、订单管理、用户管理、领养审核、数据统计”六个菜单做的。数据统计页用了 ECharts 展示近 7 日订单量折线图和宠物互动次数排行榜,数据来源是 Mapper 里的聚合查询:
SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM orders WHERE create_time >= #{startTime} GROUP BY DATE(create_time) ORDER BY day DESC这类统计报表不需要提前建什么数仓,MySQL 的聚合函数完全够用,但这个功能在演示时非常出效果。
4.3 领养申请审核的流程实现
领养审核是宠物咖啡馆系统区别于普通商城的特色功能。用户提交领养申请时,填写申请理由、养宠经验、居住环境等信息,后端创建adoption_apply记录,状态为 0(待审核),同时把宠物状态从 1 改为 2(锁定)。
管理员看到待审核列表后,可以选择通过或拒绝:
- 通过:更新申请表状态为 1(已通过),宠物状态改为 3(已领养),写入
adopted_by = 申请人ID。 - 拒绝:更新申请表状态为 2(已拒绝),填入拒绝原因,宠物状态回滚为 1(恢复可互动、可被其他人申请)。
这个流程最关键是状态回滚。我一开始只做了“通过”的逻辑,忘记写“拒绝后恢复宠物状态”,结果测试时发现被拒绝的宠物从此消失在互动列表里,折腾了半天才定位到是状态没回滚。这也是为什么我后来反复强调状态机要集中管理——否则每一个网络分支都可能漏改状态。
5. 开发中真正踩过的坑
5.1 并发修改订单与宠物状态的脏读问题
做毕设最容易忽略的就是并发,因为本地开发只有一个用户在测试,永远不会触发并发场景。但答辩时评委可能会问:“两个用户同时领养同一只宠物怎么办?”我当时没有直接答“我加了锁”,而是在代码里真的加了乐观锁方案:
@Version private Integer version;MyBatis Plus 的乐观锁插件会在更新时自动拼接WHERE version = ?,并把version + 1写入更新语句,如果影响行数为 0,说明数据已被别人改过,需要重试或提示失败。在宠物领养审核中,我会先查出宠物当前版本号,再执行状态变更更新,避免两个管理员同时审核通过同一个申请,导致宠物被两个人“领养”。
另外下单扣库存时,我也用了条件更新,保证库存不会变成负数:
boolean success = productService.update( new LambdaUpdateWrapper<Product>() .eq(Product::getId, productId) .gt(Product::getStock, quantity) .setSql("stock = stock - " + quantity) );这种写法把“校验库存大于零”和“扣减库存”合并成一条原子 SQL,不加任何锁也能防止超卖。
5.2 图片上传与静态资源映射的坑
图片上传是几乎所有 Web 项目都会踩的坑,宠物咖啡馆系统里更是有大量宠物照片、商品图片、用户头像需要处理。我最初把图片直接保存到项目 resources 目录下,后来发现两个问题:第一,通过java -jar运行时路径和开发时路径不一样,上传后图片秒变404;第二,资源目录下的文件在打包后是只读的,重启后可能丢失。
我的最终方案是:
- 上传时把图片保存到服务器本地的一个独立目录,比如
/data/pet-cafe/upload/,文件名用 UUID 重命名,避免中文和特殊字符问题 - 通过自定义配置把该目录映射为虚拟路径
/upload/**,这样访问/upload/pet/123.jpg就能直接读取磁盘文件 - 数据库里只存相对路径
/upload/pet/123.jpg,不存完整域名,方便以后迁移到对象存储
另外还要注意 Nginx 或 Spring Boot 默认上传大小限制。spring.servlet.multipart.max-file-size默认是 1MB,宠物照片动不动就好几 MB,必须手动调大,不然前端图片上传会莫名失败,还很难定位。
5.3 前后端联调时的跨域与时间格式问题
我这几年做项目学到的最重要的一件事是:后端接口写好了,和前端联调时才是真正的“开工”。宠物咖啡馆平台我采用的是前后端分离,前端 Vue 开发时跑在 5173 端口,后端在 8080 端口,直接请求必然跨域。
解决方式我用了两种:
- 后端启用统一跨域配置:表示允许来自前端开发服务器的请求,并允许携带内容类型与认证信息
- 生产部署时用 Nginx 反向代理,把
/api请求转发到后端的 8080 端口,避免跨域
我还遇到过一个典型问题:数据库存的是datetime,返回给前端变成了“2025-06-01T12:00:00”这种形式,时区还是 UTC。后端在实体类的日期字段上加了@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),同时数据库连接串里加了serverTimezone=Asia/Shanghai,问题才真正解决。这个问题如果不提前处理,前端展示时间会整整差 8 个小时,用户看到的“预约记录到店时间”永远不对。
6. 部署上线与答辩准备的实践经验
6.1 从本地打包到服务器部署的注意点
毕设评审一般只看演示效果,但如果你能把系统真正部署到云服务器上,交互体验和稳定性会上升一个档次。我采用的部署方式是前后端同机部署:
- 后端:
mvn clean package -DskipTests打出一个可执行 jar,放到服务器/opt/pet-cafe/目录下,用nohup java -jar pet-cafe.jar --spring.profiles.active=prod &启动 - 前端:Vue 项目执行
npm run build生成dist目录,把dist里的静态文件整体放到/usr/share/nginx/html/,Nginx 配置好/api反向代理,这样用户访问 80 端口看到页面、后端挂在/api,全程只有一个域名和一个入口 - 数据库:服务器上装 MySQL 8.x,把本地的建表 SQL 导入,特别注意账号权限,否则启动时报
Access denied for user很难一眼看出
这里要提醒的是,application-prod.yml里的数据库密码、Redis 密码不要用明文写死写在代码仓库里,虽然毕设无所谓,但是如果能用环境变量替代,在答辩时会显得更专业:${DB_PASSWORD}这种占位符由服务器环境变量注入。
6.2 演示数据的准备与答辩问题的应对
演示环节我踩过一次比较大的亏:第一次模拟答辩时,我发现宠物列表里一只猫都点不开,因为测试数据是在本地数据库随便瞎填的,图片路径全是乱写的,演示效果极其尴尬。后来我专门做了一套“演示数据专属包”,把所有宠物、商品都配了真实可访问的图片 URL,订单和预约数据也按过去 7 天分布造了一批,保证折线图和排行榜有分析价值。
另外我会提前整理一个“答辩问题预热清单”,把评委最可能追问的技术点都准备好:
- 为什么用 Spring Boot 不用 SSM?——生态、自动配置、内嵌容器,最重要的是社区资料丰富,遇到问题能快速找到解决方案
- MyBatis Plus 和 MyBatis 有什么区别?—— MP 本质是对 MyBatis 的增强,没有侵入性,内置通用 CRUD 方法,但复杂 SQL 还是走自定义 XML
- 如果用户量和数据量增长,架构怎么优化?—— 数据库读写分离、Redis 缓存热点数据、静态资源走对象存储,必要时按业务域拆分微服务
- 登录为什么选 JWT? —— 无状态适合前后端分离,配合 Redis 可以主动失效,同时支持多端共享会话
我还额外为项目配置了 Spring Boot Admin 监控,把服务状态、JVM 内存、日志级别切换都可视化出来。虽然本地开发时用处不大,但在答辩现场打开这个监控页面时,完全可以把项目从“增删改查”拉到“工程化产品”的高度。
这个项目从最开始的需求确认到最终部署上线,前后大概花了一个半月时间。回头复盘,最庆幸的是没有一上来就疯狂写代码,而是先花了几天把表结构和状态流梳理清楚。宠物咖啡馆这个题材刚好踩在了传统餐饮系统和情感互动类业务中间,既有常规的订单库存逻辑,又有领养审核这种偏运营向的功能,做起来不枯燥,答辩时也很有讲头。
如果你也准备做类似的题目,最后再给你一个小建议:不要只满足于功能跑通,在代码里留下几个“你认真设计过”的痕迹,比如状态机约束、条件更新防超卖、统一异常处理、参数校验注解。这些细节在答辩时的价值,远比你多写两个 CRUD 接口要大得多。