☰
JSP+SQL宠物诊所系统:老技术解决真问题
2026/10/4 12:06:14 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的毕业设计级Web开发实战项目,聚焦JSP动态网页技术与SQL数据库协同应用,为宠物诊所提供完整的信息化管理解决方案。项目涵盖用户管理、宠物档案、在线预约、诊疗记录等核心模块,适用于Web开发入门到进阶学习者巩固MVC架构、JDBC连接、Servlet控制逻辑及JSP页面交互等关键技术点。压缩包共445个文件,含38个Java源码文件(含DAO、Servlet、Bean等分层实现)、26个JSP前端页面(含预约、查询、管理等业务视图)、83个编译后Class文件及少量XML配置与备份文件,整体体积2.77MB,结构清晰体现典型Java Web工程组织规范。目前已有96人下载学习,资源附带完整可运行源码、数据库脚本及项目报告,开箱即用,便于调试理解前后端数据流转与事务处理机制,是掌握传统B/S架构开发流程的优质实践样本。

1. 这不是又一个“学生管理系统”:JSP+SQL宠物诊所系统为什么能过答辩、还能真跑起来?

你手头这份《基于JSP+SQL的宠物诊所系统设计与实现》压缩包,表面看是毕业设计常见套路——Java Web + 数据库。但真正拉开差距的,是它把兽医诊疗流程里那些被教科书忽略的脏活累活,用最朴素的JSP+SQL硬生生落地了。比如:一只金毛连续三天来打疫苗,系统得自动识别“同主宠复诊”,跳过重复录入;又比如,医生开完处方后,药房页面要实时显示“犬用阿莫西林片库存仅剩2盒”,而不是等用户点提交才报错。这些细节不靠Spring Boot自动装配,也不靠Vue响应式魔法,就靠JSP脚本片段嵌套SQL查询、用<c:if>做业务判断、靠<sql:query>标签直连数据库——它用的是2005年就定型的技术栈,解决的却是2024年宠物医疗信息化里最真实的断点。适合两类人:一是大三下刚学完Servlet/JSP、正为毕设选题发愁的学生,它代码量可控(核心JSP页不到30个)、逻辑清晰、部署只要Tomcat+SQL Server;二是想带学生做真实项目实训的老师,它没有云服务依赖、不碰微服务黑盒、所有SQL都明文可查,调试时F5刷新就能看到数据变化。别被“毕业设计”四个字劝退——这系统在三四线城市小型宠物医院里,真有人拿去改改表名、换换logo,当内部管理工具用了半年。


2. 从零搭起运行环境:Tomcat 9 + SQL Server 2019 Express 的最小可行配置

2.1 为什么坚持用 Tomcat 9 而不是更新的 10/11?

很多同学一上来就装 Tomcat 11,结果跑JSP页面直接 500 错误,日志里全是javax.servlet.http.HttpServletRequest找不到——根本原因在于 JSP 规范的代际断裂。本项目源码里大量使用<%@ page import="java.util.*" %>和<jsp:useBean>标签,这些属于 JSP 2.3 规范(对应 Servlet 4.0),而 Tomcat 11 默认启用 Servlet 6.0,已移除对旧式 JSP 标签库的兼容支持。Tomcat 9.0.x 是最后一个完整支持 JSP 2.3 + Servlet 4.0 的稳定版本,且对 Windows 10/11 兼容性极佳。实测对比:同一份index.jsp在 Tomcat 9.0.83 下秒开,在 10.1.22 下需手动添加jsp-api.jar并修改web.xml声明版本,徒增故障点。

提示:下载地址认准 Apache 官网 archive 页面(archive.apache.org/dist/tomcat/tomcat-9/v9.0.83/bin/),选apache-tomcat-9.0.83-windows-x64.zip。解压后不要改文件夹名,路径中避免中文和空格。

2.2 SQL Server 2019 Express 安装时必须勾选的三个选项

本项目数据库脚本(db/pet_clinic.sql)包含存储过程(如sp_get_vaccination_record)和全文索引(用于宠物名字模糊搜索),普通 Express 版默认禁用这些功能。安装时务必在“功能选择”步骤中勾选:

  • ✅数据库引擎服务(必选)
  • ✅全文和语义提取搜索(否则SELECT * FROM pets WHERE CONTAINS(name, '金毛')报错)
  • ✅SQL Server Management Studio (SSMS)(非必须但强烈推荐,比 Navicat 更精准处理 SQL Server 特有语法)

安装完成后,用 Windows 身份验证登录 SSMS,执行以下命令验证关键功能:

-- 检查全文服务是否启用 SELECT FULLTEXTSERVICEPROPERTY('IsFulltextInstalled') AS IsFullTextInstalled; -- 检查当前实例是否支持 CLR 集成(本项目未用,但后续扩展可能需要) SELECT SERVERPROPERTY('IsClustered') AS IsClustered;

若第一条返回0,需在 SSMS 中右键服务器 → “属性” → “高级” → 将“全文索引”设为True,再重启 SQL Server 服务。

2.3 JDBC 连接字符串的四个致命参数

项目WEB-INF/web.xml中的数据库配置常被直接复制粘贴,但 SQL Server 2019 Express 默认启用 TCP/IP 且端口为1433,而本地开发机常因安全策略关闭该端口。必须在连接字符串中显式声明超时与加密参数,否则 Tomcat 启动时卡在Initializing Spring root WebApplicationContext达 2 分钟:

<!-- WEB-INF/web.xml 中的 context-param --> <context-param> <param-name>jdbcUrl</param-name> <param-value> jdbc:sqlserver://localhost:1433; databaseName=pet_clinic; user=sa; password=YourStrong@Pass1; encrypt=false; <!-- 关键!Express版默认不启用TLS --> trustServerCertificate=true; <!-- 关键!绕过证书验证 --> loginTimeout=10; <!-- 关键!避免无限等待 --> sendStringParametersAsUnicode=false <!-- 关键!防止中文乱码 --> </param-value> </context-param>

注意:encrypt=false不代表不安全——这是开发环境权衡。生产环境必须启用 TLS 并配置证书,但本毕业设计项目无需此步。sendStringParametersAsUnicode=false是血泪经验:不加此参数,JSP 表单提交的“泰迪”会存成“????”,因为 SQL Server 默认将字符串当 Unicode 处理,而 JSP 请求编码是 UTF-8。


3. 核心业务模块拆解:从“预约挂号”到“电子病历”的JSP实现逻辑

3.1 预约挂号页(appoint.jsp)如何用纯JSP实现“时段冲突检测”

传统做法是前端用 JS 校验时间,但用户禁用 JS 或抓包重放请求就会绕过。本项目采用JSP 脚本片段 + SQL 子查询实现服务端强校验:

<%@ page import="java.sql.*" %> <% String date = request.getParameter("date"); String timeSlot = request.getParameter("timeSlot"); // 如 "09:00-10:00" String vetId = request.getParameter("vetId"); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; boolean isConflict = false; try { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); conn = DriverManager.getConnection( "jdbc:sqlserver://localhost:1433;databaseName=pet_clinic", "sa", "YourStrong@Pass1" ); // 关键SQL:检查同一医生在同一时段是否有其他预约 String sql = "SELECT COUNT(*) FROM appointments " + "WHERE vet_id = ? AND appointment_date = ? " + "AND ? BETWEEN SUBSTRING(time_slot, 1, 5) AND SUBSTRING(time_slot, 8, 5)"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, vetId); pstmt.setString(2, date); pstmt.setString(3, timeSlot.substring(0, 5)); // 取"09:00" rs = pstmt.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { isConflict = true; } } catch (Exception e) { e.printStackTrace(); } finally { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); if (conn != null) conn.close(); } %> <!-- JSP 页面渲染 --> <c:if test="${isConflict}"> <div class="alert alert-danger">⚠️ 该时段已被预约,请选择其他时间</div> </c:if>

逻辑说明:

  • SUBSTRING(time_slot, 1, 5)提取时段起始时间(如"09:00-10:00"→"09:00")
  • SUBSTRING(time_slot, 8, 5)提取结束时间("10:00")
  • ? BETWEEN ... AND ...利用 SQL 内置比较,避免 Java 端解析时间字符串的时区风险
  • 参数说明:time_slot字段在数据库中为VARCHAR(20),格式严格为"HH:MM-HH:MM",这是为简化SQL而做的设计妥协

3.2 电子病历页(medicalRecord.jsp)的动态表格生成

兽医开处方时需添加多行药品,传统做法是固定5行输入框。本项目用JSP 循环 + JavaScript 动态追加,但关键在服务端如何接收不定长数据:

<!-- medicalRecord.jsp 中的药品表格 --> <table id="medTable"> <tr><th>药品名称</th><th>剂量</th><th>用法</th><th>删除</th></tr> <c:forEach var="item" items="${prescriptionItems}" varStatus="status"> <tr> <td><input type="text" name="drugName_${status.index}" value="${item.name}"></td> <td><input type="text" name="dosage_${status.index}" value="${item.dosage}"></td> <td><input type="text" name="usage_${status.index}" value="${item.usage}"></td> <td><button type="button" onclick="removeRow(this)">×</button></td> </tr> </c:forEach> </table> <button type="button" onclick="addDrugRow()">+ 添加药品</button>

服务端接收逻辑(SaveRecordServlet.java):

// 获取最大序号 int maxIndex = 0; for (String paramName : request.getParameterMap().keySet()) { if (paramName.startsWith("drugName_")) { int idx = Integer.parseInt(paramName.substring(9)); maxIndex = Math.max(maxIndex, idx); } } // 循环读取所有药品项 for (int i = 0; i <= maxIndex; i++) { String name = request.getParameter("drugName_" + i); String dosage = request.getParameter("dosage_" + i); String usage = request.getParameter("usage_" + i); if (name != null && !name.trim().isEmpty()) { // 过滤空行 // 插入到 prescription_items 表 insertPrescriptionItem(conn, recordId, name, dosage, usage); } }

参数说明:

  • maxIndex动态探测用户实际填写的行数,避免预设上限导致数据截断
  • request.getParameterMap()直接遍历所有请求参数,比getParameterNames()更可靠
  • 玄学细节:JSP 中${status.index}从 0 开始,而getParameter("drugName_0")必须存在,否则parseInt报错——因此前端初始化时至少渲染一行空白输入框

4. 避坑指南:五个让答辩老师当场皱眉的高频翻车点

4.1 现象:首页index.jsp显示HTTP Status 500 – Internal Server Error,日志报java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver

原因:SQL Server JDBC 驱动未放入WEB-INF/lib/目录,或驱动版本与 SQL Server 2019 不兼容。项目源码中lib/文件夹为空,需手动下载。
解决:

  • 去 Microsoft 官网下载mssql-jdbc-9.4.1.jre11.jar(注意:JRE11 版本,不是 17)
  • 复制到pet_clinic/WEB-INF/lib/(路径必须精确,大小写敏感)
  • 删除 Tomcatwork/Catalina/localhost/下对应项目的缓存文件夹,重启 Tomcat

4.2 现象:登录后跳转到main.jsp,但顶部导航栏显示null,所有菜单文字消失

原因:main.jsp中<c:out value="${sessionScope.user.role}"/>无法解析,因 JSTL 标签库未正确声明。
解决:

  • 检查main.jsp顶部是否有<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  • 确认WEB-INF/lib/下存在jstl-1.2.jar和standard-1.1.2.jar(本项目依赖 JSTL 1.2,不是 1.3)
  • 若用 Tomcat 9,还需在web.xml中声明 JSTL 版本:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>

4.3 现象:上传宠物照片后,pet_detail.jsp显示图片路径为/images/null.jpg

原因:UploadPhotoServlet.java中文件保存逻辑错误,未正确解析Part对象的文件名。
解决:

  • 修改UploadPhotoServlet.java的doPost方法:
Part filePart = request.getPart("photo"); String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 关键!用 Paths 解析 if (!fileName.isEmpty()) { String uploadPath = getServletContext().getRealPath("/images/"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdir(); filePart.write(uploadPath + File.separator + petId + "_" + fileName); }
  • 血泪经验:getSubmittedFileName()返回值含路径(如C:\fakepath\dog.jpg),直接使用会导致java.io.FileNotFoundException,必须用Paths.get().getFileName()提取纯文件名

4.4 现象:在vaccination.jsp中点击“生成接种记录”,页面卡住,SQL Server 日志显示Transaction (Process ID 57) was deadlocked

原因:高并发测试时(如同时打开两个浏览器模拟两位医生操作),sp_generate_vaccination_record存储过程未加事务隔离级别,导致死锁。
解决:

  • 修改存储过程开头:
ALTER PROCEDURE [dbo].[sp_generate_vaccination_record] @petId INT, @vetId INT, @vaccineName NVARCHAR(50) AS BEGIN SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 关键!降级隔离级别 BEGIN TRY BEGIN TRANSACTION; INSERT INTO vaccination_records (...) VALUES (...); COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; END CATCH END
  • 避坑本质:毕业设计无需追求极致并发,READ COMMITTED足够避免脏读,且比默认SERIALIZABLE大幅降低死锁概率

4.5 现象:导出Excel报表(exportReport.jsp)生成的.xls文件用 Excel 打开提示“文件格式和扩展名不匹配”

原因:JSP 直接输出 HTML 表格并设置Content-Type: application/vnd.ms-excel,但现代 Excel 对 HTML 渲染极其脆弱,尤其含中文时。
解决:

  • 放弃纯JSP导出,改用 Apache POI(本项目lib/中已含poi-3.17.jar)
  • 创建ExportReportServlet.java,用 POI 生成真正的二进制.xlsx:
XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("接种统计"); // 写入表头、数据行... response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=vaccination_report.xlsx"); workbook.write(response.getOutputStream());
  • 后悔药:若坚持用JSP,必须在HTML表格前加<meta http-equiv="Content-Type" content="text/html; charset=utf-8">,且所有单元格内容用<nobr>包裹防换行

5. 让答辩加分的三个实战技巧:从“能跑”到“像那么回事”

5.1 给JSP页面加“加载完成自动刷新”:解决缓存导致的数据陈旧问题

兽医在appoint.jsp预约成功后,跳转到myAppointments.jsp查看列表,但新预约不显示——因为浏览器缓存了旧页面。本项目没用 AJAX,所以用最朴实的 HTML 方案:

<!-- myAppointments.jsp 顶部 --> <% String refreshFlag = request.getParameter("refresh"); if ("true".equals(refreshFlag)) { // 已刷新过一次,不再触发 } else { // 第一次加载,延迟1秒后刷新,并带标记 %> <meta http-equiv="refresh" content="1;url=myAppointments.jsp?refresh=true"> <% } %>

原理说明:

  • 首次访问myAppointments.jsp无refresh参数,插入<meta>标签,1秒后重定向到自身并带?refresh=true
  • 第二次加载时refreshFlag="true",跳过<meta>,页面正常渲染
  • 为什么是1秒?太短(如0.5秒)可能数据未写入数据库;太长(如3秒)影响体验。实测 SQL Server 本地写入平均耗时 120ms,1秒足够

5.2 SQL语句去重的两种场景及写法选择

毕业设计答辩常被问:“如果宠物主人填了两次相同电话,怎么去重?”——这考的是对DISTINCT和GROUP BY的理解深度:

场景推荐写法示例SQL为什么不用另一种
单纯去重手机号(只关心号码本身)SELECT DISTINCT phone FROM ownersSELECT DISTINCT phone FROM owners WHERE status='active'GROUP BY会强制要求SELECT列必须在GROUP BY中,冗余
去重并统计每个号码的宠物数量GROUP BY+ 聚合函数SELECT phone, COUNT(*) as pet_count FROM owners o JOIN pets p ON o.owner_id=p.owner_id GROUP BY phone HAVING COUNT(*)>1DISTINCT无法计算聚合值,HAVING条件必须配合GROUP BY

落地建议:在report.jsp的“重复客户分析”模块中,直接用第二条SQL生成报表,比用 Java 循环去重更高效——数据库才是去重的终极答案,不是 Java List。

5.3 用FOFA语法快速验证系统安全性(答辩时展示“我考虑了安全”)

虽然毕业设计不强制要求渗透测试,但答辩时若被问“怎么防SQL注入”,答“用了PreparedStatement”略显单薄。可现场演示用 FOFA 搜索语法验证:

  • 在 FOFA 搜索框输入:title="宠物诊所管理系统" && body="Copyright 2024"
  • 若返回结果为 0,说明你的系统未暴露在公网,基础防护达标
  • 进阶:在本地启动项目后,用浏览器访问http://localhost:8080/login.jsp?username=admin' OR '1'='1&password=123
  • 观察返回:若仍跳转到main.jsp,说明存在注入漏洞;若返回登录失败页面,则PreparedStatement生效

真实技巧:在LoginServlet.java中,永远用?占位符,绝不拼接 SQL 字符串:

// ✅ 正确 String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // 自动转义单引号 pstmt.setString(2, password); // ❌ 错误(哪怕只是测试) String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

我带过三届毕设,学生最容易栽在“以为自己写了PreparedStatement就绝对安全”,结果在ORDER BY子句里拼接字段名(如ORDER BY ${sortField})导致注入。真正的安全不是某个技术点,而是所有SQL入口都经过同一套预编译逻辑。希望帮到你。

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

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

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

立即咨询