ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP+SQL宠物诊所系统实战:从建库到部署的完整避坑指南

JSP+SQL宠物诊所系统实战:从建库到部署的完整避坑指南 简介面向计算机相关专业毕业设计场景这套基于JSPSQL的宠物诊所系统资料包含完整项目报告与源代码覆盖用户管理、宠物档案、预约挂号、诊疗记录等核心模块能够帮助学习者快速理解JavaWeb开发中JSP与数据库的配合方式。压缩包共445个文件以Java源文件、JSP页面、class编译文件、XML配置及备份文件为主整体仅2.77MB结构紧凑适合对照学习与二次开发。已有96人学习/下载对于需要完成类似选题或入门Web项目的学生具有直接参考价值。资料中不仅提供了表现层、业务层与数据层的代码实现还包含项目报告文档可辅助梳理需求分析、数据库设计及MVC分层思路是一份能落地的毕业设计参考资料。1. 这个JSPSQL的宠物诊所系统到底值不值得你照着做如果你正在找毕业设计选题或者刚接手一个基于JSP的老项目这个宠物诊所系统非常典型——它没有眼花缭乱的前后端分离没有微服务就是一个标准的JavaWeb形态浏览器发请求Servlet接收JDBC操作数据库JSP渲染页面。这种项目能帮你把Java基础、SQL设计、HTTP原理串成一条完整的链路。它的业务量不大但宠物主人、宠物档案、挂号、病历、药品管理这些实体关系足够练手。比起网上那些千篇一律的学生管理系统宠物诊所的场景自带记忆点答辩时也好讲。这篇笔记我会从建库建表开始逐个功能给代码骨架再把乱码、SQL注入、路径404这些毕业设计里最常见的坑一个个说清楚最后告诉你如何让系统从「能跑」变成「不会翻车」。2. 从需求到数据表把宠物诊所的业务拆成SQL结构以及为什么这么拆2.1 业务对象梳理宠物、主人、医生、挂号、病历、药品做毕业设计最容易犯的错是一上来就建表。我先花十分钟把业务流程走一遍这十分钟决定了后续所有代码的复杂度。一个宠物诊所最基础的一天是这样的主人带着宠物来前台登记宠物信息如果第一次来然后挂号选择医生医生问诊后写病历如果需要用药开药品记录最后主人缴费离店。你把动词转换为名词主人在系统里是一条记录宠物是一条记录挂号是一次事件病历是挂号的结果药品是独立的库存表开药是关联表。这个分析决定了你要建哪些表以及表之间的关系。常见做法是建六张核心表owner宠物主人、pet宠物档案、vet医生、appointment挂号、medical_record病历、medicine药品。再给挂号与药品之间加一张中间表appointment_medicine因为一次挂号可以开多种药一种药也可以出现在多次挂号中。这样设计的好处是你在JSP页面上做增删改查时每个页面只对应一张主表或一张明细表逻辑清晰代码量可控。2.2 建库建表一段可以直接跑的SQL脚本附带字段设计说明下面这段SQL以MySQL为例如果你用的是SQL Server把自增写法从AUTO_INCREMENT改成IDENTITY(1,1)即可其他地方差别不大。我一般把表名、字段名全部小写下划线避免Windows环境下大小写不敏感带来的奇奇怪怪的问题。-- 宠物主人表 CREATE TABLE owner ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 主人姓名, phone VARCHAR(20) UNIQUE COMMENT 手机号登录账号, password VARCHAR(64) NOT NULL COMMENT 密码MD5或SHA-256后存入, address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 宠物档案表 CREATE TABLE pet ( id INT PRIMARY KEY AUTO_INCREMENT, owner_id INT NOT NULL, name VARCHAR(50) NOT NULL, species VARCHAR(20) COMMENT 猫、狗、兔子等, breed VARCHAR(50), birth_date DATE, gender TINYINT COMMENT 0未知 1公 2母, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES owner(id) ON DELETE CASCADE ); -- 医生表 CREATE TABLE vet ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, specialty VARCHAR(100) COMMENT 专长如骨科、皮肤科, phone VARCHAR(20), available TINYINT DEFAULT 1 COMMENT 是否在职 ); -- 挂号表 CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT NOT NULL, vet_id INT NOT NULL, app_date DATE NOT NULL, app_time VARCHAR(20) COMMENT 如09:30, status TINYINT DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (pet_id) REFERENCES pet(id), FOREIGN KEY (vet_id) REFERENCES vet(id) ); -- 病历表 CREATE TABLE medical_record ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL UNIQUE COMMENT 一次挂号对应一份病历, diagnosis VARCHAR(500) COMMENT 诊断结果, treatment VARCHAR(500) COMMENT 治疗方案, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (appointment_id) REFERENCES appointment(id) ); -- 药品表 CREATE TABLE medicine ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, spec VARCHAR(100) COMMENT 规格如100片/瓶, stock INT DEFAULT 0, price DECIMAL(10,2) NOT NULL ); -- 挂号与药品中间表 CREATE TABLE appointment_medicine ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, medicine_id INT NOT NULL, quantity INT DEFAULT 1, FOREIGN KEY (appointment_id) REFERENCES appointment(id), FOREIGN KEY (medicine_id) REFERENCES medicine(id) );这段脚本里我特意设置了几个容易被忽视的点。owner.phone用了UNIQUE因为手机号作为登录账号天然唯一pet表的外键加了ON DELETE CASCADE这样删除主人时自动清理他的宠物档案避免JSP页面上先删宠物再删主人的繁琐操作medical_record的appointment_id用了UNIQUE因为一次挂号只能写一份病历这个约束在业务层漏写时数据库能兜底。注意DECIMAL(10,2)存价格不要用FLOAT否则算总价时会看到一堆小数点尾巴。2.3 外键与索引查询慢的根源往往在建模阶段就埋下了外键和索引是答辩时老师喜欢问的点。外键保证了数据完整性但过度使用会使插入和删除变慢。我的建议是业务关系明确的表用外键比如pet.owner_id、appointment.pet_id这些字段经常被JOIN有了外键索引查询时会走索引而不是全表扫描。但appointment_medicine这种中间表不需要额外建外键索引因为它的查询总是以appointment_id为条件单独建一个普通索引就够了。另外一个常见坑是日期字段。appointment表的app_date和app_time分开存而不是合成一个DATETIME是为了方便JSP页面上按日期筛选挂号记录。如果你用DATETIME查某一天的记录时就得用DATE(app_date)2024-05-20这个函数调用会让索引失效。分开存后直接WHERE app_date BETWEEN ? AND ?索引就能生效。这种细节在数据量只有几千条时感觉不到但答辩老师问起来你能说出「为什么分开」就已经赢了。3. 用JSPServlet写业务逻辑登录、挂号、病历录入的代码骨架3.1 在IDEA里新建一个JSP项目环境配置的完整过程很多新手在IDEA里新建JSP项目时直接卡住其实核心就三步创建JavaWeb项目、配置Tomcat、添加依赖。我用的是IDEA社区版新建项目时选择「Java Enterprise」勾选Web Application然后在下方的Application Server里选本地Tomcat的安装目录。如果没有这个选项说明Tomcat没配置到IDEA里需要在Settings搜索Application Servers手动指定Tomcat Home路径。创建好后项目结构里会自动生成web/WEB-INF目录和index.jsp。这一步最容易出错的是依赖问题JSP和Servlet的依赖不放在Tomcat的lib里而是放在项目的WEB-INF/lib下或者用Maven引入。如果用Mavenpom.xml里需要加两个依赖一个是javax.servlet-api另一个是javax.servlet.jsp-apiscope都设为provided因为Tomcat本身自带这些类。这两个依赖的版本要和你的Tomcat版本匹配比如Tomcat 9对应javax.servlet-api 4.xTomcat 10对应jakarta.servlet-api 5.x。用错版本会出现ClassNotFoundException这个坑在我刚做JSP项目时翻车过无数次。3.2 数据库连接池与JDBC别再用DriverManager了毕业设计里最常见的数据库写法是在每个Servlet里写Class.forName(com.mysql.cj.jdbc.Driver)然后再DriverManager.getConnection。这种写法作业能跑但每次请求都创建连接数据库一旦有几十个并发就挂了。更现实的问题是你写的代码里连接不关闭最终连接数耗尽系统「每次用几分钟就报连接超时」。我建议至少用一个简单的连接池比如Apache DBCP或者HikariCP配置放到context.xml里JSP页面上通过JNDI获取数据源。如果嫌JNDI配置麻烦也可以在工具类里静态初始化一个HikariDataSource// DBUtil.java - 工具类整个项目复用这一个数据源 import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/pet_clinic?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }这个工具类里有几个参数值得讲。jdbcUrl里的useUnicode和characterEncodingUTF-8必须写否则页面上传过来的中文到数据库里就变成问号了。serverTimezoneAsia/Shanghai是因为MySQL 8.0默认时区与JDBC驱动不一致不加会报SQLException。useSSLfalse不是不让加密而是因为本地开发用的自签名证书会导致握手失败生产环境才需要正确配置SSL。maximumPoolSize设为10是毕设级别够用的值如果你在SQL Server上跑连接写法类似jdbc:sqlserver://localhost:1433;DatabaseNamepet_clinic;encryptfalse;trustServerCertificatetrue注意SQL Server 2019及以上版本驱动默认强制加密需要显式关闭。3.3 核心功能实现挂号与病历录入的Servlet代码挂号和病历录入是系统的核心我把它们写在一个Servlet里通过action参数区分操作。这种写法在传统JSP项目里非常常见比每个功能建一个Servlet类更省代码。挂号页面提交的数据包括petId、vetId、appDate、appTimeServlet接收后做基本校验然后插入appointment表同时设置status为0。下面是挂号处理的代码// AppointmentServlet.java - 处理挂号新增 WebServlet(/appointment) public class AppointmentServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (add.equals(action)) { int petId Integer.parseInt(request.getParameter(petId)); int vetId Integer.parseInt(request.getParameter(vetId)); String appDate request.getParameter(appDate); String appTime request.getParameter(appTime); // 根据当前登录用户的id去owner表查到owner_id这里简化为直接传ownerId int ownerId Integer.parseInt(request.getParameter(ownerId)); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( INSERT INTO appointment(pet_id, vet_id, app_date, app_time, status) VALUES (?,?,?,?,0), PreparedStatement.RETURN_GENERATED_KEYS)) { ps.setInt(1, petId); ps.setInt(2, vetId); ps.setString(3, appDate); ps.setString(4, appTime); ps.executeUpdate(); // 获取自增主键用于后续病历表关联 ResultSet rs ps.getGeneratedKeys(); if (rs.next()) { request.setAttribute(appointmentId, rs.getInt(1)); } response.sendRedirect(appointmentList.jsp?msgsuccess); } catch (Exception e) { e.printStackTrace(); response.sendRedirect(appointmentAdd.jsp?msgerror); } } } }这段代码有两点值得注意。第一我用了PreparedStatement而不是Statement这是一个基本的安全底线后面避坑章节会专门讲。第二getGeneratedKeys获取插入后的自增ID这个ID要传给病历录入页面否则病历无法关联到挂号记录。如果你用的是SQL Server这段逻辑完全一样只是JDBC驱动类名和URL不同。这里没有做事务因为单独插入一张表不需要事务但病历录入时要同时更新appointment的status和插入medical_record那就得用事务了否则会出现「病历写进去了挂号还是待就诊」的数据不一致。病历录入的Servlet逻辑稍微复杂一点先插入medical_record然后更新appointment.status1再处理中间表appointment_medicine。这里必须用事务因为三个操作要么全成功要么全回滚。我用一个简单的try-with-resources包裹手动关闭连接时提交。如果你不熟悉事务可以在JDBC里写conn.setAutoCommit(false)然后执行多个语句最后conn.commit()任何一步出错就conn.rollback()。这样答辩时说「我用事务保证数据一致性」就是加分点。4. 让系统跑起来从IDEA到Tomcat的war包部署全流程4.1 本地运行配置Tomcat与数据源的几个关键参数配置Tomcat时最常见的痛苦是端口被占用、虚拟路径映射错误、以及数据源连接失败。在IDEA中Run Configuration里选择Tomcat Server然后配置Deployment把项目的Artifact添加进去。Application context建议填/pet这样访问地址就是http://localhost:8080/pet。Tomcat默认端口8080如果被其他进程占用可以在Server标签页修改HTTP port为8081或者在conf/server.xml里改Connector的port属性。我遇到过好几次8080被Oracle或者其他服务占用的改端口最快不用去kill进程。数据源配置分两种。如果你是按照第3章的方式用HikariCP写在Java代码里那不需要额外配置直接运行即可。如果你用JNDI数据源那么需要在META-INF/context.xml或conf/context.xml里写Resource节点然后在web.xml里引用。JNDI方式的优先级高Tomcat启动时会先找context.xml如果配置不对整个项目启动失败。我建议毕设直接用Java代码初始化数据源少一层配置就少一个坑答辩时也能解释清楚连接池原理。4.2 传统JSP项目打包war手把手命令与常见失败点传统JSP项目打包war本质上就是把编译后的class文件、JSP页面、WEB-INF下的web.xml和lib依赖一起压缩成war包。如果你用IDEA直接菜单Build - Build Artifacts - Build选择war包类型IDEA会输出到out/artifacts目录下。如果不用IDEA手动打war时注意目录结构war包根目录下必须有WEB-INF/web.xmlclass文件放在WEB-INF/classes下jar包放在WEB-INF/lib下。很多人手动打包时漏了lib目录导致部署后NoClassDefFoundError。还有一种常见做法是用Maven插件打包在pom.xml里加maven-war-plugin然后执行mvn clean package。但要小心如果你的项目没有使用Maven标准目录结构src/main/java、src/main/webappMaven会找不到JSP文件。遇到这种情况我一般直接放弃Maven回归IDEA自带的Artifacts打包因为毕业设计项目的目录结构往往不规整强行上Maven反而浪费时间。打包完成后把war文件扔到Tomcat的webapps目录下启动Tomcatwar包会自动解压成同名文件夹。这里有一个传统JSP项目打包war的经典坑JSP页面里如果用了taglib指令引入JSTL标签库而你没有把jstl.jar和standard.jar放进lib目录tomcat启动时不会报错但访问页面时会报500错误提示Unable to read TLD。解决方案是确认lib下有没有这两个jar或者改用Maven依赖引入。检查方法很简单打包前看看WEB-INF/lib里jar的数量少于10个就要怀疑是不是缺了。4.3 部署后的验证你不能光看页面打开要看数据是否正确部署成功后很多人打开登录页能显示就认为成功了这个标准太低了。我的习惯是准备三个验证点。第一注册一个新用户并登录看owner表里是否多了一条记录密码字段是否是密文而不是明文——如果你把密码明文存了说明登录模块的加密没做。第二新建一个宠物档案然后去挂号挂号完成后打开数据库看appointment表status是否为1或你设置的其他初始值并且病历表能通过appointment_id关联到。第三故意在页面输入一个不存在的宠物ID看系统是否报错、报错信息是否把SQL语句暴露在页面上——如果暴露了说明异常处理是空的这是答辩时最容易减分的地方。这三个验证点覆盖了最基本的写操作、读操作和异常分支。如果任何一个点失败不要急着改前端打开Tomcat的logs/localhost日志日期对应今天的那个log文件会打印完整的堆栈。我做项目调试时90%的问题都能从localhost.log里找到原因剩下的10%才是环境层面的。5. JSPSQL项目避坑乱码、注入、路径问题与黑匣子排查5.1 中文乱码页面、请求、数据库三层都要搞对乱码是JSP项目排第一的玄学问题。现象是页面上显示中文正常但从表单提交到数据库后变成了问号或者在JSP页面直接显示数据库中文是乱码。这个问题的根源在于三层编码不一致每一层都需要单独配置。第一层是JSP页面本身。页面顶部必须有% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%单纯写meta charset是不够的。很多旧教材只写contentType不写pageEncodingJSP编译时就会用默认的ISO-8859-1中文全乱。第二层是请求传递。POST请求在Servlet里必须调用request.setCharacterEncoding(UTF-8)而且要在读取任何参数之前调用。GET请求的乱码更麻烦因为Tomcat 8以上默认GET编码是UTF-8但如果你用的是Tomcat 7或更低需要在server.xml的Connector里加URIEncodingUTF-8。第三层是数据库连接。JDBC URL必须带characterEncodingUTF-8同时确保数据库表本身是utf8mb4字符集。MySQL可以通过ALTER TABLE owner CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改。三层都对了乱码问题基本消失。排查顺序很关键先看JSP源文件里的中文是否正常再看数据库表里直接手工insert中文能否正常显示最后看通过JDBC插入的数据。哪一层显示乱码问题就锁定在哪一层。这个排查思路比瞎改web.xml有效率得多。5.2 SQL注入与万能密码你的登录接口可能形同虚设JSP项目中SQL注入高发区是登录页和查询条件页。经典的万能密码是输入用户名 or 11密码随便填如果登录代码是拼接字符串的比如SELECT * FROM owner WHERE phoneusername AND passwordpassword这条语句就变成了WHERE phone or 11恒为真你就能以任意用户身份登录。这也是为什么登录是每次答辩必问的点。解决SQL注入的标准做法是使用PreparedStatement参数化查询不要用Statement拼接。上面挂号代码里我已经用了占位符?登录也遵循同样写法// LoginServlet.java - 参数化查询防止注入 String sql SELECT id, name FROM owner WHERE phone? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, md5(password)); // 密码加密后比对 ResultSet rs ps.executeQuery();这里有两点是关键用户名和密码都作为参数传入数据库驱动会正确转义特殊字符同时先用md5对密码做哈希即使数据库泄露也不会直接暴露明文。注意md5已经不算强哈希但毕设里用到MD5配合参数化查询已经足够如果你想加分可以用SHA-256加盐。如果你用的是SQL Server同样是preparedStatement只是占位符也是?不变。另外不要在日志里打印完整SQL语句因为如果SQL是拼接的日志就会变成注入攻击的线索。5.3 JSP页面路径404绝对路径与相对路径的坑JSP页面间跳转经常出现404最典型的情况是你在appointmentList.jsp里写a hrefappointmentAdd.jsp结果从Servlet跳转过来时浏览器地址栏是/pet/appointmentList然后点击链接变成/pet/appointmentAdd.jsp没问题但如果你在页面里用了../appointmentAdd.jsp而当前URL层级不同就404了。这种玄学问题的根治办法是使用绝对路径。在JSP页面顶部引入base标签或者用一个公共变量获取当前应用上下文路径% String basePath request.getContextPath(); % a href%basePath%/appointmentAdd.jsp新增挂号/a同理Servlet重定向时要用response.sendRedirect(request.getContextPath()/appointmentList.jsp)不要写相对路径。如果还是404打开浏览器F12看Network面板看请求的URL实际是什么。我曾经排查过一个明明文件存在却一直404的问题最后发现是Tomcat的应用上下文是/pet而代码里写死了/clinic/开头路径前缀错了。这个坑在部署war包到别人的机器上时尤其容易犯因为对方的context path可能和你本地不一样。5.4 数据库连接失败驱动、URL、时区、加密连接逐个排数据库连接失败是整个项目启动阶段最常见的问题现象通常是Tomcat启动成功但访问业务页面时抛SQLException。先看驱动类有没有MySQL 8以上要用com.mysql.cj.jdbc.Driver旧驱动是com.mysql.jdbc.Driver写错就是ClassNotFoundException。接着看URLMySQL连接串要写jdbc:mysql://localhost:3306/pet_clinicSQL Server是jdbc:sqlserver://localhost:1433;DatabaseNamepet_clinic。如果URL里没加时区MySQL 8会报The server time zone value йʱ is unrecognized在URL尾巴加serverTimezoneAsia/Shanghai即可。还有一个容易被忽略的是SQL Server的加密连接问题。SQL Server 2019以后的JDBC驱动默认开启SSL加密本地开发库没有证书时会报错内容类似「驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接」。解决方法是连接串显式加encryptfalse;trustServerCertificatetrue。如果已经按这个配置还是连不上检查SQL Server是否允许TCP/IP协议在Sql Server Configuration Manager里启用TCP/IP并重启服务这一步很多人会忘。另外数据库账号的密码如果包含特殊字符在URL里需要URL编码比如#要写成%23否则连接串会被截断。6. 从毕业设计到能用的系统分页、参数化查询与部署后的日常维护如果时间允许我给这个宠物诊所系统加两个必考功能分页和参数化查询的全面改造。分页能体现你对SQL的理解做法是查询总记录数然后用LIMIT offset, pageSize。JSP页面上传pageNumServlet计算offset查两次库一次是count一次是列表。pageSize设在10到20之间对诊所场景足够了。参数化查询改造的重点是登录、挂号、病历录入、药品库存更新这四个地方把所有字符串拼接SQL全部换成PreparedStatement。这不仅是安全要求也是性能优化的基础——数据库可以对预编译语句做执行计划缓存。我在自己做老系统重构时发现有些查询慢不是SQL本身的问题而是每次用不同字符串拼接导致SQL Cache失效改造后快了两个数量级。部署到生产服务器后日常维护的核心是日志和备份。Tomcat的logs目录下catalina.out记录服务运行日志localhost.log记录应用异常要养成每周翻一翻的习惯。数据库备份至少每天一次MySQL用mysqldumpSQL Server用BACKUP DATABASE命令。我给自己的项目设过一条规矩任何一次修改数据库结构先备份再动手这也是我从一次误删表数据翻车后养成的血泪教训。另一个容易被忽视的点是密码策略。如果登录验证只用手机号加密码被撞库的风险很高。我一般会加一个简单的验证码毕设里用Session存验证码字符串页面用Java生成一张图片几行代码的事但答辩时能讲出「防止暴力破解」的价值。药品库存更新时也要做事务先减库存再开药如果库存不足就要回滚避免出现负库存。把这三个功能写进去系统就不再是「只是能跑」而是能应对正常使用场景。这套思路不仅适用于这个宠物诊所项目你以后做任何JSPSQL的老系统改造都能用得上。希望帮到你。本文还有配套的精品资源点击获取
返回列表