宠物咖啡馆这几年开得到处都是但真去体验过运营节奏的人都知道吧台排队点单、撸猫时段预约、宠物当天的接待状态全靠店员脑子硬记人一多就乱套。我最近完整跑通并整理了一套宠物咖啡馆平台技术栈是很多人点过名的 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0前后端分离后端提供 REST 接口前端接管页面交互和状态数据库统一落 MySQL8.0项目还附带文档。这东西覆盖面比你想象中广宠物档案管理、预约时段、在线点单结算、后台数据维护一条业务链都有。如果你正在做 Java Web 方向的毕业设计、课程设计或想找一份能跑通完整闭环的前后端分离项目当学习样本这套系统可以作为参考。我不打算把文章写成一板一眼的功能说明书只聊三件事这套系统的业务到底是怎么搭起来的技术栈为什么这样选以及你拿到源码后从部署到扩展最容易被卡住的几个点在哪里。1. 宠物咖啡馆平台先解决什么问题业务功能与运营闭环1.1 用户端围绕“看、约、点、查”四个动作宠物咖啡馆不是普通餐饮店它的业务里有两类完全不同性质的“商品”一类是饮品甜点有固定库存和价格买单就行另一类是时间也就是和宠物互动的时段一个时段能容纳多少客人是有限的约满了就得换时间段。所以用户端最核心的四个动作一定是看、约、点、查。看浏览宠物信息包括品种、年龄、性格、疫苗接种状态。这一步等于前台把“今天哪些小动物可以见客”摆到线上。约选择日期和时段预约撸猫/撸狗互动名额。这里要做时段剩余名额判断约满不可选。点浏览咖啡馆菜单选饮品和小食下单结算时可以选择与某次预约关联也可以纯点单外带。查查看自己的订单列表、预约状态、取消记录。这部分对线下门店意义很大店员不用再花时间翻微信聊天记录。1.2 管理端档案、时段、订单、公告管理端面向咖啡馆运营人员说穿了就四块宠物档案、预约时段、订单、公告。宠物档案不只是增删改查更关键的是“接待状态”。一只宠物今天身体不适或刚接种疫苗需要静养管理端应该能快速把它设为“暂不接待”用户端立刻变成下架状态。预约时段管理负责设置每天哪些时间段可约、每个时段总名额多少。订单管理需要支持按状态筛选比如未支付、已支付、已完成、已取消必要时还能对超时未支付的订单做手动关闭。公告管理就比较常规了发布营业时间调整、新品上架提醒即可。这块系统在真实场景里最大的价值是减少人工决策负担不用靠店长早上一个一个问饲养员今天谁状态好。1.3 业务闭环从线上预约到到店体验如果把业务流程串起来大致是这样用户注册登录后浏览宠物列表发现某只猫今天状态正常于是选明天下午的时段完成预约。到店后扫码点单系统自动显示该用户当天的预约信息。后台收到预约后确认接待名额确认订单用户离店前完成核销。整个闭环涉及用户表、宠物表、预约时段表、预约记录表、菜单表、订单表和订单明细表每张表的状态流转是这套系统设计时最值得花时间琢磨的部分。2. 技术选型为什么这样搭SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0 各自的价值2.1 SpringBoot2.7.x稳定起步还是升级到3.x先回答一个很多人纠结的问题为什么选 SpringBoot2而不是 SpringBoot3SpringBoot3 强制要求 JDK17而国内多数高校课程设计和大量企业的存量 Java 项目还在用 JDK8 或 JDK11。对初学者和做毕设的人来说JDK8 SpringBoot2.7.x 是最不容易出环境问题的组合你按教程搜到的问题解决方案也最多。SpringBoot2.7 是 2.x 里最后一个支持 JDK8 的大版本2.7.18社区资料丰富生态稳定不会因为升级踩到一堆莫名其妙的新坑。这也带来一个现实选择如果你以后想走技术岗位建议在这套项目跑通后再抽时间看一遍 SpringBoot3 的迁移改动点比如 jakarta 包名变化、自动配置调整。但作为课程设计和毕业设计SpringBoot2.7 完全够用而且不丢人。2.2 MyBatis-Plus不只是少写 SQLMyBatis-Plus 在 Java Web 项目里的核心价值可以概括成三点单表 CRUD 零 SQL、Lambda 条件构造器、分页插件。单表 CRUD 零 SQL 意味着用户表和宠物表的大部分增删改查不需要你手写 SQLBaseMapper 里的 insert、selectPage 等方法直接能用。Lambda 条件构造器写起来非常接近自然语言比如查某个时段已预约数量条件就是预约表里时段ID等于某值且状态未取消。分页插件配置一次后全部查询都能走 page 方法。更实在的是MyBatis-Plus 对毕业设计文档期的“代码查重”本身没太大影响因为真正需要自己设计的是业务表和状态流转框架只是缩短了重复代码的量。2.3 MySQL8.0字符集和事务是基础选 MySQL8.0 而不是 5.7核心原因是 8.0 默认字符集是 utf8mb4可以直接存 emoji 和生僻字宠物名字里万一有特殊字符也不担心。另一个点体现在升级后的连接驱动上SpringBoot2.7 使用 mysql-connector-java 8.x 版本与 MySQL8.0 的 caching_sha2_password 认证方式配合更顺畅。在具体使用上有几个细节很容易踩坑MySQL8 对时间时区要求更严格连接 URL 必须带上 serverTimezone 参数不然启动可能直接报时间差错连接认证方式也需要注意用户创建时的插件选择。这些细节我在后面部署部分会单独列出。2.4 Vue3Element Plus前端界面的统一出口Vue3 相对 Vue2 最大的变化是组合式 APIComposition API配合script setup语法业务逻辑组织起来比 Options API 清晰很多。这套平台的前端部分用了 Vue3 Vite Element Plus Pinia Vue Router这在当前前后端分离项目里是一套非常主流的前端组合。Element Plus 的表格、表单、弹窗组件能直接把管理端页面拼出来后台管理的“增删改查”页面基本不用怎么写 CSS。用户端页面则偏自定义一些比如宠物卡片墙、时段选择器需要按业务场景做组件拆分。这一层把页面组件、路由、状态管理和接口请求分开页面代码不会写到后面乱成一团。技术在项目里的角色核心优势SpringBoot2.7REST接口、业务层、事务生态稳定JDK8友好MyBatis-PlusORM、分页、条件查询CRUD零SQL代码量少MySQL8.0数据持久化utf8mb4、事务支持好Vue3 Vite前端页面与构建组合式API清晰启动快Element Plus管理端UI开箱即用组件丰富Pinia前端状态管理比Vuex更简洁类型友好3. 数据库设计表关系、关键字段与两类订单状态机的取舍3.1 核心表清单与关系说明这套系统的表结构我按业务域拆成六组这里列最核心的八张表表名用途主要关联sys_user用户账号角色区分普通用户和管理员pet_category宠物分类关联 petpet宠物档案关联分类、预约reservation_slot预约时段关联预约reservation预约记录关联用户、时段、宠物menu菜单/菜品关联订单明细orders订单主表关联用户、订单明细order_item订单明细关联订单、菜品关系上一个用户可以有多个预约和订单一个预约只能预约一个时段一个宠物可以出现在多个预约里一个订单包含多个明细明细里记录菜品快照。预约和订单是两条独立的主链路中间通过用户ID和预约时间做弱关联。为什么故意不把预约做成订单的一种类型因为预约涉及时段名额并发控制点单涉及金额计算和事务一致性两者混在一个表里会让状态字段逻辑很拧巴。3.2 关键字段设计里值得关注的几个细节几处设计细节直接决定项目好不好写第一订单明细表必须“冗余快照”。下单时把menu_name和price原样存一份到 order_item而不是下单后动态去查菜品表。否则菜单改价后历史订单金额全变对账直接乱掉。这一点做项目时最容易漏。第二逻辑删除字段统一处理。所有业务表都加deleted字段配合 MyBatis-Plus 的TableLogic注解这样用户删除预约、管理员下架宠物等操作都走逻辑删除数据不真正消失后面如果要统计历史数据还有机会恢复。第三密码字段不要明文。用户表密码用 BCrypt 加密存储前端提交登录请求后后端校验。毕设项目里这一步看起来很基础但答辩时非常加分。第四时间字段统一用 datetime存北京时间。MySQL8 的 timestamp 有2038年问题和时区转换坑datetime 更直白。所有表都带 create_time 和 update_time可以用 MyBatis-Plus 的自动填充机制。3.3 预约与订单的状态机设计预约记录的状态建议设计为四个待确认、已确认、已完成、已取消。用户提交预约后是待确认后台通过后变已确认到店完成消耗后变已完成用户主动取消或后台超时关闭变已取消。状态流转要禁止任意跳跃比如已取消的预约不能再变回已确认。订单状态则区分得更细待支付、已支付、制作中、已完成、已取消。这里注意一个现实问题用户下单时先创建订单状态为待支付支付回调或模拟支付成功后状态改已支付。毕设如果没接真实支付可以在前端做一个“模拟支付”按钮后端提供支付成功回调接口状态一样能走完整套流转。状态字段用Integer加常量类不要直接散落在业务代码里写魔法数字。这套状态机设计的好处是预约和订单一旦分家各自的筛选、统计、看板逻辑都会清晰很多。4. 后端核心链路实现JWT登录、预约抢名额与下单事务4.1 后端项目结构和依赖初始化后端按标准的 controller/service/mapper/entity/config 分层。Controller 只负责接收参数和返回结果Service 放业务规则Mapper 是 MyBatis-Plus 的 BaseMapper 子接口Entity 对应表字段Config 统一放 JWT 拦截器、MyBatis-Plus 分页插件、跨域配置。pom.xml 里几个核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependencyJWT 相关依赖建议用 0.11.5这个版本的 API 相对稳定很多教程也基于它出问题容易搜到答案。4.2 JWT登录鉴权从生成Token到拦截请求登录接口逻辑不复杂根据用户名查用户 - 比对 BCrypt 密码 - 生成 Token 返回前端。之后前端每次请求带上 Token后端用一个拦截器统一校验。核心代码大概长这样RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } String token JwtUtil.generateToken(user.getId(), user.getRole()); return Result.ok().put(token, token).put(user, user); } }JwtUtil 负责生成和解析 Token。拦截器要做的事是从请求头取出 Authorization解析 Token 得到用户ID放入 ThreadLocal 或请求上下文供后续 Service 使用。注意几个容易忽略的点拦截器要放行登录、注册和静态资源路径不然前端还没登录就被拦了Token 过期后的提示要统一前端收到 401 状态码统一跳登录页。这些细节决定了演示时顺不顺。4.3 预约时段先查数量再插入以及并发兜底预约的核心逻辑是判断时段名额是否已满这段代码是整个预约模块最关键的地方Transactional public boolean reserve(ReservationDTO dto) { Long slotId dto.getSlotId(); Slot slot slotMapper.selectById(slotId); if (slot null || slot.getStatus() ! 1) { throw new BizException(该时段不可约); } Integer booked reservationMapper.selectCount( new LambdaQueryWrapperReservation() .eq(Reservation::getSlotId, slotId) .in(Reservation::getStatus, 0, 1) ); if (booked slot.getTotalQuota()) { throw new BizException(该时段名额已满); } Reservation r new Reservation(); r.setUserId(dto.getUserId()); r.setSlotId(slotId); r.setStatus(0); reservationMapper.insert(r); return true; }这里用的是“先查再插”的方案对课程设计和毕设完全够用。如果考虑更极端的高并发同时预约同一时段两个请求同时查到剩余1个名额、同时插入就可能超卖。更好的做法是在 reservation_slot 表上加booked字段更新时加条件WHERE booked total_quota配合UPDATE ... SET booked booked 1 WHERE slot_id ? AND booked total_quota通过数据库行锁兜底。我在整理这套项目时也建议过直接建一个(slot_id, status)的组合索引让查询走索引至少保证常规并发下查询效率没问题。至于要不要上 Redis 做名额预减要看具体并发量毕设阶段完全没必要。4.4 下单流程事务、订单号与价格快照下单流程是另一个必须加事务的地方。用户在购物车提交支付后后端要做四步生成订单主表记录、写入订单明细、计算订单总额、扣减库存如果有库存逻辑。四步里任何一步失败整个订单都不能落下所以 Service 方法要加Transactional。订单号建议单独设计不要用数据库自增ID直接当订单号太容易被预测且不美观。常见做法是时间戳 随机数 用户ID后四位例如20250507103012 4位随机数 userId后四位保证同一用户短时间内不会撞单号。价格快照是下单时必须做的遍历购物车从菜单表查询当前价格把菜品名称和价格写入 order_item。注意不要在 Service 里直接信任前端传的价格否则改个请求参数就能改订单金额这个安全问题在答辩时被问到会很难看。4.5 MyBatis-Plus高频用法分页插件与Lambda条件构造器MyBatis-Plus 的拦路虎是分页插件没配置导致 page 方法返回的数据只有 records 而没有 total。见得多的一次是前端明明回了两页的数据后台 total 却是0分页组件总页码一直是1。正确的分页插件配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }LambdaQueryWrapper 是本项目里出现频率最高的类。比如后台按订单状态筛选、按用户ID查预约记录、按宠物名称模糊搜索全部可以一条链式写法搞定。它的好处是类型安全字段名写错编译期直接报错比手写字符串写法稳得多。能少写 SQL 的地方尽量少写系统里只有多表关联查询才需要自定义 mapper XML这部分能收敛到很小。5. Vue3前端落地页面结构、状态管理与接口层封装5.1 初始化Vue3项目与路由设计前端部分我建议直接用 Vite 初始化工程不要用 Vue CLI后者新项目启动速度慢维护也接近停止。npm create vitelatest pet-cafe-front -- --template vue cd pet-cafe-front npm install npm install vue-router4 pinia axios element-plus目录结构按业务划分views 放页面组件components 放公共组件宠物卡片、时段选择器、订单状态标签api 目录按业务域拆接口文件user.js、pet.js、reservation.js、order.jsrouter 集中配置路由store 放 Pinia 状态。路由分两套一套是用户端页面包括首页、宠物列表、预约页、菜单点单页、个人中心另一套是管理端页面挂在/admin前缀下比如宠物管理、时段管理、订单管理、公告管理。路由守卫在进入管理端前校验用户角色普通用户访问/admin直接跳回首页。5.2 Pinia管理用户登录状态Pinia 是 Vue3 生态里比 Vuex 更简洁的状态管理方案。本项目里主要存两个内容登录用户的完整信息和 Token。export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }), actions: { setLogin(data) { this.token data.token this.userInfo data.user localStorage.setItem(token, this.token) localStorage.setItem(userInfo, JSON.stringify(this.userInfo)) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })配合持久化刷新页面后用户信息不会丢。登出时清掉本地存储再跳登录页这块逻辑在演示时很容易被考官问到。5.3 Axios封装和令牌注入前端所有接口请求统一走封装好的 axios 实例这样只在拦截器里把 Token 加一次不用每个请求手动带 header。响应拦截器处理两个高频情况后端返回业务错误码时统一弹出提示接口返回 401 时清空登录态并跳转登录页。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) service.interceptors.response.use( res res.data, err { if (err.response err.response.status 401) { localStorage.clear() router.push(/login) } return Promise.reject(err) } )接口统一返回规范结构比如{ code: 200, data: {...}, message: ok }前端根据 code 判断业务是否成功。这里强调一点HTTP 状态码和业务 code 是两回事不要混用。HTTP 401 只管鉴权失败业务 code 管验证码错误这类业务问题。5.4 用户端核心页面和管理端表格页用户端页面里宠物列表页是最吃组件拆分能力的一个宠物卡片组件负责展示宠物头像、名字、品种、性格标签和接待状态点击卡片进详情页详情里再嵌一个时段选择器组件用于提交预约。点单页则复用菜单分类和购物车逻辑下单完成后跳转订单详情。管理端页面就更模板化了。Element Plus 的 el-table 加 el-dialog 组合新增和编辑共用同一个弹窗表单删除用 Popconfirm 二次确认状态修改用 el-select 下拉。真正麻烦的是预约查看页需要展示某一天各时段的名额使用情况可以做成一个简单的“时段卡片网格”已满的置灰未满的显示剩余名额这样管理端核对起来一目了然。前端遇到大部分问题集中在跨域和代理配置上这块放到部署部分一起说。6. 本地运行一条龙环境检查、配置部署与踩坑实录6.1 环境安装清单先列一份我整理过的环境要求避免用错版本卡半天软件建议版本备注JDK8 或 11SpringBoot2.7 在这两个版本均稳定Maven3.6 及以上管理 Java 依赖MySQL8.0.x必须 8.05.7 部分语法插件不兼容Node.js16.19 或 18Vite4 要求 Node16Vite5 要求 Node18IDEA任意较新版本社区版够用Navicat / DataGrip选一导入 SQL 和检查数据方便Node 版本这个问题很多人忽略如果你用 Vite5 创建项目Node 版本太低会直接启动报错。建议统一装 Node18目前兼容性最稳。6.2 数据库初始化和后端配置拿到项目后先建数据库不要直接用 root 在默认字符集下导数据字符集问题会导致中文乱码。命令行执行CREATE DATABASE pet_cafe DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再导入项目提供的 SQL 文件。导入完成后检查一下各表的字符集是不是 utf8mb4尤其是 sys_user 表里有中文昵称和地址字段最容易出现乱码。后端配置文件 application.yml 里的数据源部分要重点检查spring: datasource: url: jdbc:mysql://localhost:3306/pet_cafe?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver注意allowPublicKeyRetrievaltrue这个参数。MySQL8 默认认证插件是 caching_sha2_password部分环境下客户端首次连接需要获取公钥不写这个参数可能直接报连接失败。时间参数必须加 serverTimezone很多 “Cannot create PoolableConnectionFactory” 报错都是从这来的。后端启动前顺手检查三件事Maven 是否配了阿里云镜像Lombok 插件是否已安装启动类是否正确扫描到 mapper 包。启动成功后访问http://localhost:8080应该有接口返回签名或首页提示不报红就是正常。6.3 Vite代理与前端启动前端与后端开发阶段的最佳方案是用 Vite 的 server.proxy 做代理不要用 CORS 跨域放行方案后者配置起来啰嗦且存在安全隐患。在 vite.config.js 里加export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })原理是浏览器请求的是http://localhost:3000/api/...Vite 开发服务器把/api前缀的请求转发给后端8080端口。前端代码里所有请求的 baseURL 写成/api就不会出现跨域报错。生产环境部署时再用 Nginx 做同样的事前后端各跑各的端口由 Nginx 统一转发。启动前端前先执行npm install如果慢可以把 registry 临时切到国内镜像。启动后访问http://localhost:3000能正常打开首页和登录页就算通了。6.4 高频报错与解决方案我整理了运行过程中最容易碰到的五个问题每一条都是真实踩过的报错或现象根因处理方法Communications link failure 或乱码连接URL缺少编码参数或时区参数加上characterEncodingutf8serverTimezoneAsia/ShanghaiAccess denied for user rootlocalhost密码错或MySQL8认证方式问题检查密码或创建新用户时指定mysql_native_password分页 total 恒为 0MyBatis-Plus 分页插件没注册添加MybatisPlusInterceptorBean前端请求接口一直 404/跨域Vite 代理没配置或 baseURL 写死使用/api开头配置 server.proxy登录接口报密码长度不能被识别BCrypt 加密与明文比对逻辑写错确认注册时就用 BCrypt 加密存储登录时BCrypt.checkpw比对还有一个非常隐蔽的坑如果前端用 Vite 直接打包后扔到某个静态服务器打开刷新 Vue Router 的 history 模式路由会 404因为静态服务器不知道如何处理前端路由。开发阶段没影响但如果你要部署演示建议路由用 createWebHashHistory或者在后端/Nginx配置里做好 fallback 重定向。毕设演示用 hash 模式最省事。7. 如果要扩展或改造这套源码我会从哪里下手跑通不是终点改造才是真正学到东西的过程。如果拿这套源码做毕业设计我会优先加三件事第一数据统计看板。宠物热度、时段预约率、菜品销量排行、营收趋势后端用几个聚合查询统计前端用 ECharts 出图。这一块工作量不大但展示效果非常直观也是答辩时最容易讲出彩的部分。第二消息通知。用户预约成功或订单状态变更时生成站内消息或邮件通知。不用接复杂消息队列一张 notice 表加一个查询接口就能实现却能明显提升系统完整度。第三会员与积分。用户消费累计积分积分可以抵扣订单金额。接入逻辑也不复杂需要给用户表加积分字段下单时增加积分计算。我自己带过的项目里凡是能把数据看板和消息通知这两项做明白的答辩分数普遍比光跑通 CRUD 的组高一个档次因为这证明了你理解业务链条而不只是会调框架。甩开文档直接跑代码是最容易卡住的先把数据关系和状态流转走一遍再动手改不迟。