SpringBoot+Vue宠物领养系统实战:从数据库设计到前后端联调排错 上周有个学弟抱着一套宠物领养系统的源码来找我后端 SpringBoot前端 Vue压缩包里还带着数据库脚本和说明文档。他说卡了一整天不是业务逻辑搞不明白而是项目根本起不来就算起来了宠物图片不显示、领养申请状态也乱成一锅粥。我花了两个多小时把整套代码从 controller 一路看到 mapper XML又对着数据库脚本捋了一遍状态流转算是把项目完整跑通了。这套系统虽然业务不算复杂但用户登录、宠物展示、领养申请、管理员审核、图片上传、公告管理一个不少正好覆盖了前后端分离项目最常见的几条核心链路。如果你正在做毕设或者想找一套结构清晰的 SpringBoot Vue 练手项目这篇文章应该能帮你少踩很多坑。我会按照从设计到实现、从启动到排错的顺序把整套宠物领养系统的关键点重新讲一遍。1. 项目概览这套宠物领养系统到底做了什么1.1 解决了什么真实需求宠物领养这个场景线下处理起来非常痛苦。救助站收到宠物之后通常只能在朋友圈发个照片等待消息靠缘分领养人要一家一家跑填纸质申请表然后等电话通知中间完全是黑盒。就算最后确定了领养关系纸质记录也容易被翻丢。这套系统的目标就是把“发布宠物—浏览筛选—提交申请—管理员审核—状态更新”整个流程搬到线上。系统里最核心的一条业务线是用户看到一只待领养的宠物觉得合适提交领养申请管理员进入后台查看申请理由决定通过还是驳回通过后宠物状态变为已领养其他人不能再申请如果驳回宠物重新回到待领养列表。这条线听上去简单但落到代码里牵涉用户表、宠物表、申请表三个实体之间的状态同步很多版本跑起来之后状态对不上基本都是这里出了岔子。1.2 功能清单与角色权限这套项目通常会分成用户端和管理端两端但登录入口可以共用一个页面也可以分开。最重要的两个角色是普通用户和管理员。我用一张表把常见功能列出来功能模块普通用户端管理后台账号登录注册、登录、修改头像管理员密码登录宠物浏览列表、详情、按分类筛选、搜索宠物信息的增删改查、上下架领养申请提交申请、查看自己的申请记录审核通过、驳回收藏与评论收藏宠物、发表评论删除违规评论公告通知查看公告发布公告数据统计无宠物数量、待审核申请数等简单统计如果某个版本拓展了“救助站”角色那救助站账号可以发布自己名下的宠物管理员只做平台审核。我在源码里见过的最常见结构其实是只有 user 和 admin 两种角色救助站被简化成了“管理员直接在后台发宠物”。理解角色的边界不光是看代码也是为后面改权限打基础。2. 技术选型拆解SpringBoot Vue 为什么是绝配2.1 后端选 SpringBoot 的理由这种管理型系统最核心的需求是把数据查出来、写进去、做个状态判断SpringBoot 在这个场景里几乎是零成本上手。首先是 starter 机制引入一个依赖就能把 web、数据库连接池、事务管理、参数校验全部带起来不需要像老 Spring 那样写一大堆 xml。其次是内置 Tomcat打成一个 jar 包就能跑本地开发和服务器部署都很直接。再配合 MyBatis 或 MyBatis Plus单表 CRUD 基本不用手写 SQL 逻辑开发进度会快很多。另外SpringBoot 的生态对“教程型项目”特别友好。网上能找到大量资料遇到问题搜索出来的方案基本都是可用的。这套宠物领养系统的核心操作无非是根据用户 id 查列表、插入一条领养申请、更新宠物状态、按分类统计数量用 SpringBoot 写 RESTful 接口非常顺畅。2.2 前端选 Vue 的原因Vue 的组件化开发特别适合后台管理系统这种“各种页面长得差不多”的项目。宠物卡片、分页器、状态标签、上传控件这些都可以抽成组件多个页面共用。配合 Element UI / Element Plus表格、表单、弹窗、消息提示全都有现成的原项目能在较短时间内完成很大程度靠的是这些成熟组件库。对新手来说Vue 还有一个好处单文件组件把模板、脚本、样式放在一个文件里读代码的时候不用来回跳转。比如要改领养申请页面只需要打开 apply.vue里面会有搜索框、申请表格和提交按钮思路非常清晰。这也是很多教学项目都喜欢用 Vue 的原因。2.3 代码分层与项目组织拿到源码先别急着跑应该先看一眼目录结构。合理的后端包结构一般是com.example.pet ├── controller // 接收前端请求返回结果 ├── service // 业务逻辑 │ └── impl ├── mapper // MyBatis 接口对应 XML ├── entity // 数据库实体类 ├── dto/vo // 入参封装和返回前端的数据对象 └── config // 拦截器、跨域等配置为什么一直强调分层因为 Controller 里如果写了大量业务判断短期能跑后期功能一多就变成一坨。分层不是装样子而是让每一层只负责自己的事。例如用户提交领养申请时Controller 只接收参数Service 里查宠物状态、插申请表、改宠物状态Mapper 只做数据操作。这样一旦状态逻辑出错你能直接从 Service 方法排查。更重要的是面试或者答辩被问项目架构时你能说清楚请求从 Vue 发出、到 Controller、Service、Mapper、数据库的完整链路这比说“我用的是 SpringBoot”有说服力得多。3. 数据库设计一张表到一套领养流程3.1 核心表结构和关键字段数据库设计是整套系统的地基。拿到 .sql 文件别急着导入值得先花时间把表关系看一遍。核心表一般有这几张用户表 user、宠物表 pet、宠物分类表 category、领养申请表 adoption_apply、收藏表 favorite、评论表 comment、公告表 notice。管理员可能单独建 admin 表也可能直接用 user 表里的 role 字段区分。宠物表是最核心的表关键字段大概是这样CREATE TABLE pet ( id int NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 宠物名称, category_id int DEFAULT NULL COMMENT 分类id, breed varchar(50) DEFAULT NULL COMMENT 品种, age varchar(20) DEFAULT NULL COMMENT 年龄描述, sex tinyint DEFAULT 0 COMMENT 0公1母, status tinyint DEFAULT 0 COMMENT 0待领养1申请中2已领养3已下架, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, description text COMMENT 简介描述, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物表;这里的 status 是整个领养流程的核心。用户端列表只应该查出 status0 的宠物详情页提交申请前还要再判断一次 status管理员审核通过后 status 改成 2。很多项目出现“已经被人申请了还能继续申请”的 bug就是因为列表查询和提交接口没有统一用这个字段把关。领养申请表同样重要CREATE TABLE adoption_apply ( id int NOT NULL AUTO_INCREMENT, pet_id int NOT NULL COMMENT 宠物id, user_id int NOT NULL COMMENT 申请人id, phone varchar(20) DEFAULT NULL, address varchar(255) DEFAULT NULL, reason varchar(500) DEFAULT NULL COMMENT 领养理由, status tinyint DEFAULT 0 COMMENT 0待审核1通过2驳回3已完成, apply_time datetime DEFAULT NULL, audit_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表;注意一下user_id 和 pet_id 在表结构里没有声明外键约束这是有意为之。中小型项目里物理外键会增加删改数据和表连接时的锁竞争实际开发中用逻辑外键更常见。只要业务代码里保证写入的 id 有效查询时 join 不做物理约束问题不大。3.2 领养状态流转这条线千万不能乱我把这套系统最容易被问倒的状态流转逻辑写清楚。核心是两个状态机的配合宠物 pet.status0 待领养1 申请中2 已领养3 已下架申请 adoption_apply.status0 待审核1 审核通过2 审核驳回3 领养完成完整的流程是用户提交申请时生成一条 status0 的申请记录同时把 pet.status 改成 1表示这只宠物已经有候选人了。管理员审核通过把申请记录改成 1宠物状态改成 2。如果管理员驳回申请记录改成 2宠物状态必须改回 0。如果确认领养手续办完可以把申请记录改成 3宠物状态继续保留在 2。这里最容易被忽略的一点是更新申请表和更新宠物表必须在一个事务里完成。否则可能出现“申请记录是待审核但宠物已经被改成已领养”的中间状态。写代码时记得给 Service 方法加 Transactional一旦后半步失败前半步也要回滚。还有个常见缺陷是同一个用户对同一只宠物可以反复提交申请。要防住这个提交前先查一下是否有 pet_id user_id status0 的记录存在。更保险的做法是在申请表上加一个联合唯一索引字段组合是 pet_id、user_id、status并用状态字段的逻辑值来区分。3.3 SQL 脚本和演示数据怎么用好的源码包一定会带一份像样的测试数据。如果导入后只有空表登录进去页面光秃秃的很多功能都看不出效果。正常演示数据应该包含一个管理员账号、几个普通用户、十只左右不同分类的宠物、至少两条待审核申请以及一些宠物浏览记录。这样你打开应用首页有内容申请审核也有数据可以操作不需要自己再造数据。导入 SQL 时最容易出现中文乱码。原因是 .sql 文件保存的是 UTF-8 编码但 MySQL 客户端连接字符集可能不是 UTF-8。命令行导入前先执行set names utf8mb4;或者用图形化工具导入时明确选择 UTF-8基本能解决。如果还是乱码检查数据库连接串里有没有加上characterEncodingutf8。4. 核心功能实现从登录到领养审核4.1 JWT 登录与接口拦截这套项目普遍采用 token 登录典型实现是 JWT。流程是用户提交用户名和密码后端校验通过后生成一个带用户 id 和过期时间的 token 返回给前端前端把 token 存在 localStorage 里每次请求都在 header 里带上 Authorization后端拦截器验证 token 是否有效并从 token 中取出当前用户 id。后端拦截器核心代码如下public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { throw new RuntimeException(未登录或登录已过期); } String token auth.substring(7); Integer userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } }然后注册拦截器一般放行/api/user/login、/api/user/register以及/upload/**这类静态图片资源其余/api/**要验证。前端配合起来也很简单axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config })这里有一个我实测比较多的问题不少源码密码加密用的是 MD5只是加了个盐安全性不够。如果你准备把这个项目当正式作品建议把 MD5 换成 BCrypt。Spring Security 里有现成的 BCryptPasswordEncoder或者用 hutool 的 Bcrypt 工具也行改动量不大。另外一个坑是 token 过期时间设得太短比如 10 分钟用户填个申请表就会掉线。演示项目设置 24 小时没什么问题正式项目再考虑 refresh token 机制。4.2 领养申请与状态同步提交领养申请不是简单插入一条记录它要把宠物状态、申请记录、重复校验、事务一起处理好。Service 层大致是这样Transactional public boolean applyAdopt(Integer petId, Integer userId, ApplyDTO dto) { Pet pet petMapper.selectById(petId); if (pet null || pet.getStatus() ! 0) { throw new RuntimeException(该宠物当前不可领养); } Integer unfinished applyMapper.countUnfinishedByPetAndUser(petId, userId); if (unfinished ! null unfinished 0) { throw new RuntimeException(您已有待审核的申请请勿重复提交); } AdoptionApply apply new AdoptionApply(); apply.setPetId(petId); apply.setUserId(userId); apply.setPhone(dto.getPhone()); apply.setAddress(dto.getAddress()); apply.setReason(dto.getReason()); apply.setStatus(0); apply.setApplyTime(new Date()); applyMapper.insert(apply); pet.setStatus(1); petMapper.updateById(pet); return true; }审核接口就做成两种情况通过时把申请记录的 status 改成 1同时把 pet.status 改成 2驳回时把申请记录的 status 改成 2同时把 pet.status 改成 0。一定要把这几个 update 放在同一个事务里不然会出现“申请说通过了宠物却还是申请中”这种尴尬状态。我还见过一种很隐蔽的 bug审核驳回后宠物确实回到了待领养但宠物列表接口的查询条件写的是status in (0, 1)导致重新待领养的宠物依然出现在“已申请”视图中。排查这类问题直接看 mapper XML 里的查询条件比改前端更有效。4.3 图片上传与回显宠物必须带图片不然整个系统完全没有视觉吸引力。文件上传实现不难后端接收 MultipartFile把文件保存到本地磁盘的一个 upload 目录然后返回访问路径。难点在于 SpringBoot 默认只处理 classpath 下的静态资源你上传到磁盘的文件如果不在 classpath 里前端会 404。需要在配置里把磁盘目录也当成静态资源spring: web: resources: static-locations: classpath:/static/,file:${pet.upload-path}这里的pet.upload-path是自定义配置项例如pet: upload-path: D:/pet-project/upload/上传接口大致是PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(uploadPath filename)); return Result.ok(/upload/ filename); }这里建议数据库只存相对路径/upload/20250312_xxx.jpg前端展示时拼接后端的 IP 和端口。如果把完整绝对路径存进数据库换一台电脑或者改了端口图片全挂。很多新手项目直接存http://localhost:8080/upload/1.jpg当前设备能显示部署上线后必出问题。5. 快速把源码跑起来环境、数据库、前后端5.1 需要准备的环境完整跑起一套项目需要这几样东西JDK 1.8 或 JDK 11环境变量配好Maven 3.6Node.js 14npm 随 Node 一起安装MySQL 5.7 或 MySQL 8IDEA 或 EclipseVSCode 也可以拿到源码先看一下后端 application.yml里面如果配了 Redis说明登录或者缓存用到了 Redis大多数宠物领养系统的简化版本不需要 Redis验证码和 session 直接放在本地。确认清楚再启动避免浪费时间去装额外中间件。5.2 后端启动步骤第一步用 IDEA 打开后端目录等待 Maven 把依赖下载完。第二步修改src/main/resources/application.yml里的数据库连接信息。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver连接串最好加上serverTimezoneAsia/Shanghai不然时间会有八小时误差。第三步执行 .sql 脚本创建数据库。第四步运行启动类里的 main 方法。看到类似Started PetApplication in 5.32 seconds的日志说明后端起来了。可以先在浏览器访问一个公开接口比如http://localhost:8080/api/pet/list能返回 JSON 就算大概率通了。如果 MySQL 8 连接报错说认证插件不支持caching_sha2_password大多数是因为驱动版本太低。优先升级 mysql-connector-java 版本而不是去改数据库认证方式。本地开发想快速绕过也不是不行可以在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;但这不是正规做法生产环境别这么搞。5.3 前端启动步骤进入前端目录后执行npm install。如果嫌慢可以把 npm registry 切到国内镜像npm config set registry https://registry.npmmirror.com安装完依赖先看一眼vue.config.js。很多前后端分离项目的前端在 3000 端口后端在 8080为了避免跨域都配置了 devServer 代理module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端代码里所有请求写/api/xxx浏览器会把请求交给 3000 端口的开发服务器再由它转发到 8080不会触发跨域。最后执行npm run dev浏览器打开http://localhost:3000用源码里的管理员账号登录。5.4 文档和 SQL 脚本的配合方式源码包的文档通常包含需求说明、数据库设计、接口说明和部署教程。这些文档最适合用来构建整体认知。建议你先把数据库设计里的表关联图和自己导入后的数据库对照一遍再把接口文档中关于领养申请的字段和前端页面里的表单对照一遍。这样你会很清楚地看到前端一个提交按钮后端的 Service 方法数据库的一张表三者是怎么对应上的。答辩或者后续开发这都是最扎实的底稿。6. 常见问题与排查技巧实录6.1 启动阶段问题速查我把实际跑这套项目时最常见的启动问题整理成一个速查表。问题现象可能原因解决办法启动后端口被占用8080 被其他程序占用改server.port或占用端口的进程连接数据库报错url、账号、密码不对核对 application.yml 配置连接数据库报时区错误连接串没有 timezone加serverTimezoneAsia/Shanghai导入 SQL 中文乱码客户端字符集不是 UTF-8导入前执行set names utf8mb4npm install 卡死网络问题切换镜像源后重试页面白屏前端依赖没装全或编译错误清掉 node_modules 重新 npm install6.2 前后端联调阶段的接口问题项目能启动不等于功能正常联调阶段的问题更多。最常见的是登录之后所有请求都 401。先看浏览器控制台里发出的请求头有没有带上 Authorization再看后端拦截器放行了哪些路径。很多项目登录接口已经放行但是其他接口要求 header 里的 key 叫 token而前端代码存的是 Authorizationkey 对不上就会一直 401。另一个常见问题是前端列表空数据。这种时候先在浏览器直接访问后端接口比如/api/pet/list?page1limit10确认 JSON 是否正常。如果接口有数据页面却没有多半是前端把 response.data 再包了一层字段名对不上。打开控制台查看实际返回的 JSON 结构再去前端页面找数据字段名基本能定位。跨域问题在配置了代理之后一般不会出现但如果你不是通过 npm run dev 启动而是把前端打包后放在 Nginx 里就要在 Nginx 的 location 里配置 proxy_pass 指向后端否则就会出现跨域或者 404。6.3 状态异常和业务逻辑问题这类问题在宠物领养系统里最有代表性。我遇到过几次典型的情况用户重复申请同一只宠物因为提交时没有检查 pet.status 和已有申请记录。管理员通过审核后宠物在首页还能正常申请因为列表查询条件写的是status0但审核通过后 pet.status 没有同步更新成 2。管理员驳回后宠物一直锁在“申请中”因为审核接口漏写了pet.setStatus(0)。用户删除了自己的账号领养申请记录报错因为代码做了物理删除而业务应该用逻辑删除。处理这类问题最快的方法是先复现然后同时看前端请求参数、后端日志、数据库当前状态。很多状态问题都是这三者不一致导致的。在排查时养成一个习惯每次操作前后把pet表和adoption_apply表的关键记录截图对比状态异常的节点很快就能浮出水面。7. 这套项目拿到手之后我建议你这样用7.1 从源码里能学到什么这套项目最有价值的不是“能跑”而是能学习一个完整业务闭环如何落地。很多新手初看源码容易陷入“每个文件都看了但串不起来”的状态。我的建议是先看数据库表再看前端页面和接口的对应关系最后看 Service 里的状态流转。你不需要记住每一行代码只需要建立一条主线一只流浪宠物从进入系统到被人带走的完整过程系统里哪些数据在发生变化。时间允许的话用 Postman 把以下接口全部手动调用一遍注册、登录、发布宠物、查看宠物列表、提交领养申请、审核通过、看看列表状态变化。这个过程会让你对权限控制和数据状态有非常直观的感觉比读十遍源码都有效。7.2 适合二次开发的方向如果准备把它变成自己的毕设或者作品集与其改页面颜色不如做几个有含金量的功能。优先级比较高的方向有给领养申请增加短信或邮件通知审核结果实时触达用户把角色模型从“用户和管理员”扩展成“用户、救助站、平台管理员”三种角色给宠物表加 org_id 字段让每个救助站管理自己的宠物用 Redis 缓存首页宠物列表和公告减少数据库压力把图片上传从本地磁盘迁移到对象存储接入云服务增加按月份统计领养趋势的接口和图表页面让管理员的數據面板更有说服力。这五个方向里挑一到两个做深就足够在答辩时展示你的工程能力了。每个方向都有明确的入口加字段、加接口、加页面改动路径清晰不会像无头苍蝇一样乱改。最后再分享一个小技巧。做这类全栈项目最容易被问到的就是“为什么这个状态要这样设计”。你可以提前把宠物状态和领养申请状态画成一张表哪个动作触发哪个状态改变一目了然。我自己调试这套系统的过程中最大的体会是SpringBoot 和 Vue 本身并不复杂真正麻烦的是模块之间状态和权限要保持自洽。把这条主线理顺这个宠物领养系统才算真正吃透了后面再遇到别的管理系统思路也是同一套。