JavaWeb宠物救助领养平台源码解析:从表设计到前后端交互 简介一套完整的前后端分离宠物救助及领养平台源码基于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对象塞进sessionJSP 页面上用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这条建表语句里值得抄下来的是两个细节手机号建了唯一索引避免同一个人注册两个账号create_time用DEFAULT 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表;这里最值得琢磨的是create_by字段。它记录了是哪位志愿者录入的这条宠物信息后端的PetService.addPet()方法要从 session 里取出当前登录用户 ID 填进来这样管理员在后台审核时能看到「谁提交的」。update_time用了ON UPDATE CURRENT_TIMESTAMP每次 UPDATE 语句执行后 MySQL 会自动刷新时间省掉了手动维护字段的重复代码。2.3 领养申请表申请记录和宠物状态要分开判断领养申请是整个平台里最有业务复杂度的一张表。用户点击「申请领养」时前端弹窗让他填家庭住址和养宠经验后端接到的参数却不止这些——后端还要记录pet_id、user_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表;判断逻辑应该是这样的用户申请领养时系统先UPDATE pet SET status2 WHERE id? AND status1如果影响的行数等于 1说明宠物确实还在可领养状态此时才允许插入申请记录。如果用户在页面停留了很久才提交宠物可能已经被别人申请走了那么 UPDATE 影响行数为 0后端直接返回「手慢了一步」。这套操作避免了「先查再插」两步之间别人抢先更新的竞态问题。2.4 索引策略组合索引比单列索引更适合状态筛选宠物列表页最常见的查询条件是「看所有可领养的猫按发布时间排序」SQL 大概是SELECT * FROM pet WHERE species猫 AND status1 ORDER BY create_time DESC。idx_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 里的characterEncodingutf-8对齐避免服务器返回的数据在控制台显示成问号。3. 后端分层与 Servlet 实现PetServlet 分发器与权限过滤器3.1 三层结构不要照搬 SSMServlet 直接写 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.javaPetServlet 只做三件事从 request 里取参数、调用 service、把结果转发给 JSP。不要在 Servlet 里写ResultSet rs stmt.executeQuery()这种代码——我给你写一个反例你拿到手就知道看到哪种代码要立刻重构如果 PetServlet 里同时出现了try-catch、Class.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?actionlistpage1就能调对应方法比在 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?actionapply就只检查登录状态、不检查角色这样志愿者也能访问申请领养接口管理员功能被严格锁在/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;charsetutf-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 status2 WHERE id? AND status1参数里的第三个参数是「期望的当前值」1。观察返回值是int如果等于 0 说明条件不满足可能是宠物已经被别人申请走也可能管理员下架了宠物。这套设计把并发问题挡在数据库层比在 Java 层加 synchronized 锁可靠得多毕竟 Tomcat 是多线程处理请求Java 锁一撤就穿帮。4. 前端 JSP/JS 交互登录态、分页请求和审核按钮的渲染方式4.1 登录态的保持和退出时的一个隐藏坑这套平台的前后端交互方式本质上是「后端渲染页面 前端发 Ajax 拉数据」的混合模式。首页的宠物列表不放在 JSP 里让后端循环打印而是通过一个petServlet?actionjsonList接口返回 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?actionjsonListpage${page}size8); const data await resp.json(); const container document.getElementById(petCardList); container.innerHTML data.rows.map(item div classcard img src${item.coverUrl} alt${item.name} / h3${item.name}/h3 span${item.statusText}/span button onclickapplyAdopt(${item.petId})申请领养/button /div ).join(); }拿到宠物列表后马上要过滤掉前端 XSS 威胁宠物name字段如果被人填了img srcx onerroralert(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 onclickauditPet(${item.petId}, 1)通过/button button onclickauditPet(${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 rootlocalhost典型原因是建库 SQL 没执行到或者 db.properties 里的密码和 MySQL 实际密码不一致。用 Navicat 执行.sql文件时要在执行前先手动 CREATE DATABASESQL 文件本身通常不含建库语句。第二个报错是日志里出现The server time zone value UTC is unrecognized这出现于 MySQL 8.0 与 JDBC 驱动版本不匹配。解决方式是 JDBC URL 后追加参数jdbc:mysql://localhost:3306/pet_shelter?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。第三个是中文乱码检查顺序是 JSP 文件头部pageEncodingUTF-8、web.xml 里配置 CharacterEncodingFilter、MySQL 表字符集是否为 utf8mb4这三处全部对上后重启 Tomcat、清浏览器缓存再看。部署验证可以按一条完整流程跑管理员登录 → 编辑一条宠物状态为待审核 → 切换普通用户录入一条救助信息 → 管理员过审 → 普通用户申请领养 → 管理员同意申请 → 宠物状态变为已领养。每一步走完后查 MySQL 里的pet.status和adopt_apply.check_status字段是否同步变化。这条链跑通了数据库、后端状态机、前端会话验证这三个最核心的模块就都是好的。剩下要做的就是拿真实数据压一遍 JavaWeb 的典型场景——列表页翻到第十页、图片加载慢、按钮重复点击产生重复申请——这些坑在改造升级项目时迟早会遇到。本文还有配套的精品资源点击获取