JavaWeb宠物救助领养平台源码解析:从表设计到前后端交互
2026/9/14 3:23:53 网站建设 项目流程

简介:一套完整的前后端分离宠物救助及领养平台源码,基于Java与Spring Boot开发,结合MySQL存储数据,面向高校计算机专业学生毕业设计或课程设计场景。平台分为管理员与救助者两类角色,管理员拥有用户管理、救助者管理、宠物种类管理、流浪动物管理、领养信息管理、救助信息管理、论坛管理等模块;救助者可管理流浪动物、领养信息、救助信息,并查看个人资料与系统设置。压缩包共676个文件,容量约36.64MB,除Java后端、Vue前端、JavaScript、CSS等代码文件外,还包含SQL脚本、数据库说明文档、部署指导文档、答辩PPT以及一键启动脚本,导入开发工具后即可运行。目前已有85人学习下载,配套文档覆盖从环境配置到项目部署的全过程,对于需要掌握Spring Boot整合开发、理解前后端交互流程的学习者具有切实参考价值。

1. 宠物救助领养平台的骨架:一张状态表胜过十张业务表

我拿到这类基于 JavaWeb 的宠物救助及领养平台源码时,第一件事不是打开 IDE 读代码,而是先把角色理清楚:谁会登录这套系统,他进来能做什么。一个救助站通常有三类人——录入流浪动物的志愿者、想领养宠物的普通用户、负责审核和上架内容的管理员。这三类人落在系统里就是三张表、两个菜单、一条状态机。救助平台这类项目的技术标杆并不高,但业务上有一个关键设计容易被初学者绕晕:宠物不是一成不变的「在架商品」,它有一条生命周期——待审核、可领养、已被申请、领养成功。这一条生命周期贯穿了 MySQL 表设计、Servlet 接口参数和前端按钮显隐,也是你拿到源码后能否快速改造成自己项目的分水岭。适合把 JavaWeb 当毕设或练手项目的工程师,也适合刚接手一套旧 JSP 项目的人用来理解「前后端不分离时代」的系统是怎么组织权限和数据流的。

2. MySQL 表结构:宠物状态、领养申请和角色权限的落库方式

2.1 用户表不只是存账号密码,角色字段决定菜单渲染

宠物救助平台的用户体系一般不采用 Spring Security 那套权限模型,而是用最朴素的方式:user表里放一个role字段,0 代表普通用户,1 代表志愿者,2 代表管理员。这样设计的好处是查询菜单权限时少做一次联表,登录后把一个User对象塞进session,JSP 页面上用<c:if test="${sessionScope.user.role == 2}">就能控制「审核按钮」是否渲染。

建表时的字段顺序也有讲究。业务字段放前面,状态字段放中间,时间字段统一放最后。密码字段必须设计成varchar(64),因为 MD5 摘要长度固定是 32 位,但后续如果要升级成 SHA-256 就得 64 位,一步到位能省很多迁移成本。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `phone` VARCHAR(11) NOT NULL COMMENT '登录手机号', `password` VARCHAR(64) NOT NULL COMMENT 'MD5加密密码', `nickname` VARCHAR(32) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像图片路径', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0普通用户 1志愿者 2管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

这条建表语句里值得抄下来的是两个细节:手机号建了唯一索引,避免同一个人注册两个账号;create_timeDEFAULT CURRENT_TIMESTAMP而不是在 Java 代码里new Date()写入,这样数据导入时能少写一行INSERT语句。密码字段注释里写明「MD5 加密」,防止后来维护的人误以为是明文。

2.2 宠物表用 status 字段表达完整的救助周期

宠物表的字段设计是整个数据库的核心,也是一眼看出源码质量高低的地方。很多初学者会把「是否被领养」设计成一个is_adopted的布尔字段,但实际业务中宠物要经历救助录入、等待体检、审核通过、领养人申请、回访确认等多个环节。单个布尔字段根本表达不了「待审核但已被人看中」这种中间状态。

我一般建议设计成一个status字段,枚举值为 0 待审核、1 可领养、2 领养申请中、3 领养成功、4 已下架。枚举值从 0 开始,方便接口返回后前端直接用数组下标映射文字,不需要在 Java 里写一堆 if-else。

CREATE TABLE `pet` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL COMMENT '宠物昵称', `species` VARCHAR(16) NOT NULL COMMENT '猫/狗/其他', `breed` VARCHAR(32) DEFAULT NULL COMMENT '品种', `age_month` INT DEFAULT NULL COMMENT '月龄', `gender` TINYINT DEFAULT 0 COMMENT '0未知 1公 2母', `source_type` TINYINT NOT NULL DEFAULT 0 COMMENT '0街头救助 1主人送养', `health_state` TINYINT DEFAULT 0 COMMENT '0待体检 1健康 2治疗中', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1可领养 2申请中 3已领养 4已下架', `cover_url` VARCHAR(255) DEFAULT NULL COMMENT '封面图', `description` TEXT COMMENT '救助故事或性格描述', `create_by` INT NOT NULL COMMENT '录入人ID,关联user表', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status_time` (`status`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表';

这里最值得琢磨的是create_by字段。它记录了是哪位志愿者录入的这条宠物信息,后端的PetService.addPet()方法要从 session 里取出当前登录用户 ID 填进来,这样管理员在后台审核时能看到「谁提交的」。update_time用了ON UPDATE CURRENT_TIMESTAMP,每次 UPDATE 语句执行后 MySQL 会自动刷新时间,省掉了手动维护字段的重复代码。

2.3 领养申请表:申请记录和宠物状态要分开判断

领养申请是整个平台里最有业务复杂度的一张表。用户点击「申请领养」时,前端弹窗让他填家庭住址和养宠经验,后端接到的参数却不止这些——后端还要记录pet_iduser_id和当前时间。关键设计在于:申请记录本身有一个check_status字段,但真正决定「这只宠物还能不能被别人申请」的,是pet表里的status字段。

CREATE TABLE `adopt_apply` ( `id` INT NOT NULL AUTO_INCREMENT, `pet_id` INT NOT NULL COMMENT '宠物ID', `user_id` INT NOT NULL COMMENT '申请人ID', `reason` VARCHAR(500) DEFAULT NULL COMMENT '申请理由', `address` VARCHAR(255) DEFAULT NULL COMMENT '领养地址', `check_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2拒绝', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_pet_status` (`pet_id`, `check_status`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='领养申请表';

判断逻辑应该是这样的:用户申请领养时,系统先UPDATE pet SET status=2 WHERE id=? AND status=1,如果影响的行数等于 1,说明宠物确实还在可领养状态,此时才允许插入申请记录。如果用户在页面停留了很久才提交,宠物可能已经被别人申请走了,那么 UPDATE 影响行数为 0,后端直接返回「手慢了一步」。这套操作避免了「先查再插」两步之间别人抢先更新的竞态问题。

2.4 索引策略:组合索引比单列索引更适合状态筛选

宠物列表页最常见的查询条件是「看所有可领养的猫,按发布时间排序」,SQL 大概是SELECT * FROM pet WHERE species='猫' AND status=1 ORDER BY create_time DESCidx_status_time这个组合索引能同时覆盖status等值过滤和create_time排序,比单独给 status 建索引再 filesort 要快。领养申请表的idx_pet_status也是同理,管理员审核时先按 pet_id 过滤出这条宠物下的所有申请,再按 check_status 筛选。

要注意的是,MySQL 8.0 默认的存储引擎是 InnoDB,字符集建议统一成utf8mb4。原因有两个:一是utf8mb4支持 emoji 表情,用户在前端输入宠物昵称时可能会带「🐱」字符,utf8会报Incorrect string value错误;二是与 Java 后端的 JDBC URL 里的characterEncoding=utf-8对齐,避免服务器返回的数据在控制台显示成问号。

3. 后端分层与 Servlet 实现:PetServlet 分发器与权限过滤器

3.1 三层结构不要照搬 SSM,Servlet 直接写 SQL 够用但要分层

这套平台的 JavaWeb 后端,常见做法是Servlet + JSP + JDBC,不引入 Spring 容器。代码组织上分三层:com.example.pet.servlet放控制器,com.example.pet.service放业务逻辑,com.example.pet.dao放数据库访问。分层的作用不是为了让类变多显得高级,而是当你要把数据查询从 MySQL 换成 Redis 缓存时,只需要改 dao 层实现,PetServlet 里的代码一行都不用动。

包结构长这样:

src/main/java ├── com.example.pet │ ├── filter │ │ ├── EncodingFilter.java │ │ └── AuthFilter.java │ ├── servlet │ │ ├── PetServlet.java │ │ ├── UserServlet.java │ │ └── AdoptServlet.java │ ├── service │ │ ├── PetService.java │ │ └── AdoptService.java │ └── dao │ ├── PetDao.java │ ├── AdoptDao.java │ └── DBUtil.java

PetServlet 只做三件事:从 request 里取参数、调用 service、把结果转发给 JSP。不要在 Servlet 里写ResultSet rs = stmt.executeQuery()这种代码——我给你写一个反例,你拿到手就知道看到哪种代码要立刻重构:如果 PetServlet 里同时出现了try-catchClass.forName("com.mysql.cj.jdbc.Driver")while(rs.next()),那这个项目的 dao 层和 servlet 层已经耦合死了,后续任何一处 SQL 改动都要动控制器。

3.2 PetServlet 按 action 参数分发,替代一堆小 Servlet

三个功能接口的路径设计讲究,我给的方案是「一个实体一个 Servlet 入口」,用action参数区分操作。这样 web.xml 里少配置十几个<servlet-mapping>,而且代码里 switch-case 的跳转逻辑一目了然。

@WebServlet("/pet/*") public class PetServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) { resp.sendRedirect("pet/list.jsp"); return; } switch (action) { case "list": // 分页查询宠物列表 listPets(req, resp); break; case "add": // 志愿者录入宠物 addPet(req, resp); break; case "audit": // 管理员审核宠物 auditPet(req, resp); break; case "detail": // 查看宠物详情 showDetail(req, resp); break; default: resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); doGet(req, resp); } }

这里的核心技巧是doPost里先设置编码再直接调用doGet,不要复制一份逻辑去处理 POST 请求。@WebServlet("/pet/*")用路径通配符把模块入口统一,访问pet?action=list&page=1就能调对应方法,比在 web.xml 里逐个维护清爽得多。在 doGet 方法的分发里,每个 case 调用的listPets等私有方法是实际处理请求的模块,它们内部从 request 取参数、调 service、存数据到 request attribute、再req.getRequestDispatcher("list.jsp").forward()

3.3 管理员的权限控制:把拦截逻辑放到 Filter 里而不是每个 Servlet

管理员的几个操作——审核宠物、删除评论、管理用户——都需要校验当前登录人的 role 是否为 2。如果在每个 Servlet 里都写一遍User user = (User) session.getAttribute("loginUser")判空再判断 role,代码会重复到没法看。正确做法是写一个AuthFilter,用路径前缀区分权限。

@WebFilter("/admin/*") public class AdminAuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; User loginUser = (User) req.getSession().getAttribute("loginUser"); if (loginUser == null || loginUser.getRole() != 2) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

@WebFilter("/admin/*")注解替代 web.xml 配置,是 JavaWeb 项目从 Servlet 3.0 开始的推荐做法。普通用户的接口路径不加/admin前缀,比如pet?action=apply就只检查登录状态、不检查角色,这样志愿者也能访问申请领养接口,管理员功能被严格锁在/admin/路径下。登录用户为空时直接重定向到登录页,这在调试时能很快发现「为什么我访问这个页面又跳回去了」的原因——先看路径是否被过滤器拦住。

3.4 领养申请接口和前端轮询:用状态码区分业务错误

AdoptServlet 里处理申请领养请求的接口只干两件事:判断pet.status是否等于 1,然后插入一条申请记录。和 2.3 节说的一致,不能先SELECT再决定是否INSERT,要把两步合成一条带条件的 UPDATE。

PetDao petDao = new PetDao(); int updated = petDao.updateStatus(petId, 2, 1); // 参数:目标状态2,期望当前状态1 if (updated == 0) { resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"code\": 1, \"msg\": \"手慢了,这只宠物已被申请\"}"); return; } AdoptDao adoptDao = new AdoptDao(); adoptDao.insert(new AdoptApply(petId, userId, reason, address)); resp.getWriter().write("{\"code\": 0, \"msg\": \"申请成功\"}");

updateStatus方法底层执行的是UPDATE pet SET status=2 WHERE id=? AND status=1,参数里的第三个参数是「期望的当前值」1。观察返回值是int,如果等于 0 说明条件不满足,可能是宠物已经被别人申请走,也可能管理员下架了宠物。这套设计把并发问题挡在数据库层,比在 Java 层加 synchronized 锁可靠得多,毕竟 Tomcat 是多线程处理请求,Java 锁一撤就穿帮。

4. 前端 JSP/JS 交互:登录态、分页请求和审核按钮的渲染方式

4.1 登录态的保持和退出时的一个隐藏坑

这套平台的前后端交互方式,本质上是「后端渲染页面 + 前端发 Ajax 拉数据」的混合模式。首页的宠物列表不放在 JSP 里让后端循环打印,而是通过一个petServlet?action=jsonList接口返回 JSON,前端用 JS 生成卡片 DOM。这种方式在复习期间更好调试——浏览器地址栏直接敲接口地址能看到返回的 JSON 格式,不需要每次改完 JSP 就重启 Tomcat。

登录态的判断正常放在 JSP 顶部的<c:if>标签里,登录前显示「登录/注册」,登录后显示「昵称 + 退出」。需要注意的坑是:退出登录时除了session.invalidate(),还必须用response.sendRedirect(req.getContextPath() + "/index.jsp")重定向到首页,而不是forward转发。转发的话浏览器地址栏还会停留在原来的页面,用户点击刷新按钮后会看到已退出登录的报错页或重复提交表单的提示。

4.2 用 fetch 拉取宠物列表并渲染卡片

列表接口返回的数据格式我固定为{"total": 35, "rows": [{petId, name, coverUrl, statusText}]}statusText由后端直接把枚举值转成中文文案返回,前端不再做 switch 判断。

async function loadPets(page) { const resp = await fetch(`pet?action=jsonList&page=${page}&size=8`); const data = await resp.json(); const container = document.getElementById('petCardList'); container.innerHTML = data.rows.map(item => ` <div class="card"> <img src="${item.coverUrl}" alt="${item.name}" /> <h3>${item.name}</h3> <span>${item.statusText}</span> <button onclick="applyAdopt(${item.petId})">申请领养</button> </div> `).join(''); }

拿到宠物列表后马上要过滤掉前端 XSS 威胁:宠物name字段如果被人填了<img src=x onerror=alert(1)>,这段 HTML 会被浏览器执行。后端在写入 MySQL 时用ESAPI.encoder().encodeForHTML(req.getParameter("name"))做转义,或者前端在拼接字符串之前先做一个替换函数,把<>"'全部替换成实体字符。这个替换函数在前后端分离项目里通常是框架自带能力,但这里手写 JSP 只能自己加。

4.3 分页器的三种实现,推荐后台 limit + 前端页码提交

分页器常见做法是后台LIMIT offset, size,前端把页码当作参数传给接口。页码从 1 开始,后端算 offset 是(page - 1) * size。这里容易出 bug 的地方是首页输入框里填了 0 或负数,后端需要做保护:if (page < 1) page = 1;,否则 MySQL 会报LIMIT -10的语法错误导致接口 500。

前端分页器的渲染不推荐一次把页码全部显示出来,5 页以内的系统可以简单循环输出按钮,超过 5 页就只显示「上一页,当前页,下一页」。原因不是性能问题——宠物平台的宠物数量撑死几百条,性能压力根本不在这——而是按钮太多用户视觉上会疲劳。写这套列表页的时候,把分页器函数抽成一个独立的renderPagination(total, current)函数,后续改排序、改筛选条件时不用碰它。

4.4 处理审核按钮的权限显隐

管理员的审核菜单只有一个入口:宠物卡片右下角如果有「通过/拒绝」两个小按钮,就说明当前用户有权限。这一块 JSP 配合 JSTL 实现,写在列表卡片循环体内部:

<c:if test="${sessionScope.loginUser.role == 2}"> <button onclick="auditPet(${item.petId}, 1)">通过</button> <button onclick="auditPet(${item.petId}, 4)">下架</button> </c:if>

注意审核按钮不是直接暴露给所有用户,而是由后端过滤器判断会话中的角色,前端只是隐藏按钮。如果哪天前端页面缓存了旧 HTML,用户能肉眼看到按钮但点击请求还是会被过滤器拦截。这和 3.3 节说过的「权限落在后端」是同一套逻辑的两层验证,前端隐藏是用户体验,后端拦截才是安全手段。

5. 部署与排错:源码导入 IDEA 的五个固定动作和三个高频报错

这类 zip 源码包导入后最怕的不是代码有 bug,而是环境对不上导致的连环报错。我的固定操作顺序如下:先建数据库并导入 SQL 文件,再改 DB 配置,接着确认 Tomcat 版本,最后启动看日志。顺序不能乱,因为改完数据库配置才能验证 JDBC 是否连通,而 Tomcat 启动报错信息里如果带ClassNotFoundException: com.mysql.jdbc.Driver,说明驱动 jar 还没被加载到 WEB-INF/lib 下。

高频报错有三个。第一个是Unknown database 'pet_shelter'或者Access denied for user 'root'@'localhost',典型原因是建库 SQL 没执行到,或者 db.properties 里的密码和 MySQL 实际密码不一致。用 Navicat 执行.sql文件时要在执行前先手动 CREATE DATABASE,SQL 文件本身通常不含建库语句。第二个报错是日志里出现The server time zone value 'UTC' is unrecognized,这出现于 MySQL 8.0 与 JDBC 驱动版本不匹配。解决方式是 JDBC URL 后追加参数:jdbc:mysql://localhost:3306/pet_shelter?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。第三个是中文乱码,检查顺序是 JSP 文件头部pageEncoding="UTF-8"、web.xml 里配置 CharacterEncodingFilter、MySQL 表字符集是否为 utf8mb4,这三处全部对上后重启 Tomcat、清浏览器缓存再看。

部署验证可以按一条完整流程跑:管理员登录 → 编辑一条宠物状态为待审核 → 切换普通用户录入一条救助信息 → 管理员过审 → 普通用户申请领养 → 管理员同意申请 → 宠物状态变为已领养。每一步走完后查 MySQL 里的pet.statusadopt_apply.check_status字段是否同步变化。这条链跑通了,数据库、后端状态机、前端会话验证这三个最核心的模块就都是好的。剩下要做的就是拿真实数据压一遍 JavaWeb 的典型场景——列表页翻到第十页、图片加载慢、按钮重复点击产生重复申请——这些坑在改造升级项目时迟早会遇到。

本文还有配套的精品资源,点击获取

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

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

立即咨询