资讯详情 Spring Boot流浪宠物领养平台:毕业设计完整实现解析
📅 2026/10/13 20:52:06
最近整理一份2026年的毕业设计项目合集时翻到编号14163的项目我停下来看了许久。项目名称叫“基于Java的流浪宠物领养平台”乍一看好像就是一个宠物信息管理网站实际梳理之后发现它把我认为学生在毕设阶段应该掌握的完整业务闭环全串起来了用户浏览、注册登录、流浪宠物发布、领养申请、后台审核、状态流转、图片上传、公告管理。对准备做Java方向毕设的人来说这个题材有社会温度技术栈又足够干净不会难到做不下去也不是那种一眼就能被答辩老师看穿的玩具项目。这个项目特别适合三类人一是正在为选题发愁的应届毕业生二是学过Java Web但没独立做过完整系统、想快速补齐项目经验的人三是想给简历里加一个“能讲清楚业务”的作品而不是堆功能水字数的同学。这篇内容我会围绕这个项目本身从选题思路、模块拆解、数据库设计、核心功能实现再到部署测试和答辩包装完整说一遍。里面很多细节是我实际改项目时踩过坑后总结出来的真按这路线走能省不少时间。1. 为什么在600套项目里我单独挑出流浪宠物领养平台1.1 它踩中了毕设评审所有加分点毕设答辩一般看三件事项目有没有明确的应用场景技术栈能不能对应这几年学的东西功能是不是一个完整闭环。流浪宠物领养平台三个条件全部满足。先说应用场景这几年流浪动物问题经常出现在公共讨论里很多城市都有民间救助站但领养信息往往靠朋友圈转发效率低信息也容易过期。一个在线平台可以让救助站发布流浪宠物信息让有领养意愿的人直接在线申请这就是一个真实存在的社会需求不是拍脑袋想出来的。再说技术栈基于Java的Spring Boot项目正好覆盖了面向对象、MVC、数据库、权限控制这些教学重点难度又比电商、秒杀这类项目低一节。相比之下我翻到很多“学生管理系统”“图书管理系统”之类的项目功能也不差但答辩老师每年要看几十个类似题目很难有新意。流浪宠物这个方向自带情感属性演示时PPT里放一张救助站的猫咪照片再讲“我们解决了信息不对称问题”整个汇报的感觉完全不一样。当然选这个项目不能只靠情感牌后续的完整度才是关键。1.2 适合什么技术基础的人先说结论这个项目适合“学过Java Web基础、想用Spring Boot做第一个完整系统”的同学不太适合连Java集合都用不熟的新手。因为项目里涉及到Session拦截器、MyBatis操作、事务控制、文件上传映射等内容如果没有Servlet和MySQL基础前期光环境搭建就会卡住很久。如果是学过Java基础但没系统跟过Spring Boot建议先花一周时间把Spring Boot入门、Thymeleaf模板渲染、MyBatis-Plus增删改查这三块过一遍再来看这个项目。如果时间确实紧比如距离答辩只剩两周那我的建议是不要自己从零手写了直接拿现有源码项目做二次开发先把跑通主线流程再根据自己的想法改功能。源码里会涉及一些约定俗成的写法边改边查反而比从零开始更容易理解。1.3 技术栈选择的真实理由这个项目原始版本用的是Spring Boot Thymeleaf MyBatis-Plus MySQL我评估后认为这套组合很合理尤其适合单人毕设。层面推荐方案理由JDK8或11必要时17多数学校机器和老项目兼容性更好JDK8最稳后端框架Spring Boot 2.7.x生态成熟资料多不推荐一上来用Spring Boot 3ORMMyBatis-Plus单表CRUD几乎零成本多表查询写SQL也不复杂模板引擎Thymeleaf后端直接返回页面不需要Node环境打包部署都简单数据库MySQL 5.7或8.0教材覆盖最多排错方便前端Bootstrap jQuery不追求花哨稳定可控同时能塞进真实页面这里很多人会问为什么不用Vue做前后端分离我可以明确说前后端分离需要同时维护两套工程还要处理跨域、Token鉴权、接口联调工作量至少翻倍。毕业后端方向如果有时间可以学但作为本科毕设Thymeleaf渲染的“单体应用”完全够用也更容易在答辩时讲清楚。如果把数据访问层换成JPA也能跑通但MyBatis-Plus对习惯了写SQL的人更友好排查问题更直接。2. 从业务痛点倒推系统模块上手前先画好边界2.1 三个核心角色决定系统边界流浪宠物领养平台里的角色不多但每个角色的操作路径完全不同。第一类角色是普通用户也就是想领养宠物的人他们需要浏览流浪宠物列表、查看宠物详情、收藏有意向的宠物、提交领养申请、在个人中心查看审核进度。第二类角色是救助站工作人员在系统里对应管理员他们负责录入流浪宠物信息、上传图片、审核领养申请、发布公告以及更新宠物状态。第三类角色是超级管理员负责管理普通用户和管理员账号必要时查看全站数据统计。很多新手容易犯的错是把所有功能塞到一个页面里用户和管理员共用一个界面。正确的做法是角色登录后进入不同主界面普通用户看到的是前台门户和个人中心管理员看到的是后台管理面板。前台和后台尽管在同一个Spring Boot工程里但通过路径前缀和拦截器做了清晰隔离。2.2 六大功能模块划分按业务边界项目可以拆成六个核心模块注册登录模块包含普通用户注册、账号密码登录、退出登录、Session管理。首页及宠物展示模块分页展示流浪宠物支持按品种、性别、状态筛选宠物详情页展示多张图片和领养要求。宠物管理模块管理员新增、编辑、下架流浪宠物信息宠物状态由系统自动流转。领养申请模块用户提交申请填写申请理由、联系方式管理员审核并通过或拒绝系统同步更新宠物状态。个人中心模块用户查看自己的申请记录、收藏列表、个人信息修改。后台管理模块管理员维护宠物、审核申请、发布公告、管理用户权限。模块再多本质上就是一个“发布—申请—审核”的状态机。在这个基础上增加公告、收藏等辅助功能业务就完整了。我第一次看这个项目时觉得它不复杂但正是因为模块划分干净代码结构才不乱。2.3 先写清楚接口再写代码我建议动手写代码前先用一张表把后端接口列出来不需要管RESTful规范是否完美但要保证每个界面操作都有对应接口。下面这些是这个项目必须包含的接口功能访问角色POST /register注册新用户游客POST /login登录游客GET /pet/list分页查询宠物列表所有登录用户GET /pet/{id}查看宠物详情所有登录用户POST /pet/add新增流浪宠物管理员POST /pet/update编辑宠物信息管理员POST /apply/submit提交领养申请普通用户GET /apply/my我的申请列表普通用户POST /apply/audit审核领养申请管理员POST /favorite/add收藏宠物普通用户POST /favorite/cancel取消收藏普通用户POST /notice/add发布公告管理员接口清单列出来之后再写Controller就清晰多了。很多同学代码写得混乱是因为心里没有这张表想到哪写到哪最后Controller几百行Service又空荡荡答辩时一问三不知。3. 数据库设计是核心五张表搞定领养闭环3.1 表结构清单项目数据库不复杂六张表就能覆盖所有功能。核心是用户表、宠物表、领养申请表辅助是宠物图片表、收藏表、公告表。为什么图片要单独建表我见过很多人直接在宠物表里放一个images字段用逗号把URL拼起来。这种设计在小项目里看着简单但遇到删除单张图片、统计图片数量、懒加载等情况就会变得很难受。正确的做法是t_pet_image表记录每张图片可以支持一只宠物上传多张照片。另外用逻辑外键而不是物理外键减少后续删除数据的阻力。3.2 核心SQL脚本参考以下是我认为可以直接参考的核心建表语句字段命名尽量简洁但带注释方便答辩维护CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(30) NOT NULL COMMENT 登录名, password VARCHAR(255) NOT NULL COMMENT BCrypt加密密码, nickname VARCHAR(30) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_pet ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(50) NOT NULL COMMENT 宠物名称, category VARCHAR(20) DEFAULT NULL COMMENT 猫/狗/其他, sex TINYINT DEFAULT 0 COMMENT 0未知 1公 2母, age VARCHAR(20) DEFAULT NULL COMMENT 年龄描述如2个月, description TEXT COMMENT 宠物详细介绍, cover_image VARCHAR(255) DEFAULT NULL COMMENT 封面图片, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待领养 1申请中 2已领养, publisher_id BIGINT DEFAULT NULL COMMENT 发布人关联用户表, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_apply ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 申请人ID, pet_id BIGINT NOT NULL COMMENT 宠物ID, reason VARCHAR(500) DEFAULT NULL COMMENT 领养理由, contact VARCHAR(50) DEFAULT NULL COMMENT 联系方式, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME DEFAULT NULL COMMENT 审核时间, audit_remark VARCHAR(255) DEFAULT NULL COMMENT 审核备注, PRIMARY KEY (id), UNIQUE KEY uk_user_pet (user_id, pet_id), KEY idx_pet_id (pet_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;收藏表和公告表也类似收藏表加user_id与pet_id的联合唯一索引避免重复收藏公告表只要title、content、create_time三个字段就够。3.3 状态字段和唯一索引这套表里最重要的概念是“状态流转”。t_pet.status有三种状态0待领养、1申请中、2已领养。t_apply.status有三种状态0待审核、1已通过、2已拒绝。提交领养申请时宠物状态从0变成1表示“有人正在申请”管理员审核通过后宠物状态变为2审核拒绝则宠物回到0继续等待下一任领养人。这个逻辑用一个简单的状态机就能管理但很多同学只是存了一个状态字段没有去维护它的变化导致数据乱成一团。这里有两个容易被忽视的细节。第一t_apply表要加uk_user_pet联合唯一索引防止同一个用户对同一只宠物反复提交申请。有些同学只在Service层判断并发情况下依然可能插入重复数据数据库索引是最后一道防线。第二审核通过之后其他对这个宠物的待审核申请应该统一批量改为“已拒绝”不能让多人同时显示为“通过”状态。这个批量更新逻辑在Service里很容易实现也是答辩时展示你理解业务边界的好例子。4. 关键功能实现讲解登录、发布、申请、审核、收藏4.1 登录与权限控制的简单做法这个项目建议用Session处理登录态不引入Spring Security。不是Security不好是毕设时间有限学习成本高答辩时也不容易说透。自己写一个拦截器就够了。登录成功后再Session里存放当前用户对象包括角色字段。然后定义两个拦截规则静态资源放行注册登录接口放行后台管理路径必须管理员才能访问其余页面只要登录即可。拦截器核心代码大致这样public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }再配合权限校验在用户进入后台前检查role是否为1。用Session比JWT简单的地方在于不需要在代码里解析Token也不需要处理过期时间符合单体应用的业务直觉。4.2 流浪宠物发布与图片上传宠物发布是管理员的职责。页面是一个表单填写宠物基本信息然后上传多张图片。后端使用MultipartFile接收。图片上传有几个关键点。第一保存路径不能写死在代码里最好配置在application.yml中开发和生产环境可以切换。第二文件名用UUID拼接扩展名避免中文名和重名问题。第三要限制文件大小和后缀防止有人传木马文件。第四数据库里存的是相对访问路径比如/upload/2026-01-01/uuid.jpg不是绝对磁盘路径方便前端直接访问。PostMapping(/pet/add) public String addPet(Pet pet, RequestParam(images) MultipartFile[] images, HttpSession session) { // 判断当前session用户是否是管理员 User admin (User) session.getAttribute(loginUser); if (admin null || admin.getRole() ! 1) { throw new RuntimeException(无权限); } pet.setPublisherId(admin.getId()); pet.setStatus(0); petMapper.insert(pet); // 保存上传多图 String dateDir new SimpleDateFormat(yyyy-MM-dd).format(new Date()); for (MultipartFile file : images) { if (file.isEmpty()) continue; String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); String fileName UUID.randomUUID() . ext; File dir new File(uploadDir, dateDir); if (!dir.exists()) dir.mkdirs(); File dest new File(dir, fileName); file.transferTo(dest); // 写入图片表 PetImage image new PetImage(); image.setPetId(pet.getId()); image.setImageUrl(/upload/ dateDir / fileName); petImageMapper.insert(image); } return redirect:/admin/pet/list; }这段代码里的uploadDir是从配置里注入的建议配置成项目根目录以外的绝对路径避免打包后文件丢失。这个坑我在后面部署部分会细说。4.3 领养申请的状态机处理领养申请是整个项目的核心业务。用户看到宠物详情页点击“申请领养”填写原因和联系方式提交后进入审核流程。Service层要保证几个条件宠物必须存在且状态为“待领养”当前用户不能重复申请同一宠物申请成功后宠物状态改为“申请中”。这三件事必须在同一个事务里完成。Transactional public void submitApply(User user, ApplyDTO dto) { Pet pet petMapper.selectById(dto.getPetId()); if (pet null || pet.getStatus() ! 0) { throw new RuntimeException(该宠物当前不可领养); } Apply apply new Apply(); apply.setUserId(user.getId()); apply.setPetId(dto.getPetId()); apply.setReason(dto.getReason()); apply.setContact(dto.getContact()); apply.setStatus(0); applyMapper.insert(apply); pet.setStatus(1); petMapper.updateById(pet); }审核时的Service逻辑也要注意先根据申请ID查出申请记录再更新申请状态同时更新宠物状态。如果审核通过宠物状态改为已领养如果拒绝宠物状态改回待领养。还要把同一宠物下其他的待审核申请批量置为已拒绝。Transactional public void auditApply(Long applyId, Integer status, String remark) { Apply apply applyMapper.selectById(applyId); if (apply null) return; apply.setStatus(status); apply.setAuditRemark(remark); apply.setAuditTime(new Date()); applyMapper.updateById(apply); Pet pet petMapper.selectById(apply.getPetId()); if (pet null) return; if (status 1) { pet.setStatus(2); petMapper.updateById(pet); // 拒绝同一宠物的其他申请 LambdaUpdateWrapperApply wrapper new LambdaUpdateWrapper(); wrapper.eq(Apply::getPetId, pet.getId()) .eq(Apply::getStatus, 0) .set(Apply::getStatus, 2) .set(Apply::getAuditRemark, 宠物已被其他用户领养); applyMapper.update(null, wrapper); } else if (status 2) { pet.setStatus(0); petMapper.updateById(pet); } }这段代码里有事务有状态判断有批量更新面试或答辩时能讲的东西很多。千万别小看这个逻辑很多现成源码写得并不严谨我见过的版本里甚至有人不记得把状态改回“待领养”导致被拒绝的宠物永远无法再申请。4.4 收藏与公告收藏功能比较简单但收藏按钮要有状态切换。用户已经收藏时显示“已收藏”点击取消否则显示“收藏”点击添加。后端先通过count查询是否已存在存在就删除不存在就插入。这里利用t_favorite表的联合唯一索引兜底代码里判断一下即可。公告模块就是一个简单的CRUD管理员在后台发布公告前台首页滚动展示最新几条。可以说这个模块是三段式写的实体类、Mapper接口、一个Service。熟练之后半小时就能写完但对新手来说是非常好的练手材料。5. 部署和测试中的真实踩坑记录四个高频问题5.1 MySQL时区导致的连接报错第一次启动项目大概率会遇到数据库连接失败日志里报时区错误。MySQL 8默认的时区设置和JDBC驱动不一致连接URL必须带参数。这里把我跑通时用的配置贴一下jdbc:mysql://localhost:3306/pet_adopt?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai解决时区报错allowPublicKeyRetrievaltrue解决部分MySQL 8版本的公钥检索错误。如果数据库是MySQL 5.7参数可以少一点但加上也不会有问题。5.2 图片上传后刷新看不到图片这个坑非常典型。在开发环境里许多朋友将图片上传到项目根目录/upload/通过http://localhost:8080/upload/xxx.jpg访问发现404。原因是Spring Boot默认只把classpath:/static/作为静态目录项目根目录下的upload并不在你的web环境里。解决方案是配置一个静态资源映射把外部磁盘路径和/upload/**对应起来。例如在配置类中Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }这里有个细节addResourceLocations(file: uploadDir /)结尾的斜杠必须有不然Spring拼接时会出问题。同时在application.yml中配置upload.dir开发环境可以写成./data/upload/生产环境改成实际绝对路径。5.3 LocalDateTime序列化格式不一致如果接口以JSON格式返回数据前端会收到类似createTime: [2026,1,1,10,30,0]这样的数组这是因为Jackson对Java 8时间类型默认序列化方式不够友好。要在配置文件里统一spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8加上之后接口返回的LocalDateTime就会是字符串。如果用的Thymeleaf渲染页面格式化时间要用#temporals.format(createTime, yyyy-MM-dd HH:mm:ss)别直接用toString()显示效果不一样。5.4 Jar包打包后上传文件经常丢用Spring Boot打成可运行jar包后classpath在jar内部普通的上传路径如果写的是upload/实际目录会出现在启动进程的工作目录位置不固定重启后还可能被覆盖。更麻烦的是打包时不会把动态上传的文件带进jar所以上传完的图片一旦重新部署就会消失。最好的做法就是第5.2节说的外部目录。日志和上传文件都统一放到一个data/目录下然后把upload.dir指向该目录。部署时直接用外部配置覆盖java -jar pet-adopt.jar --upload.dir/opt/pet/data/upload/如果你不想用外部参数还可以在application-prod.yml里写死绝对路径。总之不要依赖相对路径这是我在交付项目前总结出的最重要教训。6. 从“能运行”到“答辩高分”功能扩展和汇报话术6.1 低成本高展示度的三个扩展如果主线流程已经跑通还有时间我强烈建议加几个低成本但展示效果明显的扩展。第一增加首页统计图表。后端写一个统计接口按品种分类统计流浪宠物数量以及每个月的领养成功数量前端引入ECharts画一个柱状图和饼图。这个扩展工作量不大但现场演示时很有冲击力给人的感觉是“项目有数据洞察能力”。第二在宠物详情页加入地图展示。领养平台可以在地图上标记宠物所在的救助站位置。用Leaflet或者简单JS调用地图服务甚至不需要后端支持在详情页放个位置字段就行。这个功能虽然简单但能说明你考虑到线上领养的实际线下交接需求。第三导出领养协议PDF。审核通过后用户可以下载一份领养协议内容包含宠物信息、申请人信息、领养日期用到iText库或POI工具。这只是多写一个controller和模板但答辩时可以讲“无纸化领养流程”直接让项目档次提高一截。6.2 答辩演示时要重点演示的“剧情”这个项目答辩时千万不要从头到尾把每个页面都点一遍要设计一条有逻辑的使用路径。我建议准备两个浏览器窗口一个用普通用户账号登录一个用管理员账号登录。演示步骤是这样的先用普通用户身份登录搜索“橘猫”进入详情页收藏提交申请。然后切到管理员窗口刷新后台待审核列表点开申请信息审核通过。再切回普通用户窗口查看申请进度看到“已通过”。最后展示这个宠物的状态已经变成“已领养”。如果之前做了统计图表顺便切到首页看数据变化这样整条演示链路就完整了。整个过程要边点边说业务逻辑比如“提交申请时我把宠物状态从0改成1是防止别人同时申请同一只宠物”、“管理员通过后我把同一只宠物的其他申请批量改成拒绝保证数据一致”。这种表达比背代码有用得多。6.3 二次开发时容易犯的错很多同学拿到源码后第一步不是看文档而是直接启动。结果无非是数据库没导入、JDK版本不匹配、端口被占。我的建议是拿到项目后先做三件事改数据库账号密码、建库导入SQL脚本、检查application.yml里的配置项。然后把启动日志完整看一遍看到“Started Application”再打开浏览器。测试时准备一套干净的测试数据不要拿别人的业务数据直接点不然列表里全是奇怪的脏数据。另外扩展功能时不要大改原有接口最好新增加一个包路径以增量方式开发。改坏了原功能后回滚也容易。提交代码时记得忽略target目录和data目录这些是编译产物和动态数据不该进Git记录。最后再分享一个我帮人改项目时最常提的建议。很多同学把精力花在“怎么加更多功能”上忽视了“为什么这样设计”。流浪宠物领养平台的重点不在功能多而在于状态流转和数据一致性的设计思路。如果你能把宠物状态从待领养到申请中再到已领养的流转过程讲清楚把为什么给申请加唯一索引讲明白把图片为什么单独建表解释明白答辩分数基本就不会低。这个项目看起来简单但打磨好了完全能成为一份拿得出手的作品。