简介:一套面向计算机专业毕业设计场景的 JavaWeb 宠物医院管理系统源码与数据库脚本,评审分 99 分,代码完整、可直接部署运行,适合正在做毕设的学生及需要项目实战练习的学习者,也可用作课程设计或期末大作业。资源为 zip 压缩包,共 113 个文件,体积约 750KB;核心代码包括 40 个 JSP 页面、39 个 Java 类(Servlet/业务逻辑)、11 个 XML 配置以及 1 个 SQL 数据库脚本,另有少量图片、CSS、JS 静态资源,目录结构清晰,便于按模块阅读与二次开发,SQL 脚本便于初始化数据库环境。已有 102 人浏览学习。系统涵盖宠物信息管理、医生管理、用户管理等典型业务模块,整体分层明确,能帮助读者快速理解 JSP+Servlet 的 Web 开发流程、前后端数据交互与数据库设计方法,既可作为毕业设计说明书的功能实现蓝本,也能为课程设计提供可运行的完整参考。
1. JavaWeb 宠物医院管理系统:为什么它能成为高分毕设
前台还在用 Excel 记挂号、病历写在本子上、收费靠口算——这是不少县城宠物医院的真实场景。javaweb 的宠物医院管理系统,要解决的就是把挂号、接诊、处方、收费这一串手工流程搬进 Web 页面。这类项目网上不少,但能拿高分的不多:要么只有增删改查没有业务闭环,要么数据库表之间逻辑混乱。下面按我实际做这类毕设的顺序,把 JavaWeb 技术栈、七张核心表、登录鉴权与挂号接诊的完整链路、答辩前要检查的细节一层层拆开。适合正在选毕设题目、或下载了源码却讲不清楚实现的人,读完之后你能把一个“能跑”的项目讲成“会做”的项目。
2. 技术栈选型与分层架构:JavaWeb 管理系统的稳妥路线
2.1 为什么是 JSP+Servlet+MySQL:一套能把原理讲透的组合
做 JavaWeb 毕设,第一步不是写代码,而是把技术栈定下来。宠物医院管理系统的常见实现组合是 JSP + Servlet + JDBC + MySQL,部署在 Tomcat 上。选它不是因为新,而是因为它把 MVC 的每一层都摊开在明面上:Servlet 生命周期、JSP 九大内置对象、JDBC 的 Connection 获取与释放、PreparedStatement 的预编译机制,每一项都是答辩现场的高频追问点。用 Spring Boot 也能做,但框架自动装配会吞掉太多可讲的细节,对一个目标是“高分”的毕设来说,反而容易陷入“能跑但说不出原理”的尴尬。
| 对比项 | JSP + Servlet + JDBC | Spring Boot + MyBatis |
|---|---|---|
| 上手成本 | 低,绕开框架自动化 | 需要理解 IoC、ORM 约定 |
| 答辩追问面 | Servlet 生命周期、JDBC 事务 | 自动装配、代理机制 |
| 源码可读性 | 每行代码都是自己写的 | 大量注解与封装 |
| 适合人群 | 想把原理讲透的稳定路线 | 时间充裕且熟悉框架 |
如果你的课题明确写了“基于 JavaWeb”,用 JSP+Servlet 是最稳的。如果导师不限制框架,Spring Boot 也完全能做,但你要额外准备“为什么不用 SSM”这类问题的答案,否则很容易被考官顺着自动装配一路问到底。
2.2 包结构与请求走向:把 com.hospital 分成四层
一个完整的宠物医院管理系统源码,包结构应该一眼能看出分层意图。我一般会这样组织:
src/main/java ├─ com.hospital.domain // 实体类:Owner、Pet、Doctor、Appointment ├─ com.hospital.dao // JDBC 数据访问:查询与增删改 ├─ com.hospital.service // 业务层:挂号查重、事务边界 ├─ com.hospital.servlet // 控制器:接收请求、转发页面 ├─ com.hospital.filter // 登录过滤器 ├─ com.hospital.util // DBUtil、MD5Util、DateUtil src/main/webapp ├─ WEB-INF/web.xml ├─ jsp/ // login.jsp、index.jsp、pet/list.jsp ├─ css/ js/ images/依赖方向是 JSP → Servlet → Service → DAO,反向不允许。JSP 里不写 JDBC 代码,Servlet 里不写复杂业务判断,这是答辩老师最先扫一眼的地方。Service 层单独存在的意义有两个:一是承载事务边界,比如挂号时先查重再插入,两步必须在一个事务里;二是给 DAO 留出复用空间,PetDao 的 findById 既可以被挂号流程调用,也可以被病历列表调用。
2.3 web.xml 的 Servlet 映射与初始化参数
如果你的 Servlet 用了 @WebServlet 注解,web.xml 里可以不写映射。但做毕设我更建议保留显式配置,因为答辩时从 web.xml 讲请求入口比从注解讲更直观。
<servlet> <servlet-name>pet</servlet-name> <servlet-class>com.hospital.servlet.PetServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>pet</servlet-name> <url-pattern>/pet</url-pattern> </servlet-mapping>url-pattern 里的/pet决定浏览器地址栏该输入什么,JSP 里表单的 action 必须与它一致,否则会出现 404。这个配置的核心逻辑是“把 URL 和 Java 类绑定”,Tomcat 启动时读到这段就会实例化 PetServlet 并注册到路由表里。修改后需要重启 Tomcat,因为 web.xml 在启动阶段加载。注解方式可以省掉这些步骤,但两者只能选一种,混用的后果是同一个 URL 被映射两次,容器启动时报错。
2.4 把数据库连接串与字符集放进 context 参数
数据库连接信息不要散落在每个 DAO 里,统一放在 web.xml 的 context-param 中:
<context-param> <param-name>jdbcUrl</param-name> <param-value>jdbc:mysql://localhost:3306/pet_hospital?useSSL=false&serverTimezone=Asia/Shanghai</param-value> </context-param> <context-param> <param-name>jdbcUser</param-name> <param-value>root</param-value> </context-param>注意 XML 里&要写成&,这是新手最容易忽略的细节。在 DBUtil 里用getServletContext().getInitParameter("jdbcUrl")读取,比硬编码在 Java 类里好在哪?换数据库、改密码只需要动 web.xml,连编译都不用。如果你把连接参数放在 src 下的 jdbc.properties 里也可以,但用 context-param 能让答辩老师一眼看到你的配置管理意识——这是一个能主动讲出来的亮点。
3. 数据库脚本:宠物医院管理系统的七张核心表与 SQL 初始化
3.1 先梳理业务:挂号、接诊、处方、收费四个环节
数据库设计不能上来就建表,先把业务流程画成一条线:宠物主人带着宠物到前台建档,owner 表和 pet 表落数据;前台挂号,生成一张 appointment 记录;医生接诊,写 medical_record;如果开药,再生成 prescription 并关联 drug 表。四个环节对应七张核心表,外加一张 user 表做后台登录。
为什么要拆这么多表而不是一张大表?因为“一次就诊”涉及多个主体:主人可能有多只宠物,一只宠物有多次就诊,一次就诊有多条处方明细。如果全部塞进一张表,每一次复诊都要重复主人和宠物的全部字段,数据冗余不说,改个电话要 update 几十行。拆表的核心依据是“变化的频率”——主人信息一年改不了一次,但病历每次就诊都在新增,把变化频率不同的数据分开存放,这就是第三范式的朴素理解。
3.2 建表 SQL:从宠物档案到处方明细
先看两张基础表 owner 和 pet:
CREATE TABLE owner ( owner_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE pet ( pet_id INT PRIMARY KEY AUTO_INCREMENT, owner_id INT NOT NULL, pet_name VARCHAR(50) NOT NULL, species VARCHAR(20) NOT NULL COMMENT 'cat/dog/rabbit', breed VARCHAR(50), gender TINYINT DEFAULT 1 COMMENT '1公 2母', birth_date DATE, weight DECIMAL(5,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_pet_owner FOREIGN KEY (owner_id) REFERENCES owner(owner_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;外键加在多的一方,约束“每条宠物档案必须归属一个主人”;species 用字符串而不是数字,是为了让 JSP 页面直接显示,省去字典表转换;weight 用 DECIMAL(5,2),最大 999.99kg,普通猫狗不会越界。owner_id、pet_name 都加 NOT NULL,这是页面表单的必填项,数据库层再兜一道。
挂号表和病历表是核心中的核心:
CREATE TABLE appointment ( appointment_id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT NOT NULL, doctor_id INT NOT NULL, appointment_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL COMMENT '上午/下午', symptom VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '0待诊 1接诊中 2已完成 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_app_date (appointment_date), CONSTRAINT fk_app_pet FOREIGN KEY (pet_id) REFERENCES pet(pet_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status 字段是挂号流程的状态机,0→1→2 是正常路径,3 是取消。我只加了普通索引 idx_app_date 而没有建唯一索引,理由在 4.3 节会展开。病历表是典型的“多次就诊”记录:
CREATE TABLE medical_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, pet_id INT NOT NULL, doctor_id INT NOT NULL, diagnosis VARCHAR(500), treatment VARCHAR(1000), advice VARCHAR(500), record_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_record_app FOREIGN KEY (appointment_id) REFERENCES appointment(appointment_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.3 初始化数据:管理员账号、医生与药品
数据库脚本不能只有建表语句,没有初始数据的系统登录不进去。管理员账号、基础医生、常用药品都要在脚本里预置:
INSERT INTO user (username, password, role) VALUES ('admin', MD5('123456'), 'ADMIN'); INSERT INTO doctor (name, specialty, phone, title) VALUES ('李明', '外科', '13800000000', '主治医师'); INSERT INTO drug (drug_name, spec, unit, price, stock) VALUES ('阿莫西林', '250mg*24粒', '盒', 18.00, 120);密码不要明文存放,用 MySQL 自带的 MD5 函数加密,Java 端配套一个 MD5Util 工具类做同样处理。医生和药品的量不用多,3 个医生、5 种药品足够支撑演示和答辩,但字段必须完整。特别提醒:price 用十进制不用浮点,浮点运算在金额上会出 0.1+0.2=0.30000000000000004 的精度问题,DECIMAL(10,2) 才是正道。
3.4 数据库脚本的导入与导出:命令行与 IDEA 两种方式
拿到数据库脚本后,先建库再导入:
mysql -uroot -p CREATE DATABASE pet_hospital DEFAULT CHARACTER SET utf8mb4; exit mysql -uroot -p pet_hospital < pet_hospital.sql命令行导入时加上--default-character-set=utf8mb4可以避免 Windows 下脚本中的中文变成乱码。如果你习惯用 IDEA 操作,右侧 Database 面板连接到 MySQL 后,执行 SQL 脚本即可。反过来,需要修改表结构后导出脚本时,IDEA 里右键数据库 → SQL Scripts → Save to New File,导出时勾选 DROP statements,方便重复初始化。这组命令对应了检索里常说的“idea导出数据库脚本”,答辩时被问到“数据库怎么来的”可以直接讲清楚整个流程。
4. 源码骨架到功能闭环:登录、挂号、接诊的 Servlet 实现
4.1 登录逻辑:用 Session 记录登录状态
一个 JavaWeb 项目完整案例一定从登录开始。LoginServlet 的 doPost 核心代码如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || password == null || username.trim().isEmpty() || password.trim().isEmpty()) { request.setAttribute("error", "用户名和密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } User user = userDao.findByUsernameAndPassword(username, MD5Util.md5(password)); if (user == null) { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); }setCharacterEncoding 必须在读取任何参数之前调用,放在第一行就是这个原因。查不到用户时用 forward 转发到 login.jsp 并携带 error 属性,登录成功后用 sendRedirect 重定向到首页——这是两个关键区别:forward 是服务器内部跳转,地址栏不变,适合带回错误信息;sendRedirect 会发起新的 HTTP 请求,地址栏变为 index.jsp,能避免刷新页面时重复提交登录表单。
4.2 路径级鉴权:Filter 放行规则与拦截范围
登录之后不能只靠每个 Servlet 自己判断,用 Filter 在入口统一拦截。web.xml 里这样注册:
<filter> <filter-name>authFilter</filter-name> <filter-class>com.hospital.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>authFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>url-pattern 写成/*表示拦截所有请求。注意,/和/*差别很大:/只匹配路径,/*连 JSP 页面也拦,这正是我们需要的。Filter 的实现按白名单放行:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); String uri = request.getRequestURI(); if (uri.endsWith("/login.jsp") || uri.contains("/login") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".png") || uri.endsWith(".jpg")) { chain.doFilter(request, response); return; } if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); }getRequestURI() 拿到的是不带域名的路径,比如/pet_hospital/pet/list。白名单里放行登录页、登录接口和静态资源,其余请求一律检查 Session。这里有个细节:request.getSession(false)传了 false,意思是“如果没有 Session 就返回 null 而不是创建一个新的”,避免恶意请求在未登录状态下大量生成无用的 Session 对象。
4.3 挂号实现:事务包裹的查重与插入
挂号是宠物医院管理系统里最体现业务逻辑的功能。一个完整案例的含金量就在这里:不是简单 insert 一条记录,而是要先查重、再插入、且两步必须原子化。
public boolean create(Appointment appointment) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); boolean conflict = appointmentDao.existsByDoctorAndSlot( appointment.getDoctorId(), appointment.getAppointmentDate(), appointment.getTimeSlot(), appointment.getStatus() ); if (conflict) { conn.rollback(); return false; } appointmentDao.insert(appointment); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { DBUtil.close(conn, null, null); } }setAutoCommit(false) 关闭自动提交后,查重和插入变成一个事务,任何一步失败都回滚。这里为什么不用数据库唯一索引来防重?因为业务规则是“同一医生同一时段”冲突,但取消后的记录不应该继续占位,status 字段一变规则就复杂了。代码层查重用 where 条件精确控制,比唯一索引更灵活。existsByDoctorAndSlot 里的 status 参数用来排除已取消的记录,这是容易写错的地方——如果直接查 doctor_id + date + time_slot,取消过的号会让该时段永久无法再挂。
4.4 列表页数据回显:PetServlet 与 JSTL 渲染
后台列表页是另一个高频考点。以宠物档案列表为例:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String keyword = request.getParameter("keyword"); List<Pet> pets = petService.findByNameOrOwner(keyword); request.setAttribute("petList", pets); request.setAttribute("keyword", keyword); request.getRequestDispatcher("/jsp/pet/list.jsp").forward(request, response); }keyword 参数回传给 JSP 的意义在于:用户搜索“金毛”后点翻页或筛选,搜索框仍保留“金毛”两个字,否则每次操作都要重新输入。JSP 页面用 JSTL 循环渲染,避免在页面里写 Java 代码:
<c:forEach var="pet" items="${petList}"> <tr> <td>${pet.petId}</td> <td>${pet.petName}</td> <td>${pet.species}</td> <td><a href="record?petId=${pet.petId}">查看病历</a></td> </tr> </c:forEach>JSTL 的 c:forEach 等价于 for-each 循环,items 对应 request.setAttribute 里放入的 petList。注意实体类必须提供 getter,EL 表达式 ${pet.petName} 实际调用的是 getPetName()。链接 record?petId=${pet.petId} 是页面传参的标准写法,点击后由 RecordServlet 处理并跳到该宠物的历史病历页,形成“档案→病历”的完整浏览链。
5. 高分毕设的最后一公里:验证清单、答辩考点与排错
5.1 跑起来之前先过一遍六项检查
| 检查项 | 要求 | 出错典型表现 |
|---|---|---|
| JDK 与 Tomcat | JDK 1.8、Tomcat 9 | 404;控制台 NoClassDefFoundError |
| 依赖 jar | mysql-connector-java 在 WEB-INF/lib | ClassNotFoundException: Driver |
| 数据库连接串 | 库名、账号密码、时区参数 | Access denied;时区异常 |
| 字符集 | JSP、web.xml、连接串均为 UTF-8 | 页面中文全是问号 |
| 初始化数据 | 管理员账号已插入 | 登录提示用户名或密码错误 |
| 浏览器控制台 | F12 无红色 500 | 列表页打不开 |
lib 目录缺 jar 是下载的源码最常见的问题,项目部署后启动不报错但一访问数据库就抛 ClassNotFoundException,优先检查这里。
5.2 三个高频翻车点
第一个是时区问题。MySQL 8.0 连接串必须带serverTimezone=Asia/Shanghai,否则报 “The server time zone value” 错误。第二个是中文乱码,要同时满足三个位置:JSP 页面顶部pageEncoding="UTF-8"、Servlet 里request.setCharacterEncoding("UTF-8")、数据库表DEFAULT CHARSET=utf8mb4,三处缺一不可。第三个是挂号查重逻辑,如果建表时在 appointment 上加了UNIQUE KEY (doctor_id, appointment_date, time_slot),最初看起来没问题,但一旦有取消操作,这个位置就被永远占住,后续无法再挂——查重逻辑上必须排除 status=3 的记录,这也是 4.3 节代码里 status 参数存在的原因。
5.3 答辩时值得主动讲的三个技术点
答辩不要等老师问,好的展示是领着考官走。第一个主动讲三层架构的边界:JSP 不写 JDBC、Servlet 不写业务、Service 管理事务,指向自己源码里三处具体位置。第二个主动讲 PreparedStatement 的预编译机制:它怎么防 SQL 注入——参数占位符在执行阶段不参与 SQL 语法解析,所以' or '1'='1只会被当成普通字符串传入。第三个主动讲下“外键和索引的区别”:外键是约束,防止插入不存在的主人;索引是加速,让按日期查挂号不用全表扫描。这三个点每个都能展开 3 到 5 分钟的追问,而这正是高分项目和普通“能跑”项目拉开差距的地方。
本文还有配套的精品资源,点击获取