简介:本资源为基于Java技术的野生动物保护公益网站毕业设计论文,面向计算机相关专业本科生及需要完成Web项目课程设计的学习者,帮助解决传统手工信息管理中查询耗时长、管理步骤繁琐等问题。压缩包内共1个PDF文件,大小约2.95MB,内容为完整毕业论文,涵盖绪论、B/S三层结构模型、Mysql数据库选择与应用、JSP技术详解、系统权限管理、功能实现、系统性能与稳定性及结论等章节,并附中英文摘要与关键词。论文详细阐述了管理员与普通用户两类权限的设计,包括用户管理、动物及分类信息管理、公益活动发布、留言互动与公益捐赠等模块的实现思路,可作为同类公益网站开发的参考模板。目前已有124人学习下载,适合需要撰写毕业设计、了解B/S架构与JSP开发流程的读者参考借鉴。
1. 野生动物保护公益网站到底要解决什么问题
做公益类网站,最怕的不是技术难,而是做完没人用。我见过太多“基于 Java 的 XX 管理系统”,功能列表写得满满当当,最后连自己都不想打开第二次。野生动物保护公益网站不一样,它的核心诉求非常具体:让公众能快速查到某种动物的保护等级、分布区域和救助渠道,让志愿者能提交目击记录,让管理员能审核并发布救助信息。这三件事决定了它不是一张静态展示页,而是一个有角色、有流程、有数据约束的 B/S 结构 Web 应用。
用 Java 技术栈来做,最直接的理由是生态成熟。JSP + Servlet + MySQL 这套组合虽然被很多人嫌弃“老”,但它的调试链路短、部署门槛低、资料密度高,特别适合课程设计或中小型公益项目快速落地。你不需要引入微服务、消息队列这些重装备,一台 2 核 4G 的云服务器跑 Tomcat 加 MySQL 就够。这篇文章会从数据库设计一路讲到 JSP 页面渲染和常见翻车点,目标是让你照着能跑通一个可演示、可扩展的野生动物保护公益网站。
2. 数据库表结构与 MySQL 8.0 建库实操
2.1 四张核心表怎么划分职责
公益网站的数据模型不需要太复杂,但必须把“动物档案”和“目击记录”分开。我一般会建四张表:animal_species存物种基础信息,sighting_record存公众提交的目击数据,rescue_info存救助站和联系方式,admin_user存后台账号。这样拆的好处是,物种数据由管理员维护,目击记录由前端提交,两者通过species_id关联,查询时不会互相污染。
animal_species的关键字段包括:id、chinese_name、latin_name、protection_level(用枚举值 1/2/3 表示国家一级、二级、三有)、habitat、description、image_url。protection_level建议用TINYINT而不是字符串,方便排序和筛选。sighting_record需要id、species_id、location、sighting_time、reporter_name、reporter_contact、status(0 待审核 / 1 已通过 / 2 已驳回)、create_time。status字段是整个审核流程的开关,后面 JSP 页面会根据它决定显示什么。
CREATE DATABASE wildlife_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE wildlife_db; CREATE TABLE animal_species ( id INT PRIMARY KEY AUTO_INCREMENT, chinese_name VARCHAR(64) NOT NULL, latin_name VARCHAR(128), protection_level TINYINT NOT NULL COMMENT '1一级 2二级 3三有', habitat VARCHAR(255), description TEXT, image_url VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE sighting_record ( id INT PRIMARY KEY AUTO_INCREMENT, species_id INT NOT NULL, location VARCHAR(255) NOT NULL, sighting_time DATETIME NOT NULL, reporter_name VARCHAR(64), reporter_contact VARCHAR(128), status TINYINT DEFAULT 0 COMMENT '0待审核 1通过 2驳回', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_species (species_id), INDEX idx_status (status) ) ENGINE=InnoDB;建表时有两个参数值得注意。字符集用utf8mb4而不是utf8,因为动物名称里可能出现生僻字或emoji。status和protection_level都建了索引,因为后台列表页会频繁按这两个字段过滤。如果你用的是 MySQL 5.7.44 或 8.0.44,上述语法完全兼容,不需要改。
2.2 连接池配置与 JDBC 参数怎么设
JSP 页面里直接DriverManager.getConnection是最省事的写法,但并发一上来就崩。我一般用 Druid 或 HikariCP,这里以 HikariCP 为例,因为它配置项少、默认值合理。把hikari.properties放在src/main/resources下:
jdbcUrl=jdbc:mysql://127.0.0.1:3306/wildlife_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username=wildlife_user password=YourStrongPassword maximumPoolSize=10 minimumIdle=2 connectionTimeout=30000 idleTimeout=600000 maxLifetime=1800000useSSL=false在本地开发时省去证书配置的麻烦,但生产环境建议开启并配置信任库。serverTimezone必须显式指定,否则 MySQL 8.0 驱动会报时区错误。maximumPoolSize设 10 是因为公益网站并发不会太高,设太大反而浪费数据库连接。connectionTimeout30 秒是给慢查询留的缓冲,超过这个时间还没拿到连接,说明池子已经打满,需要排查是否有连接泄漏。
初始化连接池的代码放在ServletContextListener里,应用启动时执行一次:
public class AppContextListener implements ServletContextListener { private HikariDataSource dataSource; @Override public void contextInitialized(ServletContextEvent sce) { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/wildlife_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false"); config.setUsername("wildlife_user"); config.setPassword("YourStrongPassword"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource = new HikariDataSource(config); sce.getServletContext().setAttribute("dataSource", dataSource); } @Override public void contextDestroyed(ServletContextEvent sce) { if (dataSource != null) dataSource.close(); } }把dataSource放进ServletContext后,所有 JSP 和 Servlet 都能通过getServletContext().getAttribute("dataSource")拿到。这样做的好处是连接池全局唯一,不会因为页面刷新反复创建。注意contextDestroyed里必须关闭,否则 Tomcat 重启时会出现连接残留,表现为 MySQL 里一堆Sleep状态的进程。
3. JSP 页面渲染与前后端数据传递
3.1 物种列表页的分页查询与 JSTL 渲染
物种列表是访问量最大的页面,必须做分页。我一般用LIMIT offset, size配合COUNT(*)实现,不引入 PageHelper 这类插件,因为 JSP 项目里手动控制更直观。Servlet 先算总数,再查当前页数据,最后转发到 JSP。
// SpeciesListServlet.java int page = Integer.parseInt(req.getParameter("page") == null ? "1" : req.getParameter("page")); int size = 12; int offset = (page - 1) * size; String countSql = "SELECT COUNT(*) FROM animal_species"; String listSql = "SELECT id, chinese_name, protection_level, habitat, image_url FROM animal_species ORDER BY protection_level ASC, id DESC LIMIT ? OFFSET ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement countPs = conn.prepareStatement(countSql); PreparedStatement listPs = conn.prepareStatement(listSql)) { ResultSet countRs = countPs.executeQuery(); countRs.next(); int total = countRs.getInt(1); int totalPages = (int) Math.ceil(total * 1.0 / size); listPs.setInt(1, size); listPs.setInt(2, offset); ResultSet listRs = listPs.executeQuery(); List<Map<String, Object>> speciesList = new ArrayList<>(); while (listRs.next()) { Map<String, Object> row = new HashMap<>(); row.put("id", listRs.getInt("id")); row.put("chineseName", listRs.getString("chinese_name")); row.put("protectionLevel", listRs.getInt("protection_level")); row.put("habitat", listRs.getString("habitat")); row.put("imageUrl", listRs.getString("image_url")); speciesList.add(row); } req.setAttribute("speciesList", speciesList); req.setAttribute("currentPage", page); req.setAttribute("totalPages", totalPages); req.getRequestDispatcher("/species-list.jsp").forward(req, resp); }JSP 侧用 JSTL 的c:forEach渲染,避免在页面里写 Java 脚本片段。protection_level是数字,展示时需要转成中文,可以用c:choose判断:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <tr><th>名称</th><th>保护等级</th><th>栖息地</th></tr> <c:forEach items="${speciesList}" var="sp"> <tr> <td>${sp.chineseName}</td> <td> <c:choose> <c:when test="${sp.protectionLevel == 1}">国家一级</c:when> <c:when test="${sp.protectionLevel == 2}">国家二级</c:when> <c:otherwise>三有动物</c:otherwise> </c:choose> </td> <td>${sp.habitat}</td> </tr> </c:forEach> </table>分页链接用c:url拼接,注意&要写成&,否则 HTML 校验会报错。如果列表页需要图片懒加载,image_url为空时给一个占位图路径,不要让浏览器去请求空地址,否则控制台会刷一堆 404。
3.2 目击记录提交页的表单校验与防重复提交
目击记录提交是公众参与的核心入口,但也是最容易出问题的地方。用户可能填错时间、重复点击提交、或者上传超大图片。我的做法是在前端做基础校验,后端做最终校验,数据库加唯一约束兜底。
前端用 HTML5 的required和pattern做第一层拦截:
<form action="submitSighting" method="post" enctype="multipart/form-data"> <select name="speciesId" required> <option value="">请选择物种</option> <c:forEach items="${allSpecies}" var="sp"> <option value="${sp.id}">${sp.chineseName}</option> </c:forEach> </select> <input type="text" name="location" required maxlength="255" placeholder="目击地点"> <input type="datetime-local" name="sightingTime" required> <input type="text" name="reporterName" maxlength="64" placeholder="您的称呼"> <input type="text" name="reporterContact" maxlength="128" placeholder="联系方式"> <input type="file" name="photo" accept="image/*"> <button type="submit">提交记录</button> </form>后端在 Servlet 里做三件事:校验sightingTime不能晚于当前时间、校验speciesId在数据库里存在、用synchronized或数据库唯一索引防止同一用户短时间内重复提交。我一般会在sighting_record上加一个UNIQUE KEY uk_reporter_time (reporter_contact, sighting_time),这样即使前端被绕过,数据库也会拒绝重复数据。
String contact = req.getParameter("reporterContact"); String sightingTimeStr = req.getParameter("sightingTime"); LocalDateTime sightingTime = LocalDateTime.parse(sightingTimeStr); if (sightingTime.isAfter(LocalDateTime.now())) { req.setAttribute("error", "目击时间不能晚于当前时间"); req.getRequestDispatcher("/sighting-form.jsp").forward(req, resp); return; } // 检查物种是否存在 String checkSql = "SELECT COUNT(*) FROM animal_species WHERE id = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, speciesId); ResultSet rs = ps.executeQuery(); rs.next(); if (rs.getInt(1) == 0) { req.setAttribute("error", "所选物种不存在"); req.getRequestDispatcher("/sighting-form.jsp").forward(req, resp); return; } }文件上传用Part接口,限制大小在web.xml里配<multipart-config>,单文件不超过 5MB,超过就返回错误页。图片存到服务器本地目录,数据库只存相对路径,这样迁移时不用动数据。
4. 避坑与排查:JSP + MySQL 项目最容易翻车的五个点
4.1 中文乱码:从数据库到页面的全链路排查
现象:物种名称在数据库里查出来是正常中文,但 JSP 页面显示成????或乱码。原因通常出在三个环节:数据库连接串没指定characterEncoding、JSP 页面没声明pageEncoding、Tomcat 的URIEncoding没配。解决顺序是先从数据库查起,用SHOW VARIABLES LIKE 'character%'确认character_set_server是utf8mb4,然后检查连接串里有没有useUnicode=true&characterEncoding=utf8mb4,最后在 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>。Tomcat 的server.xml里 Connector 加URIEncoding="UTF-8",否则 GET 请求参数会乱码。
4.2 连接池耗尽:页面卡死但日志没报错
现象:网站运行一段时间后所有页面都转圈,重启 Tomcat 就恢复。原因多半是某个 Servlet 里拿了连接没关,或者ResultSet没关导致连接无法归还。排查方法是登录 MySQL 执行SHOW PROCESSLIST,如果看到大量Sleep状态的连接且Time值很大,基本可以确认是泄漏。解决方式是用 try-with-resources 包裹所有Connection、PreparedStatement、ResultSet,确保异常时也能关闭。另外把 HikariCP 的leakDetectionThreshold设为 60000,超过 60 秒没归还就打印堆栈,能快速定位到具体代码行。
4.3 图片上传后无法显示:路径拼接的坑
现象:上传成功,数据库里也有路径,但<img src="${sp.imageUrl}">显示裂图。原因通常是存了绝对路径(如C:\upload\xxx.jpg),浏览器无法访问。解决方式是数据库只存相对路径(如/uploads/20250101/abc.jpg),JSP 里用${pageContext.request.contextPath}${sp.imageUrl}拼接上下文路径。另外确认 Tomcat 的server.xml里有没有配置<Context docBase="/data/uploads" path="/uploads"/>,没有的话需要把上传目录映射成虚拟路径。
4.4 时间字段差 8 小时:时区配置不一致
现象:用户提交的目击时间是 14:00,数据库里存的是 06:00。原因是 JVM 时区和 MySQL 时区不一致。解决方式是在连接串里显式指定serverTimezone=Asia/Shanghai,同时在 MySQL 的my.cnf里设default-time-zone='+08:00'。如果用的是 Docker 部署 MySQL,还要在docker run时加-e TZ=Asia/Shanghai,否则容器内部是 UTC 时间。
4.5 后台审核状态不更新:事务没提交
现象:管理员点击“通过”按钮后,页面提示成功,但刷新列表状态还是“待审核”。原因通常是 JDBC 默认自动提交被关掉了,但代码里忘了conn.commit()。解决方式是在执行UPDATE后显式调用commit(),或者确认conn.setAutoCommit(true)没有被误改。如果用了 Spring 的@Transactional,检查是否因为异常被吞掉导致回滚。最稳妥的做法是在UPDATE后立刻SELECT一次确认状态已变更,再返回成功提示。
5. 让公益网站真正可用的三个进阶技巧
5.1 用 SQL 视图简化多表关联查询
目击记录列表需要同时显示物种名称和保护等级,如果每次都在 Java 里拼两张表的数据,代码会越来越臃肿。我一般会建一个视图:
CREATE VIEW v_sighting_detail AS SELECT s.id, s.location, s.sighting_time, s.reporter_name, s.status, a.chinese_name, a.protection_level FROM sighting_record s LEFT JOIN animal_species a ON s.species_id = a.id;这样后台查询直接SELECT * FROM v_sighting_detail WHERE status = 0 ORDER BY sighting_time DESC,Java 侧只处理一个结果集。视图的代价是每次查询都会执行 JOIN,但在数据量不超过十万行时完全可接受。如果后续数据量大了,再把视图改成物化表或加缓存。
5.2 用过滤器统一处理编码和登录校验
每个 JSP 都写一遍编码声明和登录检查太累,用Filter一次性解决:
@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }登录校验的 Filter 只拦截/admin/*路径,检查session.getAttribute("adminUser")是否为空,为空就重定向到登录页。注意 Filter 的顺序,编码 Filter 要放在最前面,否则登录校验里读参数也会乱码。
5.3 用定时任务清理过期未审核记录
目击记录如果 30 天没人审核,基本就是无效数据,留着只会拖慢查询。我一般用ScheduledExecutorService每天凌晨跑一次清理:
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { String sql = "DELETE FROM sighting_record WHERE status = 0 AND create_time < DATE_SUB(NOW(), INTERVAL 30 DAY)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { int deleted = ps.executeUpdate(); System.out.println("清理未审核记录: " + deleted + " 条"); } catch (SQLException e) { e.printStackTrace(); } }, 0, 24, TimeUnit.HOURS);这个任务放在ServletContextListener的contextInitialized里启动,contextDestroyed里调用scheduler.shutdown()。注意清理前最好先备份到历史表,万一误删还能找回。我自己的习惯是先在测试库跑一遍SELECT COUNT(*)确认数量,再执行DELETE,这个后悔药值得留。
做公益网站和做商业项目最大的区别是,你永远不知道下一个提交目击记录的人是谁,可能是护林员,也可能是小学生。所以页面要简单、反馈要明确、容错要足够。我踩过最深的坑是图片上传没限制大小,结果一个用户传了 20MB 的 RAW 格式照片,直接把 Tomcat 内存打满。从那以后我所有上传接口都先在前端压缩,后端再校验一次。希望帮到你。
本文还有配套的精品资源,点击获取