news 2026/10/7 13:45:19

Jsp+Servlet医院预约挂号系统开发详解:从部署到防超挂全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Jsp+Servlet医院预约挂号系统开发详解:从部署到防超挂全解析

简介:这是一份基于Jsp+Servlet的医院预约挂号系统完整源码包,主要面向Java初学者、课程设计以及毕业设计人群,用来帮助理解传统JavaWeb的分层开发方式与前后端交互过程。压缩包整体约31.95MB,共收录1451个文件,其中比较有代表性的是400个js交互脚本、148个html静态页面、78个jsp动态视图、62个jar依赖包、40个css样式文件以及数据库SQL脚本,基本覆盖了前端展示、后端逻辑、样式美化和数据初始化等环节。系统前台具备首页展示、用户注册、医生查询、公告信息等模块,后台则包含医院信息管理、科室信息管理、医生信息管理、会员信息管理和系统公告管理,功能设计齐全,拿到后可直接在Eclipse或IDEA中导入运行。资源同时提供详细部署教程、相关软件工具和MySQL5.7数据库脚本,配合Tomcat7/8/9即可快速搭建环境,适合作为课程设计或入门实战项目参考。目前已有134人学习下载,值得有类似需求的开发者收藏。

1. 什么是「Jsp+Servlet医院预约挂号系统」:一个部署五分钟就可能翻车的老实项目

如果你第一次把这个系统部署到 Tomcat 上,最可能遇到的不是挂号页面,而是ClassNotFoundException: javax.servlet...,紧接着是数据库连接失败、JSP 里中文乱码、登录后 404,最后你会怀疑是不是自己漏了什么配置。其实大部分时候系统本身没问题,是你的 Tomcat 版本、JDBC 驱动和路径写法在捣乱。“Jsp+Servlet医院预约挂号系统”是 Java Web 课程设计和毕业设计里最常见的一种项目形态:JSP 负责页面展示,Servlet 负责接收请求和控制跳转,JDBC 直接操作 MySQL 数据库。它没有 Spring Boot 的自动装配,所以每一条请求链路都看得见、改得动、讲得清。这套东西解决的是“从零实现患者端预约、后台管理、号源扣减”的完整闭环,适合课程设计、毕业设计,也适合想补 Java Web 底子的人。

2. 先把骨架立起来:六张表、目录结构与配套环境的落地细节

很多人一上来就写 Servlet,写到一半发现缺表、缺字段、页面路径乱成一团。我一般先把数据模型和项目目录定死,再写业务代码,这样后面基本不会推翻重来。

2.1 六张表就够了:最小可用的预约挂号数据模型

一个能跑通完整流程的“Jsp+Servlet医院预约挂号系统”,核心数据表可以控制在六张:科室表、医生表、排班表、预约记录表、患者表、管理员表。科室和医生是基础数据,排班表承载号源,预约记录表是业务核心,患者表和管理员表分别支撑前端登录和后台登录。

CREATE DATABASE IF NOT EXISTS hospital_register DEFAULT CHARACTER SET utf8mb4; USE hospital_register; CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, description VARCHAR(255) ) ENGINE=InnoDB; CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, name VARCHAR(30) NOT NULL, title VARCHAR(30), intro VARCHAR(500), status TINYINT DEFAULT 1 COMMENT '1正常出诊 0停诊', CONSTRAINT fk_doctor_dept FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINE=InnoDB; CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date VARCHAR(10) NOT NULL COMMENT '出诊日期,格式yyyy-MM-dd', time_bucket VARCHAR(10) NOT NULL COMMENT 'MORNING上午 / AFTERNOON下午', total INT NOT NULL DEFAULT 30, booked INT NOT NULL DEFAULT 0, CONSTRAINT fk_schedule_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(id) ) ENGINE=InnoDB; CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) NOT NULL, id_card VARCHAR(18), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, schedule_id INT NOT NULL, appt_no INT NOT NULL COMMENT '同一排班内的排队序号', status TINYINT DEFAULT 0 COMMENT '0已预约 1已取消 2已完成 3已过期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_appt_patient FOREIGN KEY (patient_id) REFERENCES patient(id), CONSTRAINT fk_appt_schedule FOREIGN KEY (schedule_id) REFERENCES schedule(id) ) ENGINE=InnoDB; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB;

几个关键参数说明:schedule.total和schedule.booked是防超挂的核心,预约占号时不是“插入一条记录”就完事,而是基于这两个字段做条件更新,后面会专门讲。work_date我用VARCHAR(10)存字符串而不是DATE类型,因为“yyyy-MM-dd”的字符串比较和LocalDate.parse的转换都直观,也不容易踩时区坑。appointment.status用TINYINT数字枚举,不要用中文枚举字符串,否则乱码和统计都会很痛苦。字符集统一用utf8mb4,能存 emoji 表情的患者昵称,也避免中文变问号的问题。除此之外,患者登录密码在实际项目中一定要加盐哈希,至少用BCrypt或SHA-256,这里建表只保留字段位。

2.2 目录结构:Eclipse 动态 Web 项目和 Maven 的等价关系

这套系统最常见的目录结构是传统 Dynamic Web Project。src下放 Java 源码,WebContent下放 JSP、CSS、JS 和WEB-INF/web.xml。如果你用 IDEA 或者 Maven,记住一个等价关系:WebContent等价于src/main/webapp,JSP 放错位置会导致 404,而且是那种“文件明明存在但访问不到”的 404。

hospital_register/ ├── src/ │ ├── com/hospital/entity/ # 实体类:Doctor、Schedule、Patient │ ├── com/hospital/dao/ # JDBC 数据访问:DoctorDao、ScheduleDao │ ├── com/hospital/service/ # 业务层:挂号事务、退号事务 │ ├── com/hospital/servlet/ # 控制器:LoginServlet、BookServlet │ └── com/hospital/filter/ # 登录拦截、编码过滤器 ├── WebContent/ │ ├── index.jsp # 首页/登录页 │ ├── patient/ │ │ ├── register.jsp # 患者注册 │ │ └── my_appointment.jsp # 我的预约列表,也就是个人信息展示页 │ ├── admin/ │ │ ├── dept_list.jsp # 科室管理 │ │ └── doctor_list.jsp # 医生管理 │ ├── css/ │ ├── js/ │ └── WEB-INF/ │ └── web.xml └── WebContent/WEB-INF/lib/ └── mysql-connector-java-8.0.33.jar

Servlet 配置有两条路:@WebServlet注解或web.xml映射,二选一即可。很多翻车现场是两个都写了,导致同一个 Servlet 被注册两次,表现为登录后随机 500 或重复执行。我习惯用注解,简单直接,web.xml只留欢迎页和会话超时配置。这里还需要特别提醒:如果你用的是 Tomcat 10 及以上,老项目的javax.servlet包已经改名为jakarta.servlet,一启动就是 ClassNotFoundException。最省事的方案是直接换 Tomcat 9,后面还会提到。

后台管理页面如果不想自己写图标,常见做法是直接在 JSP 里静态引入 Element UI 的 CDN,也就是把 CSS 和 JS 的<link>、<script>标签写死在公共头部 JSP 片段里。不要在 JSP 项目里跑 npm 构建,Java Web 项目的前端资源定位和前后端分离项目完全不同,静态引入反而是最不容易出错的。到了这一步,骨架已经立好,下一章开始穿业务链路。

3. 从登录到号源扣减:Servlet+JSP 的核心业务链路怎么穿起来

骨架搭完,接下来是整条业务链路:患者注册登录、浏览科室医生、提交预约、查看个人预约记录、取消预约。这条链路每个点都能拆成一个 Servlet + 一个 JSP 对。

3.1 登录会话与拦截:用 Session 管住“我是谁”

登录是第一个 Servlet,也是 Session 的第一个使用场景。登录成功把患者 ID 和用户名放进 Session,后续所有预约操作从 Session 里取当前用户,而不是信任前端传过来的patientId参数。这点很重要,因为前端传来的参数可以被篡改,你永远不知道请求背后是谁。

@WebServlet("/login.do") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); PatientDao dao = new PatientDao(); Patient patient = dao.findByUsernameAndPassword(username, password); if (patient == null) { // 登录失败回到登录页,带一个错误标记 resp.sendRedirect(req.getContextPath() + "/index.jsp?error=1"); return; } HttpSession session = req.getSession(); session.setAttribute("patient", patient); resp.sendRedirect(req.getContextPath() + "/patient/schedule_list.jsp"); } }

逻辑说明:req.getContextPath()返回部署后的上下文路径,比如/hospital_register。如果写死"/hospital_register",换一个部署名整个项目就全 404 了。登录失败用重定向而不是 forward,是为了防止刷新页面时表单重复提交。Session 默认 30 分钟无操作失效,可以在web.xml里用<session-config>调整会话超时参数。这里要注意,登录密码校验应该调用 DAO 里的哈希比对方法,不要在 Servlet 里拼 SQL。

光有登录还不够,没登录的人照样能直接访问schedule_list.jsp看到排班、甚至直接 POST 一个book.do请求占号。所以还需要一个登录拦截过滤器,保护/patient/*和/admin/*下面的资源。

@WebFilter("/patient/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(false); if (session == null || session.getAttribute("patient") == null) { HttpServletResponse resp = (HttpServletResponse) response; resp.sendRedirect(req.getContextPath() + "/index.jsp"); return; } chain.doFilter(request, response); } }

逻辑说明:req.getSession(false)表示“如果当前没有 Session 就返回 null,而不是新建一个”。很多新手直接写req.getSession(),结果没登录的人访问一次页面也会被创建出 Session,加大服务器内存压力。拦截路径只要写在/patient/*上,index.jsp、静态 CSS 都不会被挡住,避免出现“登录页样式丢了”的尴尬。

3.2 预约挂号事务:最容易被问倒的防超挂问题

预约挂号是整个系统里最核心的业务,也是最容易在答辩时被老师追问的环节。很多第一版实现是“先查schedule.booked是否小于total,再 INSERT 一条预约记录”。这个写法单机单用户没问题,一旦有两个请求同时查到booked=29且total=30,就会产生两条预约记录,号源就超挂了。解决思路不是加synchronized,因为 Servlet 是多实例部署时锁不住;而是用一条条件更新语句把“判断号源”和“扣减号源”合并成一个原子操作。

public boolean book(Patient patient, int scheduleId) { Connection conn = null; PreparedStatement update = null; PreparedStatement insert = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:只有 booked 小于 total 时才会真的更新成功 String lockSql = "UPDATE schedule SET booked = booked + 1 " + "WHERE id = ? AND booked < total"; update = conn.prepareStatement(lockSql); update.setInt(1, scheduleId); int rows = update.executeUpdate(); if (rows != 1) { conn.rollback(); return false; } String insertSql = "INSERT INTO appointment(patient_id, schedule_id, appt_no, status) " + "VALUES(?, ?, ?, 0)"; insert = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); insert.setInt(1, patient.getId()); insert.setInt(2, scheduleId); insert.setInt(3, 0); insert.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn != null) conn.rollback(); log.error("预约失败,scheduleId={}", scheduleId, e); return false; } finally { DBUtil.close(insert, update, conn); } }

这段逻辑说明很关键:UPDATE ... WHERE booked < total这一行,MySQL 在行锁层面保证同一时刻只有一个事务能改这一行的booked字段,两个并发请求最终只有一个rows=1,另一个rows=0直接回滚。这就是乐观并发控制里最常用的一招,写接口比“先查后插”少一次查询,也天然防超挂。appt_no这里先置 0,正式系统里应该在同一个事务里查当前已约人数加 1,或者用数据库窗口函数生成;如果只是课程设计,可以在插入后查自增主键作为排队序号,简单但不严谨。

3.3 我的预约与退号:个人信息展示页不只能看,还要能改状态

“我的预约”页面是典型的 JSP 个人信息展示页,它要做的不是干巴巴地输出列表,而是把预约记录、科室、医生、日期、号序、状态都展示出来,并且提供“取消预约”按钮。退号并不是把记录DELETE掉,而是把appointment.status改成1,同时把schedule.booked减回去,这两个操作也必须放在同一事务里。

@WebServlet("/cancel.do") public class CancelServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(false); Patient patient = (Patient) session.getAttribute("patient"); int apptId = Integer.parseInt(req.getParameter("apptId")); boolean ok = appointmentService.cancel(patient.getId(), apptId); resp.sendRedirect(req.getContextPath() + "/patient/my_appointment.jsp?cancel=" + ok); } }

service 层实现里,先做归属校验:UPDATE appointment SET status = 1 WHERE id = ? AND patient_id = ? AND status = 0。这个条件里的patient_id是防越权操作的关键参数,用户只能取消自己的预约。影响行数为 1 时,才去执行UPDATE schedule SET booked = booked - 1。注意booked不能减成负数,SQL 里要加AND booked > 0,否则会出现脏数据。页面提交按钮要带apptId,这个参数来自列表页循环输出,不要用隐藏域存整个对象,只存 ID 就行。

到了这里,登录、预约、取消的主链路已经通了。接下来一章是把系统“喂饱”的关键:编码、连接、时间、分页这四个参数,别看它们不起眼,几乎所有的运行期异常都出在这四类参数上。

4. 四个必调参数:编码、连接、日期与分页的实际取值

JSP+Servlet 项目没有 Spring Boot 的自动配置,所有参数都得自己写对。我维护过几个这种老项目,总结下来真正影响存活率的就四类:编码、数据库连接、日期处理、分页。

4.1 编码过滤器:一个 Filter 解决 80% 的中文乱码

中文乱码是 JSP 项目最常见的翻车点,而且乱码分三种:请求参数乱码、响应页面乱码、数据库存储乱码。解决请求和响应乱码的最稳做法是写一个全局过滤器,拦截所有请求,在 Servlet 读取参数之前把编码设置好。

@WebFilter("/*") public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8"); chain.doFilter(request, response); } }

逻辑说明:request.setCharacterEncoding只影响 POST 表单参数的解码;GET 请求的查询字符串编码取决于 Tomcat 的URIEncoding配置。Tomcat 8.0 以后默认URIEncoding就是 UTF-8,所以只要你在 JSP 页面顶部也声明了pageEncoding="UTF-8",GET 和 POST 两边都不会乱。response.setContentType("text/html; charset=UTF-8")是设置响应体的字符集,如果不写,Tomcat 默认按 ISO-8859-1 输出中文,浏览器就会看到一堆问号。这里有个血泪经验:这个过滤器必须注册在第一个位置,如果你有多个 Filter,编码过滤器要放在最前面,否则其他过滤器已经读取了参数,后面的字符集设置就不生效了。

4.2 JDBC 连接参数:Driver 类名和 url 的两代差异

MySQL 5.x 和 MySQL 8.x 的 JDBC 驱动、URL 参数差别很大,这也是ClassNotFoundException和连接超时的高发来源。直接看参数表:

环境MySQL 5.7MySQL 8.0
驱动类名com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver
连接 URLjdbc:mysql://127.0.0.1:3306/hospital_register?useUnicode=true&characterEncoding=utf8同左,并追加serverTimezone=Asia/Shanghai
驱动 jar 放置WEB-INF/lib下,且不能在打包时遗漏同左
String url = "jdbc:mysql://127.0.0.1:3306/hospital_register" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai";

这里characterEncoding=utf8必须写,它保证 Java 程序写进数据库的中文不是乱码。useSSL=false是为了避免 MySQL 8 默认 SSL 握手导致的连接警告。serverTimezone=Asia/Shanghai是 MySQL 8 的强制参数,不写会直接报The server time zone value 'XXX' is unrecognized。

如果是课程设计这种小并发场景,不引入连接池也能跑,但每次请求都DriverManager.getConnection在高并发下会慢得明显。我一般会用一个最简单的连接池配置,常见参数如下:

参数建议值说明
initialSize5启动时预建连接数
maxTotal20最大连接数,超过会等待
maxIdle10最大空闲连接数
maxWaitMillis3000拿不到连接时的等待毫秒数,超时抛异常
validationQuerySELECT 1连接池回收前验证连接是否可用

连接池参数不是越大越好。我曾经把maxTotal调到 100,结果 MySQL 默认连接数上限是 151,系统没高并发反而因为连接堆积把自己搞垮。小系统maxTotal=20足够,关键是连接用完要close(),否则池子迟早被耗尽。

4.3 日期处理:字符串日期比 Date 更省心

JSP 项目里日期处理最容易出幺蛾子。SimpleDateFormat是非线程安全的,如果把它定义成 Servlet 的成员变量,两个并发请求同时parse就会出现诡异的NumberFormatException。另外,浏览器表单提交的日期格式是yyyy-MM-dd,直接用java.util.Date接收再格式化,很容易掉进时区坑。我的做法是排班日期和预约日期都用字符串传输和存储。

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String workDate = req.getParameter("workDate"); try { LocalDate d = LocalDate.parse(workDate, fmt); // 只允许预约今天和未来的日期 if (d.isBefore(LocalDate.now())) { req.setAttribute("error", "不能预约过去的日期"); req.getRequestDispatcher("/patient/schedule_list.jsp").forward(req, resp); return; } } catch (DateTimeParseException e) { req.setAttribute("error", "日期格式必须为yyyy-MM-dd"); // 回到列表页重新选择 }

逻辑说明:LocalDate.parse在收到2024-02-30这种非法日期时会抛DateTimeParseException,必须在接口层捕获并提示,不要让它一路穿透到数据库层。字符串比较"2025-01-10".compareTo("2025-01-09")的结果和日期先后一致,所以work_date >= CURDATE()这种 SQL 判断也可以直接写字符串比较,数据库索引也能用上。日期在 JSP 页面展示时不要做任何格式化,直接输出字符串,避免后端Date类型在页面渲染时多出一个00:00:00或者时区偏移。

4.4 分页参数:预约列表的 limit 写法

患者预约记录会随着使用变多,不能一页全查出来。分页的核心是pageNo和pageSize两个参数,SQL 用LIMIT offset, pageSize实现。常见的翻车是pageNo从 0 开始还是从 1 开始不一致,前端传 1,后端却用 0 当 offset,导致第一页数据丢掉。

SELECT a.id, d.name AS dept_name, doc.name AS doctor_name, s.work_date, s.time_bucket, a.appt_no, a.status, a.create_time FROM appointment a JOIN schedule s ON a.schedule_id = s.id JOIN doctor doc ON s.doctor_id = doc.id JOIN department d ON doc.dept_id = d.id WHERE a.patient_id = ? ORDER BY a.create_time DESC LIMIT ?, ?;

后端计算 offset 时int offset = (pageNo - 1) * pageSize,pageNo从 1 开始,pageSize默认 10。对于pageNo小于 1 的非法输入要重置为 1,pageSize超过 50 要截断,防止一次拉走全表。ORDER BY a.create_time DESC是预约列表的排序关键,没有它分页会重复或漏数据。如果列表还要显示“已取消/已完成”状态,可以在 SQL 后面加AND a.status = ?,但这个筛选条件要在分页总数 SQL 里也写一遍,否则页数统计会错。

5. 部署与避坑:war 包、Tomcat 版本与五个高发问题排查

代码写得再顺,部署不上等于零。JSP+Servlet 项目的部署方式相对固定,但版本兼容问题特别多,这一章把部署步骤和最高发的五个问题一次说清。

5.1 从源码到 war 包:两种打包路径

最传统的做法是在 Eclipse 或 IDEA 里导出 war 包,然后丢进 Tomcat 的webapps目录。IDEA 用户一般先配置 Artifacts,Build > Build Artifacts > Build,输出目录里就是hospital_register.war。Eclipse 用户直接右键项目 > Export > WAR file即可。如果你用 Maven 管理依赖,mvn clean package生成 war 包,但要注意pom.xml里packaging必须是war而不是jar,否则 Tomcat 只会给你一个解压不出来的死包。

# 将 war 包部署到 Tomcat cp hospital_register.war $CATALINA_HOME/webapps/ cd $CATALINA_HOME/bin ./startup.sh # 默认访问地址 # http://127.0.0.1:8080/hospital_register/

war 包部署后 Tomcat 会自动解压。如果之前部署过旧版本,第二次覆盖时要把旧的解压目录一起删除,只覆盖 war 包会导致类文件新旧混用,出现“改了代码不生效”的玄学问题。这是部署后的第一坑,而且你几乎必然会遇到。

5.2 高发问题一:Tomcat 10 下 javax.servlet 包直接 ClassNotFoundException

现象:项目在 Tomcat 9 上好好的,换成 Tomcat 10 启动后,页面一访问就报java.lang.ClassNotFoundException: javax.servlet.ServletException或NoClassDefFoundError。原因:Tomcat 10 起把 Servlet 规范从 Java EE 迁移到 Jakarta EE,所有javax.servlet.*都改成了jakarta.servlet.*,老代码和依赖老 Servlet API 的第三方 jar 全部失效。解决:最省事的做法是回到 Tomcat 9 部署,JSP+Servlet 老项目不要轻易迁移到 Tomcat 10+。如果必须用,把源码里所有import javax.servlet批量替换成import jakarta.servlet,并且移除WEB-INF/lib里自带的旧servlet-api.jar,否则两个 jar 的类冲突会让 Tomcat 直接拒绝启动。没有后悔药可吃,选版本前先确认环境。

5.3 高发问题二:MySQL 驱动加载失败与 timezone 报错

现象:启动不报错,但一执行 DAO 查询就报ClassNotFoundException: com.mysql.jdbc.Driver,或者The server time zone value '中国标准时间' is unrecognized。原因:驱动 jar 没有真正进入WEB-INF/lib,或者 MySQL 8 用了 MySQL 5 的驱动类名,以及 URL 缺少serverTimezone参数。解决:把mysql-connector-java-8.0.33.jar放进WEB-INF/lib,确认不是只放在 IDE 的 classpath 里;MySQL 8 的连接 URL 必须带serverTimezone=Asia/Shanghai;MySQL 5.7 则继续用com.mysql.jdbc.Driver。判断驱动是否生效,可以打开解压目录看WEB-INF/lib里有没有 jar 文件,不要只盯 IDE 里的引用列表。

5.4 高发问题三:登录后跳转 404,CSS 和图片全部丢失

现象:登录成功后跳转到http://127.0.0.1:8080/patient/my_appointment.jsp显示 404,或者页面 HTML 出来了但 CSS、图片全是 404。原因:页面里用了相对路径href="css/style.css",在hospital_register/patient/这个 URL 下,浏览器会解析成hospital_register/patient/css/style.css,自然找不到。显示图片同理,JSP 里图片路径必须基于上下文根。解决:所有静态资源和页面跳转统一在 JSP 里加一个<base>标签,或者直接用 JSP 表达式拼绝对路径。

<% String ctx = request.getContextPath(); %> <base href="<%=ctx%>/"> <link rel="stylesheet" href="css/style.css"> <img src="images/doctor.png" alt="医生头像">

表单提交也一样道理,action必须写绝对路径:

<form action="${pageContext.request.contextPath}/book.do" method="post">

${pageContext.request.contextPath}是 JSP EL 表达式,等价于request.getContextPath()。不要写死/hospital_register/book.do,换个部署名又是 404。这是我见过最多的一类问题,根源就是“相对路径在子目录页面里失效”,养成所有路径都带上下文根的习惯能一次治好。

5.5 高发问题四:双击提交按钮生成两条预约记录

现象:患者点了一次“提交预约”,数据库里出现两条 appointment 记录,号源被重复扣减。原因:第一次提交还没返回,用户又点了一次;或者页面用 JS 做了window.location.reload()想在“加载完后刷新一次”拿到最新状态,把上一次 POST 又提交了一遍。解决:前端提交后立刻把按钮置灰禁用,后端在book()里用事务内的条件更新兜底,提交成功后用sendRedirect而不是forward,刷新页面只会重放 GET 请求,不会重复 POST。不要在预约提交页面里写location.reload(),这个函数在 GET 页面用可以,在表单页用就是在制造重复数据。

5.6 高发问题五:数据库里中文变成问号

现象:页面显示正常,但打开数据库表看到患者姓名是???。原因:JSP 页面编码、请求编码、连接 URL 编码、数据库表字符集四层里面有一层不是 UTF-8。解决:建库时用DEFAULT CHARACTER SET utf8mb4;连接 URL 带characterEncoding=utf8;JSP 文件头部声明pageEncoding="UTF-8";最后用第一节的编码过滤器统一收口。这四层我一般按“数据库表 → 连接 URL → JSP 声明 → 全局 Filter”的顺序排查,每改一层就测一次,基本能定位到具体是哪层漏了。

6. 验证与进阶:用自测脚本证明系统能挂号、能退号、不超挂

功能写完不是终点,要能证明它稳定才算真正完成。我每次交付前都会跑一遍自测脚本,覆盖“未登录拦截、登录、预约、退号”四条链路。这里分享一个简单的 bash 自测方式,不用装额外工具,curl 加 grep 就够。

#!/usr/bin/env bash BASE="http://127.0.0.1:8080/hospital_register" # 1. 未登录访问受保护资源,应该跳回登录页,而不是 200 curl -s -o /dev/null -w "%{http_code}\n" "$BASE/patient/my_appointment.jsp" # 2. 模拟登录并保存会话 Cookie curl -s -c cookies.txt -d "username=test&password=123456" "$BASE/login.do" # 3. 提交预约请求,判定是否提示成功 curl -s -b cookies.txt -d "scheduleId=1" "$BASE/book.do" | grep -q "预约成功" && echo "book ok" # 4. 退号后再查一次列表,状态应变为“已取消” curl -s -b cookies.txt -d "apptId=1" "$BASE/cancel.do" | grep -q "取消成功" && echo "cancel ok"

测试时注意第一次请求返回的 200 可能只是登录页本身的状态码,所以要用grep去匹配页面里的关键文案,而不是只看 HTTP 状态码。另外,在跑脚本前先把schedule.total临时改成 1,然后并发请求两次book.do,能直观验证防超挂是否生效。这个方法比手动点页面快很多,也适合在答辩前做冒烟测试。

如果想再进阶一步,可以做两个小优化。一是把管理端和患者端的权限用两个 Filter 分开,避免患者直接访问/admin/*的后台页面;二是把挂号数据按科室做一个统计页面,SQL 里GROUP BY department.name加COUNT(appointment.id),这个统计在答辩时非常加分。还有一点,如果要做诊室平面图或候诊区可视化,不要在 JSP 里写死图片坐标,那是维护黑洞;常见做法是把桌位或房间的坐标存成一张数据表,页面循环输出位置参数,图片只负责底图渲染,这样调整点位不用改页面。

我自己以前每次交付这类 Jsp+Servlet 项目,总在 Tomcat 版本和路径上栽跟头。后来养成了一个习惯:部署前先确认外部环境,再跑一遍自测脚本,最后才提交代码。这个流程帮我省下大量半夜排错的时间,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 13:45:00

基于RTL8367S与XS2184的工业PoE交换机硬件设计实战

搞工业PoE交换机这事儿&#xff0c;说难不难&#xff0c;说简单也不简单。前两年我用Realtek的RTL8367S搭配国产的XS2184&#xff0c;完整做了一版8口千兆工业级PoE交换机&#xff0c;从原理图到PCB&#xff0c;再到高低温、浪涌测试&#xff0c;一路踩了不少坑。这篇文章就把整…

作者头像 李华
网站建设 2026/10/7 13:44:44

仿真强化学习完整流程:环境搭建、训练调参与策略部署实践

仿真强化学习这个方向&#xff0c;这两年热度一直没降过&#xff0c;但真正能把“仿真”和“强化学习”串成一套顺手流程的人&#xff0c;其实不多。我最近把内部代号叫 Microduck 的项目完整跑了一遍&#xff0c;核心就是搭建“仿真环境 强化学习训练”的闭环流程&#xff1a…

作者头像 李华
网站建设 2026/10/7 13:44:17

大模型落地卡在数据治理?RAG与Excel模板导入的工程实践

1. 从"数据喂不动模型"说起&#xff1a;大模型落地最真实的卡点 过去一年多&#xff0c;我参与过好几个企业级大模型的落地项目&#xff0c;从最早的"先跑个Demo看看效果"&#xff0c;到后来真正要上生产、要对接业务系统&#xff0c;中间踩的坑几乎都指向…

作者头像 李华
网站建设 2026/10/7 13:44:10

AI-native架构重建制造业ERP/MES系统:从排产到执行的完整实践

我在制造业信息化这条路上摸爬滚打了十多年&#xff0c;从早期单体 ERP 到后来微服务改造&#xff0c;再到这两年接触的各种"智能工厂"项目&#xff0c;见得太多了。今年年初我给自己定了一个有点疯狂的目标&#xff1a;用 AI-native 架构&#xff0c;从零重建一套面…

作者头像 李华
网站建设 2026/10/7 13:44:09

ponytail 插件怎么用:轻量级任务流与快捷动作效率方案实操指南

1. 从“ponytail”这个标题说起&#xff1a;它到底是什么第一次看到“ponytail”这个词&#xff0c;大多数人脑子里蹦出来的画面是扎在脑后的那束马尾辫。但在技术圈和效率工具圈里&#xff0c;ponytail 早就不是发型那么简单了。最近一段时间&#xff0c;ponytail skill、pony…

作者头像 李华
网站建设 2026/10/7 13:43:36

Agent驱动RAG知识库面试全链路:从文档切分到pgvector选型与Agent调度

1. 面试官问的不是术语&#xff0c;是你有没有真的跑过链路 DocResearch 这个项目名字听起来像个文档问答工具&#xff0c;但真正面过这类岗位的人都知道&#xff0c;面试官关心的从来不是你能不能背出 RAG 的全称&#xff0c;而是你能不能把一条完整的链路讲清楚&#xff1a;文…

作者头像 李华