多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于JSP+SQL的宠物诊所系统:设计、实现与避坑指南

基于JSP+SQL的宠物诊所系统:设计、实现与避坑指南 简介这是一份基于JSPSQL的宠物诊所系统毕业设计完整资源面向需要完成Web课程设计或毕业设计的计算机专业学生旨在解决JSP动态页面与SQL数据库结合开发中的设计难题。压缩包共445个文件主要包括Java源代码、JSP页面、class编译产物、jar依赖库及项目报告文档整体仅2.77MB便于快速下载浏览Java与JSP文件覆盖用户、宠物、预约、诊疗四大核心模块class与jar文件支撑系统编译运行报告文档则包含需求分析、数据库建表和功能设计说明。目前已有96人学习下载。资源以MVC架构组织业务逻辑用JavaBean实现页面交互借助EL表达式和JSP标签库数据持久层通过JDBC完成事务处理并配有完整项目报告从系统设计到部署测试均有详细记录可作为毕业设计直接参考或二次开发基础适合想深入理解JSP与SQL整合应用的开发者。1. 基于JSPSQL的宠物诊所系统它解决什么问题适合谁做如果你正在为毕业设计选题发愁看到“基于JSPSQL的宠物诊所系统设计与实现”这个题目大概率只想搞明白一件事这东西能跑吗答案是能而且它可能是你一个月内最快能交付的系统。JSP负责把页面渲染出来SQL负责把宠物主人、挂号、病历、药品这些数据存住中间用JDBC连接就是这个项目全部的技术骨架。它适合时间紧、想稳过的同学也适合想搞懂传统Java Web分层到底怎么一回事的从业者——别嫌它老老技术意味着文档多、坑少、答辩时老师问不出幺蛾子。2. 先把系统拆开宠物诊所的功能模块、7张表与三层架构2.1 功能边界别把需求做成全家桶做毕业设计最忌讳需求膨胀。宠物诊所听起来小但如果你把预约、挂号、诊疗、开药、收费、库存、报表、权限全塞进去一个月写不完。拿到这个标题后我一般会把功能收敛成三个角色、五条链路管理员维护员工账号、查看营业统计、管理药品基础信息。前台可并入管理员角色登记宠物主人和宠物档案、办理挂号。医生填写接诊病历、开处方、选择药品生成收费单。对应的核心业务流程只有三条宠物建档 → 挂号 → 医生接诊开方以及药品入库 → 扣减库存 → 收费。不要做在线支付不要做短信提醒这些写在“未来展望”里就够答辩用了。页面数量控制在 10 个以内登录页、主页、主人管理、宠物管理、挂号页、接诊页、病历列表、药品管理、收费单、统计页。这一数量级在校招项目或毕设里是刚刚好的。2.2 数据库设计7张表怎么落表结构是这个项目报告的加分项评审老师第一眼看的往往是 ER 图和建表 SQL。我会把表拆成用户、主人、宠物、挂号、病历、药品、处方明细这 7 张外加一张可选的就诊参数表用来存号源。核心三张表的建表语句可以直接抄CREATE DATABASE pet_clinic DEFAULT CHARACTER SET utf8mb4; USE pet_clinic; CREATE TABLE t_owner ( owner_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主人ID, owner_name VARCHAR(30) NOT NULL COMMENT 姓名, phone VARCHAR(11) NOT NULL COMMENT 手机号, address VARCHAR(100) COMMENT 住址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间 ) ENGINEInnoDB COMMENT宠物主人表; CREATE TABLE t_pet ( pet_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 宠物ID, owner_id INT NOT NULL COMMENT 关联主人ID, pet_name VARCHAR(30) NOT NULL COMMENT 宠物名, species VARCHAR(20) NOT NULL COMMENT 物种犬/猫/鸟等, breed VARCHAR(30) COMMENT 品种, birthday DATE COMMENT 出生日期, gender CHAR(1) COMMENT 公/母, CONSTRAINT fk_pet_owner FOREIGN KEY (owner_id) REFERENCES t_owner(owner_id) ) ENGINEInnoDB COMMENT宠物档案表; CREATE TABLE t_appointment ( appt_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 挂号ID, pet_id INT NOT NULL COMMENT 宠物ID, doctor_id INT NOT NULL COMMENT 接诊医生ID, appt_date DATE NOT NULL COMMENT 就诊日期, appt_time VARCHAR(10) COMMENT 时段, status TINYINT DEFAULT 0 COMMENT 0待诊 1已完成 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_appt_pet FOREIGN KEY (pet_id) REFERENCES t_pet(pet_id) ) ENGINEInnoDB COMMENT挂号表;说明三点第一owner是 MySQL 的保留字所以表名一律加t_前缀避免报错第二外键一定要建虽然业务代码里未必用但报告里的 ER 图和外键约束能证明你懂关系型数据库第三状态字段用TINYINT而不是VARCHAR存中文排序和统计都好写页面显示时再翻译成文字。病历表t_medical_record、药品表t_drug、处方明细表t_prescription按同样的规范补齐重点是药品表里要加stock INT字段挂号前检查号源、开药前检查库存都是这个系统的逻辑亮点。2.3 架构选型JSP直连SQL还是硬上SSM有的同学看到现在招聘全是 Spring Boot就想在毕设里强行上 SSM结果被配置文件劝退。这个题目既然写着 JSPSQL就说明指导老师预期的是“JSP Servlet JDBC”这条传统链路。我建议不要改技术栈原因有三一是 JSP 直连 SQL 的代码量足够一个学期课程设计的体量方便你逐行讲清楚二是 Servlet 里写 JDBC 的逻辑对理解 HTTP 请求生命周期更直接三是答辩时老师大概率会追问“Connection 是谁关闭的”“如何防止 SQL 注入”这类基础问题纯 JDBC 代码反而容易答。用三层架构来组织代码com.pet.entity放实体类com.pet.dao放数据访问com.pet.servlet放控制层JSP 只做视图渲染别在 JSP 里写业务逻辑这一点会在第四章的代码里体现。3. 本地复现最小环境版本组合、建库脚本与 Tomcat 部署3.1 版本搭配记住这套组合少踩一半坑这套系统对版本敏感尤其体现在 JDBC 驱动和 Tomcat 的兼容性上。我实测过比较稳的组合是JDK 1.8 Tomcat 8.5 MySQL 5.7 mysql-connector-java 5.1.49。如果你从代码包里拿到的源码是 SQL Server 的脚本也可以临时切换但新装 SQL Server 2008 R2 在 Windows 11 上问题很多驱动还得分 x86/x64我建议优先用 MySQL。支持你选择 MySQL 的理由还有一点好——你本地出问题后在社区里一搜一大把答案SQL Server 的老版本报错搜起来全是英文论坛。3.2 初始化数据库mysql 命令行导入脚本无论是代码包里自带的.sql文件还是自己按第二章的语句建导入方式都一样。打开命令行mysql -u root -p进入 MySQL 后执行source C:/work/pet_clinic.sql;如果你用的是 Navicat 或 DataGrip直接运行整个 SQL 文件也行。注意一点很多毕设代码包里的 SQL 文件带有DROP DATABASE开头这是为了让你反复跑初始化脚本但如果你电脑上有其他项目共用了一个pet_clinic库名这个操作会直接清掉。所以导入前先打开 SQL 文件看一眼把DROP DATABASE注释掉更保险。3.3 部署到 Tomcat直接用 war 包还是走 IDEA如果你拿到的是完整源代码最省心的方式是导入 IDEA配置 Tomcat 后直接跑。第 1 步IDEA 里 Project Structure 把 Language Level 设为 8第 2 步Add Tomcat Server选本地 Tomcat 8.5 路径第 3 步Deployment 里加 ArtifactApplication Context 填/pet_clinic。启动后访问http://localhost:8080/pet_clinic/。如果代码包只给了你一个.war文件直接复制到 Tomcat 的webapps目录启动 Tomcat 后会自动解压部署。前者的好处是能断点调试后者的好处是省去环境配置验收演示时用 war 包更稳。这段配置的关键参数是 database URL一定在DBUtil.java里核对端口号。MySQL 默认 3306如果你机器上装了多个版本改了端口驱动就会连不上。另外切记把数据库账号密码改成你自己的代码包里默认的root/123456在你机器上大概率不通。3.4 启动顺序先启动数据库再启动 Tomcat这条听起来像废话但很多第一次跑项目的人翻车就翻在这。MySQL 服务没起来的时候Tomcat 启动不报错页面也能打开但只要一访问登录接口立刻抛Communications link failure。所以养成习惯先确认netstat -ano | findstr 3306有监听再启动 Tomcat。Tomcat 端口被占是另一个高频翻车点改conf/server.xml里的 8080 为 8081 是后悔药不过改完所有访问 URL 都要跟着变建议后端代码里如果有硬编码 localhost:8080 的地方一起改掉。4. 核心代码链路JDBC 工具类、登录防注入与挂号事务4.1 数据库连接工具类所有 DAO 的地基写一个DBUtil.java所有数据库操作都从这拿连接这是纯 JDBC 项目最标准的做法。直接抄下面这个版本package com.pet.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/pet_clinic?useUnicodetruecharacterEncodingutf8useSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明静态代码块里Class.forName负责加载驱动类这样getConnection里就不需要再写驱动注册。URL 里的characterEncodingutf8是为了让数据库连接以 UTF-8 传输中文少了这个参数你后端的插入和查询都可能出现乱码页面调半天也找不到原因。useSSLfalse是 5.1.49 驱动在 MySQL 5.7 上的必需项不加会报警告甚至失败。注意com.mysql.jdbc.Driver只在旧版驱动里存在如果你用的是 8.x 新版驱动类名是com.mysql.cj.jdbc.Driver还要额外加serverTimezoneAsia/Shanghai很多同学报错SQLException: The server time zone value就是因为这个。建议按我给的 5.1.49 版本走省心。4.2 登录验证PreparedStatement 与 SQL 注入的正面交锋登录是每个系统都会被答辩老师点名的位置因为网上流传的万能密码 OR 11在拼接 SQL 的场景下真的能绕过验证。如果你在 DAO 里写了字符串拼接那系统就是个筛子。这里给出正确写法public User login(String username, String password) { String sql SELECT user_id, user_name, role FROM t_user WHERE user_name ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { return new User(rs.getInt(user_id), rs.getString(user_name), rs.getString(role)); } } catch (SQLException e) { e.printStackTrace(); } return null; }这段代码的亮点在于?占位符。PreparedStatement会先把 SQL 结构发给数据库预编译再把参数值作为纯数据传进去用户输入哪怕包含单引号、OR 11也不会改变原 SQL 的语义。注意上面用了 JDK 7 的 try-with-resources 写法Connection、PreparedStatement、ResultSet都会自动关闭这条建议在写报告时特意提一句表明你不是只会finally里手写关闭的初学者。关于密码存储别在表里存明文。哪怕项目报告没有硬性要求你也应该在代码里加一层 MD5String hashed DigestUtils.md5Hex(password pet_clinic_salt);这段代码来自 commons-codec 工具包加盐是为了避免两个用户密码相同导致 MD5 结果一致。把 salt 写死在代码里虽然不算安全但答辩时能说出“防彩虹表”这个点就已经超出大部分毕设水平了。4.3 挂号与开药一个事务里扣库存诊所系统的业务亮点在挂号后的号源占用以及开处方后的药品库存扣减。如果只写单表插入代码很简单但答辩老师一定会追问“如果两张表只成功了一张怎么办”。这就是事务的用武之地。以开药为例public boolean createPrescription(Prescription p, int drugId, int quantity) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第1步插入处方主表 String insertPresc INSERT INTO t_prescription(appt_id, drug_id, quantity, total_price) VALUES (?,?,?,?); PreparedStatement ps1 conn.prepareStatement(insertPresc); ps1.setInt(1, p.getApptId()); ps1.setInt(2, drugId); ps1.setInt(3, quantity); ps1.setBigDecimal(4, p.getTotalPrice()); ps1.executeUpdate(); ps1.close(); // 第2步扣减药品库存 String updateStock UPDATE t_drug SET stock stock - ? WHERE drug_id ? AND stock ?; PreparedStatement ps2 conn.prepareStatement(updateStock); ps2.setInt(1, quantity); ps2.setInt(2, drugId); ps2.setInt(3, quantity); int rows ps2.executeUpdate(); if (rows 0) { conn.rollback(); // 库存不足回滚 return false; } ps2.close(); conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn ! null) { conn.setAutoCommit(true); // 还原默认状态 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明第 1 步插入处方记录第 2 步用UPDATE ... SET stock stock - ? WHERE drug_id ? AND stock ?实现安全扣减。注意WHERE里的stock ?是条件更新如果库存不足MySQL 返回影响行数为 0代码走rollback回滚保证不会出现处方写了但库存没扣、或者库存变负数的情况。setAutoCommit(false)之后必须手动commit或rollbackfinally 里再把它恢复成 true 并关闭连接这是纯 JDBC 事务的标准写法也是报告中可以重点画流程图的素材。4.4 JSP 页面渲染JSTL 循环取代 ScriptletJSP 页面里最该避免的就是那种满屏% for (...) { %的老写法。用 JSTL 和 EL 表达式能把代码缩短一半可读性也完全不一样。展示挂号列表时% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered tr th挂号单号/th th宠物名/th th主人/th th就诊日期/th th状态/th /tr c:forEach items${appointmentList} vara tr td${a.apptId}/td td${a.petName}/td td${a.ownerName}/td td${a.apptDate}/td td c:choose c:when test${a.status 0}待诊/c:when c:when test${a.status 1}已完成/c:when c:otherwise已取消/c:otherwise /c:choose /td /tr /c:forEach /table这里的${a.apptId}会调用实体类的 getter 方法所以实体类属性名必须和它对应。常见坑是数据库字段appt_id映射到实体类时写成了apptidEL 表达式就取不到值页面显示空白也不报错——遇到这种情况先检查实体类属性名别急着改数据库。5. 避坑清单从连不上数据库到页面白屏的 6 个排查方向5.1 现象启动 Tomcat 后访问登录页报 500日志里ClassNotFoundException: com.mysql.jdbc.Driver原因mysql-connector-java 的 jar 包没有放进 WEB-INF/lib 目录。IDEA 里常见的坑是你在 Project Structure 的 Libraries 里加了依赖但 Artifact 的 Output Layout 里没有勾选它导致编译能过、运行时找不到类。解决检查WEB-INF/lib下是否有mysql-connector-java-5.1.49.jar。如果没有把 jar 文件直接复制进去重启 Tomcat。以后凡是遇到“编译期好好的运行期报 ClassNotFound”优先查这个目录。5.2 现象数据库里中文都正常页面输入中文后存进去的是问号原因字符集在三层里有一层断了。常见断点包括MySQL 表本身是 latin1、连接 URL 没带characterEncodingutf8、JSP 页面没声明% page contentTypetext/html;charsetUTF-8 %、Servlet 里没写request.setCharacterEncoding(UTF-8)。解决按 表 → 连接 → 页面 → 请求 四个方向逐一排查。最省事的办法是把查询语句改成SHOW CREATE TABLE t_owner确认表默认字符集是utf8mb4然后再检查 DBUtil 里的 URL 和 JSP 头部的 page 指令。写一个过滤器统一处理编码比在每个 Servlet 里重复写更干净public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); }5.3 现象Tomcat 启动了访问http://localhost:8080/pet_clinic/返回 404原因应用上下文路径对不上。一种是 war 包解压后的目录名和访问路径不一致另一种是 IDEA 里 Application Context 写成了根路径/。解决如果你用 war 包部署确定访问路径就是webapps下解压出的目录名例如pet_clinic那 URL 就是/pet_clinic/。IDEA 部署的话打开 Edit Configurations → Deployment查看 Application Context 到底填了什么改完要 Apply 再重新启动不要只点刷新按钮。5.4 现象页面能打开表格数据全部空白控制台也不报错原因这种“静默失败”十有八九是 EL 表达式没解析出数据。要么是实体类没有无参构造方法要么是属性名和 getter 方法对不上。比如实体类里字段叫petName你写${a.petName}但类里只有getpetName()这种不规范的方法名EL 就取不到。解决给实体类加上标准的 getter/setter可以用 IDEA 的 Generate 功能生成别手写。另一个排查思路是在 JSP 页面上临时输出${appointmentList.size()}如果显示 0说明 Servlet 传值就有问题回过去检查 request.setAttribute 的 key 是否和页面上写的一致。5.5 现象登录页提交后直接看到 SQL 报错Unknown column xxx in where clause原因SQL 语句里写的列名和数据库真实列名不一致。最常见的情况是写实体类时把create_time写成了createTimeJava 里驼峰数据库里下划线这俩不会自动转换。解决检查t_appointment等表里的真实列名以DESC t_appointment输出为准。如果你用 IDEA 的数据库面板直接双击表结构看列名更快。建议所有 SQL 都手写并复制粘贴不要凭记忆敲减少这种低级翻车。5.6 现象页面数据量大了以后挂号列表打开要等 3 秒查询变慢原因没有索引。t_appointment表关联pet_id、doctor_id、appt_date但建表语句里只有主键和外键外键在 InnoDB 下会自动建索引但appt_date这种经常作为查询条件的字段没有索引全表扫描速度自然慢。解决为高频查询字段补索引CREATE INDEX idx_appt_date ON t_appointment(appt_date); CREATE INDEX idx_pet_owner ON t_pet(owner_id);这段 SQL 加完后报告里可以写“慢查询优化”的案例先用EXPLAIN SELECT ... WHERE appt_date 2024-06-01看到是全表扫描 typeALL加索引后再看 typerefrows 下降一个数量级。这个点在答辩时是非常加分的实操细节。6. 让系统经得起答辩统计 SQL、包结构与验证顺序6.1 顺手加一个“按月挂号统计”用 SQL 窗口函数撑场面这个统计功能适合放在首页或者管理员的报表页面。传统写法是GROUP BY后按月份分组但如果你想展示“每月挂号数 累计挂号数”窗口函数一行就能搞定SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS monthly_count, SUM(COUNT(*)) OVER (ORDER BY DATE_FORMAT(create_time, %Y-%m)) AS cumulative FROM t_appointment GROUP BY DATE_FORMAT(create_time, %Y-%m);MySQL 5.7 不支持窗口函数你需要把数据库换成 MySQL 8.0或者改用子查询。我建议如果代码包里用的是 SQL ServerOVER窗口函数反而是原生支持的。这个功能不是标题的必需项但绝对是答辩时的主动加分项因为指导老师看到 JSP 项目里居然用了窗口函数会认为你有进阶知识储备。6.2 答辩前的验证顺序按主流程走三遍我自己的习惯是验收前两天用“新环境三连测”第一遍把数据库删掉重导验证 SQL 脚本完整第二遍把 Tomcat 的 work 目录清掉重启确保没有旧编译缓存第三遍关闭电脑再开机只启动 MySQL 和 Tomcat全程录屏。三遍都通了现场演示基本不会翻车。录屏视频放手机里万一现场网络出问题或服务器起不来掏出视频也是一种补救手段。6.3 代码包的整理习惯源代码管理不是只给 Git 用的即使是一个人写毕设也要把代码包整理成“交付级”结构src目录下按entity/dao/servlet/util分包web目录下按admin/doctor/common分页面doc目录放数据库脚本和项目报告。我以前见过不少同学交上来的 zip 解压后一堆杂乱文件连哪个是入口页面都找不到答辩时印象分直接扣掉一截。源码管理这条习惯哪怕以后不写 JSP 了换到任何语言都适用。最后一句话留给一个我踩过的坑有一次我为了省事直接把整个项目文件夹复制到另一台电脑上部署结果忘了改数据库密码现场演示时连不上库场面一度很狼狈。所以密码和端口这类环境变量相关的东西交付前一定要集中检查一遍。希望这些步骤和避坑经验能帮你把这个 JSPSQL 的宠物诊所系统稳稳跑起来答辩顺利。本文还有配套的精品资源点击获取
返回列表