☰
SpringBoot+Vue宠物领养系统实战:从数据库设计到前后端联调排错
2026/10/11 16:58:53 网站建设 项目流程

上周有个学弟抱着一套宠物领养系统的源码来找我,后端 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物表';

这里的 status 是整个领养流程的核心。用户端列表只应该查出 status=0 的宠物;详情页提交申请前,还要再判断一次 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='领养申请表';

注意一下,user_id 和 pet_id 在表结构里没有声明外键约束,这是有意为之。中小型项目里物理外键会增加删改数据和表连接时的锁竞争,实际开发中用逻辑外键更常见。只要业务代码里保证写入的 id 有效,查询时 join 不做物理约束,问题不大。

3.2 领养状态流转,这条线千万不能乱

我把这套系统最容易被问倒的状态流转逻辑写清楚。核心是两个状态机的配合:

  • 宠物 pet.status:
    • 0 待领养
    • 1 申请中
    • 2 已领养
    • 3 已下架
  • 申请 adoption_apply.status:
    • 0 待审核
    • 1 审核通过
    • 2 审核驳回
    • 3 领养完成

完整的流程是:用户提交申请时,生成一条 status=0 的申请记录,同时把 pet.status 改成 1,表示这只宠物已经有候选人了。管理员审核通过,把申请记录改成 1,宠物状态改成 2。如果管理员驳回,申请记录改成 2,宠物状态必须改回 0。如果确认领养手续办完,可以把申请记录改成 3,宠物状态继续保留在 2。

这里最容易被忽略的一点是:更新申请表和更新宠物表必须在一个事务里完成。否则可能出现“申请记录是待审核,但宠物已经被改成已领养”的中间状态。写代码时记得给 Service 方法加 @Transactional,一旦后半步失败,前半步也要回滚。

还有个常见缺陷是同一个用户对同一只宠物可以反复提交申请。要防住这个,提交前先查一下是否有 pet_id + user_id + status=0 的记录存在。更保险的做法是在申请表上加一个联合唯一索引,字段组合是 pet_id、user_id、status,并用状态字段的逻辑值来区分。

3.3 SQL 脚本和演示数据怎么用

好的源码包一定会带一份像样的测试数据。如果导入后只有空表,登录进去页面光秃秃的,很多功能都看不出效果。正常演示数据应该包含:一个管理员账号、几个普通用户、十只左右不同分类的宠物、至少两条待审核申请,以及一些宠物浏览记录。这样你打开应用,首页有内容,申请审核也有数据可以操作,不需要自己再造数据。

导入 SQL 时最容易出现中文乱码。原因是 .sql 文件保存的是 UTF-8 编码,但 MySQL 客户端连接字符集可能不是 UTF-8。命令行导入前先执行set names utf8mb4;,或者用图形化工具导入时明确选择 UTF-8,基本能解决。如果还是乱码,检查数据库连接串里有没有加上characterEncoding=utf8。

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.6+
  • Node.js 14+,npm 随 Node 一起安装
  • MySQL 5.7 或 MySQL 8
  • IDEA 或 Eclipse,VSCode 也可以

拿到源码先看一下后端 application.yml,里面如果配了 Redis,说明登录或者缓存用到了 Redis;大多数宠物领养系统的简化版本不需要 Redis,验证码和 session 直接放在本地。确认清楚再启动,避免浪费时间去装额外中间件。

5.2 后端启动步骤

第一步,用 IDEA 打开后端目录,等待 Maven 把依赖下载完。第二步,修改src/main/resources/application.yml里的数据库连接信息。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,连接串最好加上serverTimezone=Asia/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 'root'@'localhost' 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加serverTimezone=Asia/Shanghai
导入 SQL 中文乱码客户端字符集不是 UTF-8导入前执行set names utf8mb4
npm install 卡死网络问题切换镜像源后重试
页面白屏前端依赖没装全或编译错误清掉 node_modules 重新 npm install

6.2 前后端联调阶段的接口问题

项目能启动不等于功能正常,联调阶段的问题更多。最常见的是登录之后所有请求都 401。先看浏览器控制台里发出的请求头有没有带上 Authorization,再看后端拦截器放行了哪些路径。很多项目登录接口已经放行,但是其他接口要求 header 里的 key 叫 token,而前端代码存的是 Authorization,key 对不上就会一直 401。

另一个常见问题是前端列表空数据。这种时候先在浏览器直接访问后端接口,比如/api/pet/list?page=1&limit=10,确认 JSON 是否正常。如果接口有数据,页面却没有,多半是前端把 response.data 再包了一层,字段名对不上。打开控制台查看实际返回的 JSON 结构,再去前端页面找数据字段名,基本能定位。

跨域问题在配置了代理之后一般不会出现,但如果你不是通过 npm run dev 启动,而是把前端打包后放在 Nginx 里,就要在 Nginx 的 location 里配置 proxy_pass 指向后端,否则就会出现跨域或者 404。

6.3 状态异常和业务逻辑问题

这类问题在宠物领养系统里最有代表性。我遇到过几次典型的情况:

  • 用户重复申请同一只宠物:因为提交时没有检查 pet.status 和已有申请记录。
  • 管理员通过审核后,宠物在首页还能正常申请:因为列表查询条件写的是status=0,但审核通过后 pet.status 没有同步更新成 2。
  • 管理员驳回后,宠物一直锁在“申请中”:因为审核接口漏写了pet.setStatus(0)。
  • 用户删除了自己的账号,领养申请记录报错:因为代码做了物理删除,而业务应该用逻辑删除。

处理这类问题,最快的方法是先复现,然后同时看前端请求参数、后端日志、数据库当前状态。很多状态问题都是这三者不一致导致的。在排查时养成一个习惯:每次操作前后,把pet表和adoption_apply表的关键记录截图对比,状态异常的节点很快就能浮出水面。

7. 这套项目拿到手之后,我建议你这样用

7.1 从源码里能学到什么

这套项目最有价值的不是“能跑”,而是能学习一个完整业务闭环如何落地。很多新手初看源码,容易陷入“每个文件都看了,但串不起来”的状态。我的建议是先看数据库表,再看前端页面和接口的对应关系,最后看 Service 里的状态流转。你不需要记住每一行代码,只需要建立一条主线:一只流浪宠物从进入系统到被人带走的完整过程,系统里哪些数据在发生变化。

时间允许的话,用 Postman 把以下接口全部手动调用一遍:注册、登录、发布宠物、查看宠物列表、提交领养申请、审核通过、看看列表状态变化。这个过程会让你对权限控制和数据状态有非常直观的感觉,比读十遍源码都有效。

7.2 适合二次开发的方向

如果准备把它变成自己的毕设或者作品集,与其改页面颜色,不如做几个有含金量的功能。优先级比较高的方向有:

  • 给领养申请增加短信或邮件通知,审核结果实时触达用户;
  • 把角色模型从“用户和管理员”扩展成“用户、救助站、平台管理员”三种角色,给宠物表加 org_id 字段,让每个救助站管理自己的宠物;
  • 用 Redis 缓存首页宠物列表和公告,减少数据库压力;
  • 把图片上传从本地磁盘迁移到对象存储,接入云服务;
  • 增加按月份统计领养趋势的接口和图表页面,让管理员的數據面板更有说服力。

这五个方向里,挑一到两个做深,就足够在答辩时展示你的工程能力了。每个方向都有明确的入口:加字段、加接口、加页面,改动路径清晰,不会像无头苍蝇一样乱改。

最后再分享一个小技巧。做这类全栈项目,最容易被问到的就是“为什么这个状态要这样设计”。你可以提前把宠物状态和领养申请状态画成一张表,哪个动作触发哪个状态改变,一目了然。我自己调试这套系统的过程中最大的体会是:SpringBoot 和 Vue 本身并不复杂,真正麻烦的是模块之间状态和权限要保持自洽。把这条主线理顺,这个宠物领养系统才算真正吃透了,后面再遇到别的管理系统,思路也是同一套。

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

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

立即咨询