做一个完整项目最大的痛点不是“不会敲代码”,而是“不知道整个系统该怎么串起来”。很多同学学完Java基础、SpringBoot、Vue,真到自己动手做一个带前后端的完整项目时,还是容易卡壳:表结构怎么设计、接口怎么分层、前端怎么对接、权限怎么控制、图片怎么上传、领养申请这种多状态流程怎么流转,每一步都是坎。
所以当我看到“Java Web 宠物领养系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】”这个标题的时候,第一反应是:这套技术组合,基本就是当下Java Web项目最常见、最适合拿来练手、也最贴近企业实际开发的一套配置。SpringBoot负责后端接口,Vue3负责前端页面,MyBatis-Plus把数据库操作简化到一个不可思议的程度,MySQL8.0作为存储底座,再加一套完整的说明文档——对于正在做毕业设计、或者想找一份完整项目来学习前后端分离开发的同学来说,这个项目刚好长在需求点上。
这篇文章我不想对着源码一条条念,而是想以一个做过类似项目的开发者的视角,拆一拆这个宠物领养系统背后的设计思路、技术选型逻辑、数据库怎么建、关键功能怎么落、前端怎么跟后端配合,以及我在实际开发中踩过哪些坑。无论你是准备拿这个项目做毕设,还是想通过阅读源码提升自己的项目经验,这篇拆解应该都能帮你省下不少时间。
1. 项目整体设计与技术选型思路
1.1 为什么这套技术组合是“黄金搭配”
先聊技术选型。SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,这四个放在一起,几乎覆盖了一个小型前后端分离项目的全部核心环节。
SpringBoot2不用多说,它把Spring家族的配置复杂度降到了最低,内置Tomcat,打一个jar包直接跑,不需要额外装服务器。对于学生项目和中小型系统来说,SpringBoot2是绝对的主流选择。这里要注意一个细节:为什么很多项目还用SpringBoot2而不是SpringBoot3?因为SpringBoot3强制要求JDK17,而很多学校机房、老教程、以及大量第三方依赖对JDK8的生态还非常依赖。SpringBoot2配合JDK8,是一个“稳”字当头的组合,遇到问题网上一搜全是解决方案,根本不用担心卡住。
Vue3是前端框架的当红炸子鸡,Composition API的引入让组件逻辑的组织方式更清晰,配合Vite构建工具,开发体验比Vue2的webpack时代快了一个量级。在这个项目里,Vue3主要负责用户端和管理端的页面渲染,通过axios调用后端的RESTful接口,实现前后端完全分离。
MyBatis-Plus则是MyBatis的增强版,它最核心的价值是“单表CRUD不用写SQL”——BaseMapper接口内置了insert、delete、update、selectById等常用方法,还支持Wrapper条件构造器做动态查询。说白了,在宠物管理这类以单表操作为主的业务场景里,MyBatis-Plus能帮你省掉大概60%的重复SQL编写工作。
MySQL8.0作为数据库,相比5.7,在窗口函数、CTE(公共表表达式)、JSON支持、性能优化上都有明显提升。而且MySQL8.0在字符集上默认使用utf8mb4,对中文、以及表情符号这类特殊字符都能完美存储,这个版本选得很合理——至少你不会遇到老版本那种“中文能存但emoji变问号”的尴尬。
1.2 前后端分离架构下,代码该怎么组织
这个项目既然用了Vue3,那一定是前后端分离的项目结构。后端是一个独立的SpringBoot工程,前端是一个独立的Vue工程,两者之间通过HTTP接口通信。
先从后端说起。一个合理的SpringBoot工程结构通常是这样的:
src/main/java/com/xxx/pet ├── controller // 接收前端请求,返回JSON ├── service // 业务逻辑层,处理具体业务规则 ├── mapper // MyBatis-Plus的数据访问层 ├── entity // 数据库表对应的实体类 ├── common // 公共类:统一返回结果、异常处理、工具类 ├── config // 配置类:跨域、拦截器、文件上传等这种分层方式在社区里叫“Controller-Service-Mapper”三层架构,也是目前Java后端最主流的写法。Controller只负责参数接收和结果返回,不写业务;Service负责核心逻辑,比如领养申请时检查宠物是否已被领养;Mapper负责跟数据库打交道。各层职责单一,出问题的时候定位非常方便。
前端Vue3工程的结构则更偏向按“页面/组件”来组织:
src ├── views // 页面级别组件,比如宠物列表页、领养详情页 ├── components // 可复用组件,比如导航栏、宠物卡片、图片上传 ├── api // 接口请求封装,统一管理所有后端URL ├── router // Vue Router路由配置 ├── store // Pinia状态管理,比如保存用户登录状态前端会基于UI组件库(比如Element Plus)快速搭建后台管理页面,用户端页面则更偏定制化,往往需要自己写样式。这个项目的管理端大体上就是“左侧菜单栏 + 顶部导航栏 + 右侧内容区”的经典后台布局,配合Element Plus的table、form、dialog等组件,能很快拼出完整功能页面。
1.3 这个系统到底解决了什么问题
宠物领养系统本质上解决的是一个社会化的实际问题:流浪动物救助机构或宠物主人要发布待领养宠物信息,领养人要浏览宠物、了解详情、提交领养申请;救助机构需要审核申请人的条件、跟进领养进度。如果没有一个线上系统,这些流程会变得非常低效:信息靠张贴告示、申请靠手写表格、审核靠电话沟通。
所以这个系统的核心价值在于,把“发布宠物—浏览信息—提交领养申请—线下审核—结果反馈”这条链路搬到了线上。从功能层面来看,系统需要提供两类角色、两类功能集:
- 普通用户:注册登录、浏览宠物列表、查看宠物详情、提交领养申请、查看自己的申请记录、收藏宠物、修改个人资料。
- 管理员:宠物信息管理(发布/编辑/下架)、领养申请审核(通过/拒绝)、用户管理、公告管理、留言管理。
判断一个宠物领养系统合不合格,就看这两类角色对应的功能链路是否走通。接下来我按模块拆开细讲。
2. 功能模块拆解:用户端与管理端的核心功能
2.1 用户端:从“看到宠物”到“成功领养”的完整闭环
用户端是这个系统的门面,也是浏览量和交互量最大的部分。一个合格的用户端至少要包含以下功能模块:
- 宠物浏览与筛选:宠物列表页是整个系统的流量入口。用户需要按宠物种类(猫/狗/兔子等)、性别、年龄、所在城市等条件筛选宠物。这个功能在后端对应的是一个带条件查询的分页接口,MyBatis-Plus的LambdaQueryWrapper在这时非常好用,你只需要根据前端传过来的参数动态拼接查询条件即可,不需要手写一堆if判断SQL字符串。
- 宠物详情页与图片展示:详情页需要展示宠物的多张图片、基本资料、健康状况描述、救助故事等。这里有一个小细节——图片存储。小项目通常不引入独立的文件服务器,而是把图片以文件形式上传到服务器本地目录,数据库中仅保存图片的URL路径。前端通过完整的URL访问图片即可。
- 领养申请与多状态流转:这是整个系统业务逻辑最重的部分。用户点击“申请领养”后,需要填写领养表单:住址、是否有养宠经验、家庭成员是否同意等。提交后生成一条申请记录,状态为“待审核”;管理员审核通过后变成“已通过”,拒绝则是“已拒绝”。用户可以在“我的申请”列表里实时看到审核进度。
- 个人中心与收藏功能:用户可以维护自己的头像、昵称、手机号、住址等基本信息;收藏功能则对应一张“用户-宠物”的关联表,实现的是简单的用户与宠物多对多关系。用MyBatis-Plus做收藏的添加和取消,本质上就是对关联表做insert和delete,逻辑非常清爽。
- 公告与留言:公告模块解决的是平台在“宠物领养成功啦”“基地开放日”等场景下向用户发布公共信息的需求。留言功能则给用户提供一个表达疑问或反馈的轻量通道。这两个模块在技术上都很简单,核心还是单表的CRUD。
2.2 管理端:审核与数据维护的工作台
管理端是给平台运营人员用的,核心目标是一个词:高效。所有需要“人工介入”的操作都放在管理端完成,包括:
- 宠物信息管理:管理员可以新增宠物、编辑宠物信息、上传宠物多张图片、设置宠物当前状态(待领养/已申请/已领养/下架)。这个模块背后就是一张pet表,所有操作都在围绕这张表做增删改查。
- 领养审核管理:审核列表要能展示所有申请记录,包含申请人信息、申请时间、申请状态。点击“详情”可以查看用户填写的完整申请表单。通过或拒绝,本质上是对申请记录的status字段做一次update。
- 用户管理:查看用户列表、重置用户密码、禁用异常账号。管理员对这个模块的诉求是“看得见、控得住”。
- 数据统计:一个加分项。统计每日新增宠物数、每月领养成功数、用户注册趋势。如果是基于MySQL做,几条count、group by的SQL就能搞定,用ECharts画个趋势图,整个系统的完成度会瞬间上一个档次。
2.3 角色权限怎么控制
很多初学者容易忽略权限设计,但实际上“普通用户只能操作自己的数据,管理员才能操作所有数据”是系统安全的基本要求。这个项目通常采用JWT(JSON Web Token)方案做登录认证:
- 用户登录成功后,后端签发一个包含用户ID和角色的token返回给前端。
- 前端把token存在localStorage里,每次请求接口时在请求头里带上
Authorization: Bearer <token>。 - 后端通过拦截器校验token的合法性,并从中解析出当前用户的ID和角色。
- 对于需要进行权限控制的接口,后端在Controller层做一次角色判断,比如宠物管理相关的接口只有管理员角色能访问。
这里有一个实操建议:后端可以定义@RequireAdmin这类自定义注解,配合拦截器实现“注解式权限控制”。这样比在每一个Controller方法里手写if判断要优雅得多。权限这块虽然初学时候觉得复杂,但一旦做成,对整个项目的安全性是一个质的提升。
3. 数据库设计:几张核心表与关键字段解析
3.1 核心表结构与设计思路
一个宠物领养系统的数据库设计并不复杂,核心表大概6~7张就够用了。我整理了一个标准的设计清单:
| 表名 | 用途说明 | 关键字段 |
|---|---|---|
| user | 用户表 | id、username、password(加密存储)、nickname、phone、avatar、role、status、create_time |
| pet | 宠物表 | id、name、type(猫/狗)、gender、age、city、description、cover_image、status、create_time |
| pet_image | 宠物图片表 | id、pet_id、image_url、sort |
| adoption_apply | 领养申请表 | id、user_id、pet_id、reason、address、experience、status、create_time、update_time |
| favorite | 收藏表 | id、user_id、pet_id、create_time |
| notice | 公告表 | id、title、content、create_time |
| comment / message | 留言表 | id、user_id、content、create_time |
如果你打开源码对照,这些表大概八九不离十。设计时需要注意的几个细节:
第一个是用户表的密码字段。一定要加密存储,正常情况下不能用明文。项目里常用的做法是使用Spring Security自带的BCryptPasswordEncoder对密码哈希,即使数据库泄露,攻击者拿到的也只是一串哈希值而不是原始密码。
第二个是宠物多张图片的问题。如果只把多张图片塞进pet表的一个字段里(比如用逗号拼接URL),虽然能实现,但后续做“修改某一图片顺序”或“删除单张图片”时会非常难受。标准的做法是单独建一张pet_image关联表,一个宠物对应多张图片,这就是“一对多”的关系,完全符合数据库范式化的设计思路。
第三个是所有表都建议保留create_time字段,有必要的还加上update_time。哪怕你目前还没有数据分析需求,这也是一种性价比极高的“预留设计”——后期做任何时间维度的统计时,你都会庆幸当初加了这个字段。另外MyBatis-Plus支持字段自动填充,只要在实体类的create_time上标注@TableField(fill = FieldFill.INSERT)并在配置类里实现MetaObjectHandler,插数据时就会自动填上当前时间,完全不用手动set,这个特性值得用起来。
3.2 领养申请表的状态字段设计
领养申请表是整个系统里最有“业务感”的一张表。它最核心的一个字段是status,建议用tinyint类型,用数字表示状态。一个常用的设计是:
0 = 待审核 1 = 审核通过 2 = 审核拒绝 3 = 已取消 4 = 已完成(宠物已成功交到领养人手上)为什么用数字而不是直接存中文?存储更省空间是其次,关键是状态扩展方便。比如哪天想加一个“待回访”状态,直接在代码里加一个常量值就行,不需要改表结构。这里推荐在Java代码里用枚举来定义状态常量,比如:
public enum ApplyStatus { PENDING(0, "待审核"), APPROVED(1, "审核通过"), REJECTED(2, "审核拒绝"), CANCELED(3, "已取消"), COMPLETED(4, "已完成"); private final int value; private final String desc; ApplyStatus(int value, String desc) { this.value = value; this.desc = desc; } }这样在代码各处都通过枚举引用,而不是散落一堆魔法数字,可读性会好非常多,也方便前端做状态标签的颜色映射。
3.3 表关联查询的几种方式
这个项目涉及不少关联查询场景:查询宠物时要把封面图或图片列表带出来,查询申请列表时要带出用户名和宠物名,查询收藏列表时要带出宠物摘要信息。
第一种方案是写SQL关联查询,用MyBatis-Plus的XML文件自定义多表联查SQL。比如查询申请列表时:
SELECT a.*, u.nickname AS user_name, u.phone AS user_phone, p.name AS pet_name FROM adoption_apply a LEFT JOIN user u ON a.user_id = u.id LEFT JOIN pet p ON a.pet_id = p.id ORDER BY a.create_time DESC这种方案性能好,SQL直观,适合“关联的字段必须一次性查出”的场景。缺点是需要手写SQL,无法完全依赖MyBatis-Plus的自动CRUD。
第二种方案是分步查询,先用一个Mapper查出主表数据,再在Service层循环调用另一个Mapper补充关联信息。这种方案代码逻辑清晰,但会引入N+1查询问题,性能上不如联表SQL,适合数据量小的场景。
我的建议是:列表页这种高频、数据量可能增长的场景用第一种方案;详情页的零散信息补全可以用第二种方案。这个取舍原则在整个项目里都适用。
4. 后端实现要点:从登录鉴权到文件上传
4.1 JWT登录鉴权的完整流程
登录鉴权功能是这个项目后端最值得仔细研究的部分,因为它不是一个孤立的点,而是牵涉到拦截器、全局异常处理、前端路由守卫等多个环节。
整个流程是这样的:
- 用户提交用户名和密码到
/api/user/login接口。 - 后端根据用户名从数据库查出用户记录,用BCrypt校验密码是否匹配。
- 校验通过后,用JWT工具类生成一个token,token的payload中包含userId和role,再用一个密钥签名。
- 后端返回给前端的数据结构是:
{ code: 200, token: "xxx", userInfo: { id, username, nickname, avatar, role } }。 - 前端把token存入localStorage,用户信息存入Pinia。后续所有请求都在axios请求拦截器里统一加上Authorization请求头。
- 后端定义一个TokenInterceptor,配置在WebMvcConfigurer里,指定哪些路径需要拦截、哪些路径放行(比如登录接口和宠物浏览接口通常放行)。
- 拦截器里解析token,如果token不存在或已过期,直接返回401状态码;如果合法,把userId放入请求上下文(比如用ThreadLocal或request attribute),供后续Controller使用。
这里的核心心得是:token的过期时间不要太长也不要太短,一般7天比较合适。过期时间太短会让用户体验很痛苦,经常登录;太长则存在安全隐患。另外,JWT虽然叫“无状态”,但如果你想要在用户修改密码或被封禁后让已签发的token立即失效,就得引入token黑名单机制——这是进阶话题,初次做项目不一定要做,但脑子里要知道这个限制。
4.2 统一返回结果与全局异常处理
很多初学者写接口时,一个接口返回一个Map,另一个接口返回一个对象,第三个接口直接返回字符串。这样看起来能跑,但前端对接时非常痛苦,因为没有一个统一的格式约定。
标准做法是定义一个Result泛型类,所有接口统一返回它:
public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String msg; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { return new Result<>(200, "操作成功", data); } public static <T> Result<T> error(String msg) { return new Result<>(500, msg, null); } }配合全局异常处理器@RestControllerAdvice,业务代码里只需要正常写逻辑、抛异常,所有非预期情况都会被统一捕获并转换为约定的JSON格式返回给前端。这样做的好处是:前端axios拦截器只需要判断res.data.code就能知道请求是否成功,不需要在每个页面里做各种繁琐的错误处理。
4.3 文件上传的两种落地方式
宠物领养系统必然涉及图片上传,这是很多新手卡住的地方。上传文件的核心逻辑并不复杂:前端把文件通过multipart/form-data格式POST到后端,后端用MultipartFile接收,然后把文件写到服务器本地目录,最后返回可访问的URL给前端。
一个标准的图片上传接口实现:
@PostMapping("/api/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 1. 生成唯一文件名,避免重名覆盖 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; // 2. 确定存储目录,按日期分文件夹,方便后期管理 String datePath = LocalDate.now().toString(); File dir = new File(uploadPath + "/" + datePath); if (!dir.exists()) { dir.mkdirs(); } // 3. 保存文件 File dest = new File(dir, fileName); file.transferTo(dest); // 4. 返回可访问的URL String url = "/uploads/" + datePath + "/" + fileName; return Result.success(url); }这里有几个必须注意的坑:
- 文件后缀白名单校验一定要做。只允许jpg、jpeg、png、gif、webp等常见图片格式。如果不过滤,用户上传了一个jsp或exe文件,然后又恰好部署在Tomcat的webapps目录下,后果是灾难性的。
- 文件大小要限制。SpringBoot的
spring.servlet.multipart.max-file-size默认是1MB,对宠物照片来说可能偏小,建议调大到5MB。具体场景可以灵活调,但一定要有上限,防止有人上传超大垃圾文件把磁盘搞满。 - 图片访问路径要配置静态资源映射,否则Vue页面无法直接访问你上传到本地目录的图片。在SpringBoot里只需要在配置类里加一句
addResourceHandlers("/uploads/**").addResourceLocations("file:" + uploadPath + "/")即可。如果依赖Nginx部署,则用Nginx的location /uploads/ { alias /xxx/uploads/; }来映射。 - 如果你想把图片上传做得更专业,可以引入云存储服务,但初学者阶段用本地存储完全够用。等部署上线时,再把本地上传路径替换成云存储,代码改动量其实不大,因为对前端来说URL没有变化。
4.4 MyBatis-Plus的高效用法
MyBatis-Plus在这个项目里能极大提升开发效率,我建议重点关注这几个特性:
- BaseMapper内置方法:
selectById、selectList、selectPage、insert、updateById、deleteById,覆盖了绝大多数单表操作。宠物管理模块基本上靠这些内置方法就能完成,你几乎不需要在Mapper XML里写一行SQL。 - LambdaQueryWrapper条件构造器:这是动态条件查询的利器。例如宠物列表的筛选条件可能有类型、性别、城市、状态,前端不一定每次都会传全部参数,用Wrapper的
eq方法可以根据条件自动拼SQL,代码可读性比手写SQL拼接要好得多:
LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(petType)) { wrapper.eq(Pet::getType, petType); } if (StringUtils.isNotBlank(city)) { wrapper.eq(Pet::getCity, city); } wrapper.eq(Pet::getStatus, 0); // 只展示待领养的宠物 wrapper.orderByDesc(Pet::getCreateTime); Page<Pet> page = petMapper.selectPage(new Page<>(current, size), wrapper);- 分页插件:需要配置一个PaginationInnerInterceptor,配置之后就正常使用
selectPage方法,它会在底层自动拦截SQL并生成LIMIT语句。初次使用的时候容易漏掉这个配置,导致分页不生效,这里提醒一下。 - 逻辑删除:给pet表和apply表加上一个
deleted字段,并用@TableLogic注解标注。这样执行delete操作时,MyBatis-Plus会自动把它变成UPDATE ... SET deleted=1,数据不会真的被物理删除。这个设计在正式项目里几乎是标配,防止运营人员误操作导致数据无法恢复。
记住一点:MyBatis-Plus再怎么强大,也只是单表操作的强大。涉及复杂多表联查、需要精细控制SQL性能的场景,还是要老老实实写XML。把它当成“80%简单场景的快捷方式,20%复杂场景的忠实执行者”来用,你就能在这个项目里游刃有余。
5. 前端Vue3实现:页面搭建与接口对接
5.1 Vue3工程结构与页面路由设计
前端工程基于Vue3 + Vite搭建,UI组件库使用Element Plus。工程结构按“页面功能”划分,核心的页面路由大致如下:
| 路由路径 | 页面 | 角色 |
|---|---|---|
| / | 首页/宠物列表 | 所有人 |
| /pet/:id | 宠物详情 | 所有人 |
| /login | 登录页 | 所有人 |
| /register | 注册页 | 所有人 |
| /user/profile | 个人中心 | 普通用户 |
| /user/myApply | 我的申请列表 | 普通用户 |
| /user/favorites | 我的收藏 | 普通用户 |
| /admin | 后台管理首页(数据统计) | 管理员 |
| /admin/pet | 宠物管理 | 管理员 |
| /admin/apply | 领养审核 | 管理员 |
| /admin/user | 用户管理 | 管理员 |
| /admin/notice | 公告管理 | 管理员 |
这里有一个关键设计:路由守卫。Vue Router的beforeEach导航守卫可以用来实现前端层面的访问控制。比如,访问/admin下的任何页面,都要先判断当前用户的角色是不是管理员,如果不是,直接redirect到登录页或首页。这个配合后端拦截器做“双保险”——前端守的是用户体验,后端守的是数据安全。
5.2 API请求封装是前端工程的核心地基
如果后端接口设计得好,前端开发70%的时间其实是花在页面布局和数据绑定上。而接口调用这块,一定要在项目一开始就做好封装,否则后面会乱成一锅粥。
首先创建一个request.js,在里面初始化axios实例,配置baseURL、超时时间,并加上请求拦截器和响应拦截器:
import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/store/user' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带token request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截器:统一处理业务状态码 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } else if (res.code === 401) { ElMessage.error('登录状态已过期,请重新登录') // 跳转到登录页 window.location.href = '/login' return Promise.reject(new Error('未登录')) } else { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request然后把每个模块的接口调用单独封装成一个API文件,例如src/api/pet.js:
import request from '@/utils/request' export function getPetList(params) { return request.get('/pet/page', { params }) } export function getPetDetail(id) { return request.get(`/pet/${id}`) } export function applyAdopt(data) { return request.post('/pet/apply', data) } export function uploadImage(file) { const formData = new FormData() formData.append('file', file) return request.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) }这种“每个页面对应一套API函数”的模式,极大提高了接口的复用性和可维护性。后续如果接口路径变了,或者接口参数调整了,只需要改一个文件,全站的调用都会同步更新。
5.3 列表查询与分页:前端最常见的三种业务场景
前端的列表查询,看起来很简单,实际写起来有几个容易忽略的细节。我把常见的三种场景拆开来讲:
场景一:宠物列表页,带筛选条件和分页。
这种页面通常有一个筛选区(下拉框、输入框、搜索按钮)和一个表格/卡片列表区,下面还有一个分页组件。交互逻辑是:搜索时重置页码为1,翻页时重新请求数据。前端代码逻辑大致是:
const queryParams = reactive({ current: 1, size: 12, type: '', city: '', keyword: '' }) const petList = ref([]) const total = ref(0) async function loadPets() { const data = await getPetList(queryParams) petList.value = data.records total.value = data.total } function handleSearch() { queryParams.current = 1 loadPets() } function handlePageChange(page) { queryParams.current = page loadPets() }场景二:领养申请提交后,前端如何反馈状态。
申请表单提交成功,要给用户一个明确反馈,同时清空表单、禁用按钮防止重复提交。很多新人不做“按钮loading状态”,结果用户手快点了好几遍,生成了多条重复申请记录。所以提交按钮一定要在请求期间禁用,并显示“提交中...”,等请求结束后再恢复。这个细节虽然简单,但直接关系到数据质量。
场景三:图片上传组件。
在管理后台发布宠物时,需要支持上传多张图片并预览。前端使用Element Plus的el-upload组件,配置action指向后台上传接口,设置headers带token,通过on-success回调拿到后端返回的URL,再把URL追加到一个数组里。这里的关键是,el-upload默认支持一次上传多张,控制好limit属性(比如最多5张),图片数量超出后自动隐藏上传入口。
6. 关键功能实现:领养流程与业务状态流转
6.1 从“申请领养”到“领养成功”的完整业务链路
这个系统最有业务价值的功能,就是领养申请的状态流转。很多学生项目把领养功能做成“用户点击申请 → 申请表插入一条记录 → 完”,这其实是不完整的。一个真正的领养流程,至少应该包含以下几个关键节点,并且每个节点都要有对应的逻辑判断和状态变更:
第一步:提交申请。用户进入宠物详情页,点击“申请领养”按钮,弹出申请表。用户填写领养理由、居住情况、养宠经验等信息后提交。
后端接收请求时,不能傻乎乎地直接插入申请记录,而要做至少三个校验:
- 用户是否已登录(这个交给拦截器处理)。
- 这条宠物记录是否存在,status是否为“待领养”。
- 这个用户是否已经申请过这只宠物,如果存在“待审核”的申请记录,应该提示“您已申请过该宠物,请耐心等待审核”。
第三步的校验尤其重要。如果不判断,用户可以疯狂点击提交生成大量重复申请,管理员审核时会非常崩溃。
第二步:状态变更。申请提交成功时,把宠物的status从“待领养”改为“已申请”,避免其他用户继续提交申请。但这里要考虑一个问题:如果审核拒绝了,宠物状态要恢复成“待领养”,方便其他人继续申请。
第三步:管理员审核。管理员在后台看到申请列表,点击“通过”时,后端要做的事务性操作是:
- 更新这条申请记录的状态为“已通过”。
- 把对应宠物的状态改为“已领养”。
- 拒绝同一宠物下的其他待审核申请,把它们的状态改为“已拒绝”。
第三步这个逻辑很有讲究,用代码表示大致是:
@Transactional public void approveApply(Long applyId) { // 1. 查出申请记录 Apply apply = applyMapper.selectById(applyId); if (apply == null || apply.getStatus() != 0) { throw new BusinessException("申请不存在或已处理"); } // 2. 更新申请状态 apply.setStatus(1); applyMapper.updateById(apply); // 3. 更新宠物状态 Pet pet = petMapper.selectById(apply.getPetId()); pet.setStatus(2); // 已领养 petMapper.updateById(pet); // 4. 拒绝该宠物的其他待审核申请 LambdaQueryWrapper<Apply> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Apply::getPetId, apply.getPetId()) .eq(Apply::getStatus, 0) .ne(Apply::getId, applyId); List<Apply> otherApplies = applyMapper.selectList(wrapper); for (Apply other : otherApplies) { other.setStatus(2); // 已拒绝 applyMapper.updateById(other); } }@Transactional注解必须加上,因为这里涉及多张表的更新。任何一步失败,整个事务都会回滚,不会出现“申请通过了但宠物状态没变”这种数据不一致的脏状态。这个事务设计的完整程度,直接决定这个项目的“业务完成度”。
第四步:用户查看结果。用户在“我的申请”列表中看到审核状态,审核通过的可以联系管理员进行线下交接。如果管理员有闲心,还可以在用户确认收到宠物后,把申请状态更新为“已完成”,这就构成了完整闭环。
6.2 宠物上下架与推荐排序的实现技巧
宠物上下架本质上是修改pet表的status字段。status可以设计成多种值:0待领养,1已申请,2已领养,3已下架。管理员在宠物管理页面,可以通过一个下拉框筛选不同状态的宠物,也可以一键下架某条信息。
这里想多聊两句列表排序。宠物列表的默认排序规则,很多项目都是简单按创建时间倒序。但更好的做法是“综合排序”:优先展示新发布的、或者剩余待领养时间短的宠物。实现上不一定复杂,比如在SQL里按create_time desc排序即可。如果日后有运营需求,想给某些宠物加“置顶”标签,可以在pet表加一个is_top字段,排序时ORDER BY is_top DESC, create_time DESC。这种小而实用的设计,面试时能主动讲出来会很加分。
6.3 公告模块与留言模块的轻量实现
公告模块和管理端的用户管理一样,本质是单表的增删改查。它有一个细节:用户端首页或者宠物列表页顶部,需要展示“最新公告”,所以后端要提供一个“查询最新N条公告”的接口。实现起来就是按时间倒序查出指定条数,不存在难点。
留言模块做一个“内容合法性过滤”更完善。用户留言时,如果留言内容包含敏感词,直接拒绝提交或自动替换成星号。这个可以做前端简单校验,也可以在后端维护一个敏感词列表。敏感词检测本身是个庞大的课题,但在这个项目里做到“有总比没有强”即可,不追求全面覆盖。
7. 常见问题排查与实操避坑记录
7.1 环境配置阶段的经典坑
我见过太多同学在这个阶段卡住,下面这几条是我实测下来最容易踩的:
- MySQL8.0的认证插件问题。MySQL8.0默认使用
caching_sha2_password认证,而一些老版本的驱动(比如5.1.x)不支持,会导致连接报错。解决办法是:确保应用使用mysql-connector-java8.0.x及以上的驱动;如果仍然报错,可以在MySQL里把用户的认证插件改回mysql_native_password。不过更推荐的还是升级驱动,因为从8.0.34版本开始,官方把驱动包名已经改成了com.mysql:mysql-connector-j,配置时注意别引错了。 - 时区问题。数据库连接串一定带上
serverTimezone=Asia/Shanghai,否则你存进去的时间可能比本地时间少8个小时。这个问题排查起来最恶心,因为它不属于“报错”范畴,而是“明明插入了时间,却差8小时”。 - 端口冲突。SpringBoot默认8080端口,如果你本机的8080被占用,启动会直接失败。解决办法是改application.yml里的端口号,或者用
--server.port=8081参数启动。
application.yml里的核心配置长这样,写的时候可以直接参考:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_adoption?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 07.2 前后端联调阶段的跨域问题
前后端分离项目,前端运行在Vite开发服务器的5173端口,后端运行在8080端口,前端发请求时会出现跨域。跨域的本质是浏览器同源策略的限制:只要协议、域名、端口有一个不同,就视为跨域。
解决办法有三种:
- 后端配置CORS:在SpringBoot里写配置类,允许指定来源跨域。开发阶段可以配置
allowedOriginPatterns("*"),上线前再收紧。 - 前端使用Vite代理:在
vite.config.js里配置server.proxy,把前端请求代理到后端地址。这样做的好处是前端请求的相对路径不用改,部署后也方便用Nginx统一转发。 - 前后端都部署在同一个域下面:生产环境常见方案,前端打包后放在Nginx静态目录,Nginx把
/api/前缀的请求反代到后端服务,浏览器就不会跨域了。
开发阶段我建议直接用第一种方案,简单直接;部署阶段用第三种,干净规范。
7.3 数据库层面的常见问题
分页查询是列表页最常用的功能。如果你用MyBatis-Plus的selectPage但是忘了配置PaginationInnerInterceptor拦截器,你会发现分页不生效,或者直接查出全部数据。我建议把它写在MyBatisPlusConfig类里,代码是:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }还有一个高频问题是数据库中有延迟关联字段,比如申请列表里需要显示宠物名,你可能会在Apply实体类里额外加一个petName字段。注意这个字段并不是表里的真实字段,要记得在字段上标注@TableField(exist = false),告诉MyBatis-Plus“这个字段不需要映射到数据库”。漏掉这个注解的结果是:执行MyBatis-Plus的插入或更新方法时,它会尝试去操作pet_name这一列,然后报“Field 'pet_name' doesn't have a default value”或“Unknown column”之类的错,排查起来挺绕的。
7.4 部署上线阶段的注意点
如果你打算把这个项目部署到服务器上,而不是停留在本地运行,有几个细节值得注意:
- 前端打包:在Vue项目根目录执行
npm run build,会在dist/目录生成静态文件。把这些文件上传到服务器的Nginx的html目录下即可。 - 后端打包:SpringBoot项目用
mvn clean package打成jar包,通过java -jar命令启动。服务器上一定要用nohup或者systemd来守护进程,防止终端一关服务就停了。 - 数据库迁移:本地MySQL和服务器MySQL版本尽量保持一致。最稳妥的做法是本地用
mysqldump导出SQL文件,服务器上导入,而不是手动重建表。 - 配置文件分离:生产环境的数据库密码、文件上传路径应该和本地不同。SpringBoot支持
application-prod.yml和application-dev.yml分环境配置,启动时通过--spring.profiles.active=prod指定环境。这是企业级项目的标准做法,自己也养成这个习惯,面试时多一个可讲的亮点。
8. 如何把这个项目真正变成你的项目
源码可以下载,但项目经验抄不走。如果你只是想“跑起来交个毕设”,那这篇拆解到这里你可以直接收藏去操作了。但如果你想在面试时能对答如流,把“宠物领养系统”讲成一个有趣的作品,我建议你至少做这几件事:
第一,通读核心代码,理解每一条业务规则为什么要这么设计。比如领养审核时的“拒绝对应宠物的其他待审核申请”逻辑,如果你能主动讲出“这是为了避免一个宠物被多次成功申请,导致数据混乱”,面试官的印象分会完全不同。
第二,在原有功能上做至少一个独立的小扩展。比如加一个“领养回访记录表”,让管理员可以登记领养后的电话回访情况;或者加一个“宠物疫苗记录”模块。扩展功能不需要多复杂,但要让面试官看到你有独立的分析设计能力。我自己辅导过的朋友里,凡是做了这个动作的,面试质量都有明显提升。
第三,把项目部署到一台真实的云服务器上,写一份简单的部署文档。这个动作看起来不起眼,但它体现的是“从开发到交付”的完整链路意识。很多Java开发干了两年,依然搞不清楚Nginx、Linux、进程守护这些事,你如果在毕业前就搞定了,起步会比同龄人快很多。
第四,优化一些明显可以更完善的体验细节。比如宠物列表页的图片懒加载,可以顺手支持;比如申请被拒绝时,给用户展示拒绝原因;比如用户注册时,做一次邮箱或手机号格式校验。这些细节肉眼可见地提升系统“完成度”,比你在简历上多写一行“精通XX框架”有说服力得多。
在我自己做这类项目的经验里,最大的体会是:一个人能能从零把一个前后端分离系统完整落地,最稀缺的能力不是“会写某段代码”,而是“在面对一堆零散需求时,能设计出清晰的结构,把每个模块串起来,并且让系统里的数据自洽”。宠物领养系统虽然不算复杂,但它恰好覆盖了“完整业务闭环必须经历的所有环节”——登录鉴权、多角色权限、图片文件处理、复杂状态流转、列表查询分页、后台管理面板。这一套走下来,你对Java Web开发的理解会从“一个个零散的知识点”变成“一整条清晰的业务链路”。
最后再分享一个写代码以外的习惯:每当你写完一个模块,把数据库表、核心接口、关键页面截个图,整理成一份简单的项目笔记。做毕设也好,做个人作品也好,这些笔记会在你写简历、准备面试、跟别人介绍项目时,成为你最省力的素材库。哪怕毕业多年后回看,也会是一份很扎实的成长记录。