ARTICLE DETAIL

资讯详情

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

医院预约挂号系统 JavaWeb 实战:从数据库设计到业务闭环全解析

医院预约挂号系统 JavaWeb 实战:从数据库设计到业务闭环全解析 简介基于JavaWeb开发的医院预约挂号管理系统源码包适合正在准备期末大作业、课程设计或毕业设计的计算机专业学生参考。项目包含完整的源码、前端页面与数据库脚本围绕预约挂号管理的核心流程组织可直接导入开发环境运行也便于对照学习相关开发技术。压缩包共343个文件总大小约8.94MB其中包含60个Java源文件、11个XML配置文件、11个CSS样式文件、9个JavaScript文件以及SQL脚本同时附带大量页面截图方便快速了解界面效果与功能模块。该设计已获老师指导并通过代码逻辑清晰属于纯手打的高分项目对初学者而言实用且容易上手可帮助理解JavaWeb项目的前后端交互与数据库设计思路。目前已有541人学习下载是一份可用于答辩演示和项目复现的高质量参考资料。1. 分数差距不来自功能数量医院预约挂号系统的业务闭环是什么选题一小时跑通两小时答辩三分钟——这是 javaweb 大作业最常见的节奏。医院预约挂号系统是这类课设里的常青树业务熟悉、模块边界清晰、数据库关系不复杂又能把增删改查、登录态、分页、权限这些考点全部覆盖。可同一个题目有人拿 60 分有人拿 95 分差距从来不在功能数量而在业务闭环患者能不能走完「选科室→选医生→看号源→提交预约→号源扣减→生成记录→查询/取消」的完整链路后台能不能对科室、医生、排班、订单做有效管理数据库看着像是一个能跑的真系统而不是拼凑的示例堆。这套技术方案核心由 JSP Servlet JDBC 连接池 MySQL 构成部署在 Tomcat 上特别适合正在做 javaweb 课程设计、毕业设计预演或者想把一个「完整 javaweb 项目案例 mysql 全流程」当作模板来改的同学。下面从数据库开始按我自己做这类系统的顺序把它拆开。2. 建好数据库预约系统才能立住表结构与初始化数据2.1 五张核心表把挂号拆成「科室—医生—排班—预约」四层很多大作业翻车的第一个原因是上来就建一张 appointment 表里面塞了科室名、医生名、患者名、时段、状态——所有信息平铺在一张表里。看起来简单但做预约业务时立刻卡住想查某个科室还有哪些医生、某位医生明天上午还剩几个号SQL 写得又长又绕改动字段还要连带改前端。正确的做法是拆成四层科室是医生归属医生是排班主体排班承载某一天上午/下午的号源预约订单则是患者与排班的一次绑定。我一般设计五张核心表department科室、doctor医生、patient患者、doctor_schedule排班、appointment预约订单。再加一张 admin 表做后台登录结构上就够了。排班表是整个系统的枢纽它同时关联医生、记录出诊日期、上下午号源总量和已约数量预约发生时只在排班表上做扣减而不是直接改医生表。这样的好处是一位医生一天可以有多条排班记录、上午下午号源互不干扰取消预约时也只要还原排班表上的计数。顺序上有讲究先建 department再建 doctor然后 patient、doctor_schedule、appointment。外键关系不复杂但删除顺序要反着来避免约束报错。2.2 建库脚本MySQL 8.0 一次跑通压缩包里所谓的「数据库」行业惯例就是一份 .sql 文件包含建库、建表、插入初始化数据三部分。下面这份脚本是我做类似管理系统时惯用的最小结构字符集统一 utf8mb4存储引擎 InnoDBMySQL 5.7 和 8.0 都能直接跑。-- init.sql CREATE DATABASE IF NOT EXISTS hospital_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hospital_db; CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, location VARCHAR(100) COMMENT 门诊位置, description VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT科室表; CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, name VARCHAR(30) NOT NULL, title VARCHAR(30) COMMENT 职称主任医师/副主任医师/主治医师, intro VARCHAR(255), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_doctor_dept FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINEInnoDB COMMENT医生表; CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5 摘要不存明文, real_name VARCHAR(30) NOT NULL, phone VARCHAR(20), id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT患者表; CREATE TABLE doctor_schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, period TINYINT NOT NULL COMMENT 1上午 2下午, am_quota INT DEFAULT 30 COMMENT 上午号源总量, pm_quota INT DEFAULT 20 COMMENT 下午号源总量, booked_am INT DEFAULT 0, booked_pm INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1放号 0停诊, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_date_period (doctor_id, work_date, period), CONSTRAINT fk_schedule_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(id) ) ENGINEInnoDB COMMENT医生排班表; CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, schedule_id INT NOT NULL, visit_date DATE NOT NULL, visit_period TINYINT NOT NULL COMMENT 1上午 2下午, status TINYINT DEFAULT 1 COMMENT 1已预约 2已取消 3已完成, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME, CONSTRAINT fk_appt_patient FOREIGN KEY (patient_id) REFERENCES patient(id), CONSTRAINT fk_appt_schedule FOREIGN KEY (schedule_id) REFERENCES doctor_schedule(id) ) ENGINEInnoDB COMMENT预约订单表;逻辑说明排班表用UNIQUE KEY uk_doctor_date_period保证同一位医生同一天同一时段只有一条记录倒逼前端在「新增排班」时先查重数据层面先拦住重复录入。period用 TINYINT 而不是字符串是因为前端的下拉框和后端的 Integer 判断都不需要做字符串匹配性能和省空间在课设里无所谓但编码习惯会加分。appointment表不直接冗余医生姓名、科室名通过schedule_id关联查询这符合第二范式也是答辩时老师最爱问的「为什么不在订单表里直接存医生名」的标准答案。参数说明am_quota30、pm_quota20是我按普通门诊半天 30-40 个号的经验给出的默认值做演示时不要固定死后面初始化数据里要给不同科室不同趟。status字段统一用 TINYINT 加注释的写法避免用char(1)因为 Java 端取值后跟Integer比较比跟字符串比较更自然。2.3 初始化数据的三个讲究密码脱敏、号源参数、演示日期初始化数据是让评审老师一眼觉得「这系统有人用过」的关键。我见到不少大作业数据库里就两条测试数据还都是 user1/user1排班空空的点进医生详情页直接白屏——这比功能缺一个还减分。初始化时要照顾三件事第一密码不要存明文。患者表里的 password 字段存 MD5 摘要md5(123456)即可前端登录时用DigestUtils.md5Hex生成摘要后再传 SQL避免数据库被打开时密码直接暴露。第二号源参数要有区分度。内科一个上午 30 号专家门诊一个上午 15 号这样前端页面渲染出来才有「号源紧张/充足」的对比演示时有话讲。第三演示日期必须用「相对当前日期」生成不能用固定日期。用CURDATE()和DATE_ADD生成未来三天的排班比写死2024-05-20强得多——否则评审老师在 2025 年打开系统看到的全是已过期停诊的排班第一印象直接崩。-- 初始化演示数据核心片段 INSERT INTO department (name, location, description) VALUES (内科, 门诊楼2层, 呼吸、消化系统常见病), (骨科, 门诊楼3层, 关节与脊柱损伤), (儿科, 门诊楼1层, 14岁以下儿童); INSERT INTO doctor (dept_id, name, title, intro) VALUES (1, 张医生, 主任医师, 擅长慢性呼吸系统疾病), (1, 李医生, 主治医师, 消化内科方向), (2, 王医生, 副主任医师, 关节置换与运动损伤), (3, 赵医生, 主治医师, 小儿呼吸道感染); INSERT INTO patient (username, password, real_name, phone, id_card) VALUES (demo, e10adc3949ba59abbe56e057f20f883e, 演示患者, 13800000000, 110101199001011234); -- 未来三天的排班每天上午放号专家号15个普通号30个 INSERT INTO doctor_schedule (doctor_id, work_date, period, am_quota, pm_quota) SELECT d.id, CURDATE() INTERVAL 1 DAY, 1, 15, 20 FROM doctor d WHERE d.name 张医生; INSERT INTO doctor_schedule (doctor_id, work_date, period, am_quota, pm_quota) SELECT d.id, CURDATE() INTERVAL 2 DAY, 1, 30, 25 FROM doctor d WHERE d.name 李医生;参数说明MD5 摘要e10adc3949ba59abbe56e057f20f883e对应明文123456是测试账号的标准写法。CURDATE() INTERVAL 1 DAY比用DATE_ADD(CURDATE(), INTERVAL 1 DAY)更简洁两者等价。插入排班时用SELECT d.id FROM doctor d WHERE d.name 张医生而不是硬编码doctor_id1是为了避免前面医生表的自增 ID 因删改而变化时初始化数据脱节。3. 从 dao 到 servlet把预约挂号主流程在 javaweb 里跑通3.1 包结构与依赖用 Maven 把骨架搭出来拿到一个 javaweb 项目先看它的包结构基本就能判断值不值 95 分。我推荐按com.xxx.hospital为根包下面分五层entity实体、dao数据访问、service业务逻辑、servlet控制器、filter过滤器再加一个util放连接池工具类。很多低分大作业的问题就是所有 Servlet 堆在默认包、SQL 写满 JSP连一个独立的 dao 层都没有这属于一打开就暴露的问题。IDE 的配置上我用 IDEA 举例新建项目时直接选 Jakarta EE / Web 骨架或者在普通 Maven 项目的pom.xml里加上war打包方式和javax.servlet-api依赖然后配置 Tomcat。这里的坑是 IDEA 里新建的「Web」项目默认不在src/main/java下建源码目录要和 Maven 的标准目录结构对齐否则后面 servlet 编译不到。dependencies !-- Servlet 与 JSP 依赖Tomcat 运行时可去掉 scopeprovided 的注释 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- 前端 JSTL 标签库 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies逻辑说明scopeprovided的意思是编译时需要、运行时由 Tomcat 提供如果不加打包时会把 servlet-api 打进去部署到 Tomcat 时可能和容器自带的类冲突典型表现是启动时NoSuchMethodError。这个坑我不止一次见人踩过。MySQL 驱动版本和数据库版本尽量大版本一致5.7 用 8.x 驱动没问题但反过来 8.0 数据库配 5.1 驱动会报Public Key Retrieval is not allowed。如果你拿到的是一个不打 Maven 的普通 Web 项目常见做法是把 mysql-connector、druid、jstl 的 jar 直接拷到WEB-INF/lib目录下。IDEA 里注意右键 jar 选Add as Library否则编译通过但运行时报ClassNotFoundException。这个区别我放在后面避坑章节细说。3.2 Druid 连接池参数db.properties 里的关键配置传统 javaweb 课设最爱用 JDBC 直连每次请求都DriverManager.getConnection功能没问题但答辩时老师问一句「多个用户同时预约会不会把数据库连接打满」就直接卡壳。用连接池是性价比极高的加分项Druid 在国产项目中普及率最高配置也直观。# db.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 # 连接池参数 pool.initialSize5 pool.maxActive30 pool.minIdle5 pool.maxWait60000 pool.validationQuerySELECT 1 pool.testWhileIdletrue pool.timeBetweenEvictionRunsMillis60000参数说明serverTimezoneAsia/Shanghai解决 MySQL 8.0 下驱动默认使用 UTC 导致时间差 8 小时的问题不配的话CURDATE()查出来的日期和本地日期对不上演示时你会发现「今天」的排班查不出来。allowPublicKeyRetrievaltrue是 MySQL 8.0 默认 caching_sha2_password 认证插件的常见报错解法开发环境直接开。initialSize5表示启动时预创建 5 个物理连接maxActive30是上限maxWait60000是拿连接的最大等待毫秒数超过就抛超时异常。testWhileIdletrue让连接池在空闲时用validationQuerySELECT 1探测连接是否有效避免数据库重启后 Tomcat 还拿着死连接——这个参数在答辩演示当天特别有用因为评委往往要求现场重启 MySQL。对应的工具类用一个静态块初始化 DruidDataSource然后通过DruidDataSourceFactory.createDataSource(props)生成数据源最后dataSource.getConnection()获取连接。这个类写成单例所有 dao 都从它拿 Connection避免每个 dao 都 new 一个数据源。3.3 核心方法号源扣减的事务实现预约挂号最有技术含量的一步是「扣减号源」和「生成订单」必须作为一个事务原子提交。低分写法是先SELECT booked_am FROM doctor_schedule WHERE id?拿到余量后在 Java 里判断大于 0 就UPDATE加 1再INSERT预约记录。这在大作业演示时通常没问题但两个浏览器同时提交时两个请求都可能读到同样的余量导致超卖。老师要的是线程安全的意识最简单的解法是让数据库自己保证原子性。// AppointmentService.book 核心方法 public boolean book(int patientId, int scheduleId, Date visitDate, int period) throws SQLException { Connection conn null; PreparedStatement updateStmt null; PreparedStatement insertStmt null; try { conn DruidUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步条件更新排班号源返回值影响行数天然防超卖 String updateSql UPDATE doctor_schedule SET (period 1 ? booked_am booked_am 1 : booked_pm booked_pm 1 ) WHERE id ? AND status 1 AND (period 1 ? booked_am am_quota : booked_pm pm_quota); updateStmt conn.prepareStatement(updateSql); updateStmt.setInt(1, scheduleId); int rows updateStmt.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 号源已满或停诊 } // 第二步插入预约订单 String insertSql INSERT INTO appointment (patient_id, schedule_id, visit_date, visit_period, status) VALUES (?, ?, ?, ?, 1); insertStmt conn.prepareStatement(insertSql); insertStmt.setInt(1, patientId); insertStmt.setInt(2, scheduleId); insertStmt.setDate(3, new java.sql.Date(visitDate.getTime())); insertStmt.setInt(4, period); insertStmt.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (insertStmt ! null) insertStmt.close(); if (updateStmt ! null) updateStmt.close(); if (conn ! null) conn.close(); // 归还到连接池不是物理关闭 } }逻辑说明关键在UPDATE ... WHERE id? AND booked_am am_quota这条 SQL 把「检查余量」和「扣减名额」合并成一个原子操作。数据库的行锁保证同一时刻只有一个请求能成功更新这一行executeUpdate返回 0 就说明余量不足或排班停诊直接回滚。业务上不需要先SELECT再判断既少一次查询又堵住了并发超卖。conn.setAutoCommit(false)开启事务两个语句要么都成功要么都回滚不会出现「订单建了号没扣」或者「号扣了订单没生成」的中间状态。这里有个参数容易踩visitDate从 Servlet 接收时是String不要直接用request.getParameter(visitDate)拼进 SQLMySQL 默认DATETIME和字符串比较虽然能用但遇到2025/03/20这种格式就废了。Servlet 端用SimpleDateFormat(yyyy-MM-dd)解析成java.util.Date再转java.sql.Date。如果前端传的是yyyy-MM-dd也可以用java.sql.Date.valueOf(字符串)一步到位。finally里的conn.close()在连接池场景下是归还连接而不是真关闭这是 Druid 代理对象的行为不用特殊处理。一个常见的误用是有人手动在finally里conn.commit()如果前面已经回滚这里会抛Cannot commit when autoCommit is enabled之类的问题记住commit/rollback只出现在 try 和 catch 里。4. 前端页面与权限控制让系统看起来值 95 分的四个模块4.1 患者端的预约动线列表页→详情页→提交表单患者端的交互动线决定演示是否顺畅。页面不需要多炫但以下几点必须有登录/注册页、科室列表页、医生列表页、排班号源展示页、预约提交页、我的预约列表页。几个页面之间靠 URL 参数串联deptList.jsp → doctorList.jsp?deptId1 → scheduleList.jsp?doctorId1 → book.jsp?scheduleId5。JSP 页面里建议用 EL 和 JSTL 输出数据少写% %scriptlet。下面给出排班展示页取数据的 Servlet 片段和 JSP 核心表格这是整个预约流程的入口。// ScheduleServlet 的 doGet返回某位医生的未来排班 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Integer doctorId Integer.valueOf(req.getParameter(doctorId)); ListDoctorSchedule list scheduleDao.findAvailableByDoctor(doctorId); req.setAttribute(scheduleList, list); req.getRequestDispatcher(/WEB-INF/jsp/scheduleList.jsp).forward(req, resp); }!-- scheduleList.jsp 核心片段 -- % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 cellpadding8 cellspacing0 tr th出诊日期/th th时段/th th剩余号源/th th操作/th /tr c:forEach items${scheduleList} vars tr td${s.workDate}/td td${s.period 1 ? 上午 : 下午}/td td c:if test${s.period 1} ${s.amQuota - s.bookedAm} / ${s.amQuota} /c:if c:if test${s.period 2} ${s.pmQuota - s.bookedPm} / ${s.pmQuota} /c:if /td td a hrefbook?scheduleId${s.id}预约/a /td /tr /c:forEach /table逻辑说明Servlet 只负责把数据塞进request然后forward到 JSPJSP 用 JSTL 的c:forEach循环渲染。这里要注意/WEB-INF/jsp/目录的用法——JSP 放在 WEB-INF 下用户无法通过 URL 直接访问.jsp文件强制走 Servlet避免地址栏输入scheduleList.jsp绕过登录。这是流程上很重要的一环。${s.period 1 ? 上午 : 下午}是 EL 三目运算符比在 Java 里转换后放 request 更省事。剩余号源展示的是可预约数/总量评审老师看到「余 3 / 15」比看到「15」直观得多这也是细节加分项。book这个 Servlet 接收scheduleId参数后先查排班确定还有号源再 forward 到预约确认页页面显示医生名、科室、日期、时段让患者二次确认后提交。我在预约确认页上会加一行「提交后不可重复预约同医生同日期同时段」的提示同时在后端也做校验防止前端绕过页面直接请求接口重复下单。4.2 医生端与管理后台另一个视角的增删改查很多大作业只做了患者端后台管理全靠手改数据库——这离 95 分有明显距离。管理后台至少要覆盖科室管理增删改查、医生管理按科室筛选、新增、编辑、删除、排班管理为医生添加未来三天的排班、停诊操作、预约管理查看所有订单列表、按状态筛选。这四组功能本质都是标准增删改查但组合在一起就构成了系统「管理端」的完整拼图也能自然覆盖 javaweb 必考的数据库增删改查考点。排班管理里一个实用的代码片段是批量生成排班。很多时候讲师会问「你怎么保证有号可约」答案就是管理员提前录排班。// ScheduleAdminServlet 新增排班的批量处理 String[] workDates req.getParameterValues(workDate); String doctorId req.getParameter(doctorId); String amQuota req.getParameter(amQuota); String pmQuota req.getParameter(pmQuota); String[] periods req.getParameterValues(period); // 按「医生日期时段」查重后批量插入 boolean allInserted true; for (String dateStr : workDates) { if (scheduleDao.exists(doctorId, dateStr, period)) { allInserted false; // 跳过重复排班 continue; } scheduleDao.insert(doctorId, dateStr, period, amQuota, pmQuota); }逻辑说明getParameterValues是处理多选框的标准方式前端用一个「可多选日期」的复选框列表让管理员勾选未来一周的工作日后端批量插入。exists查询利用排班表唯一键提前拦截避免数据库抛DuplicateEntry异常然后整个页面报错——这比直接插入然后 catch 异常的方法要优雅而且演示时不会出现半截红字报错页。管理员删除医生时要先确认该医生是否有未完成的排班和预约我通常在删除前查 count大于 0 就提示「请先处理该医生的排班与预约」防止外键约束直接 500。管理端页面不需要单独写一套样式复用患者端的 CSS 即可。关键是导航清晰左侧菜单或者顶部 tab 分为「科室管理 / 医生管理 / 排班管理 / 预约管理」每个页面都有新增按钮和列表表格操作后跳回列表并带上?msgsuccess参数页面顶部显示一条绿色提示。这套交互做下来评委的印象就是「完整」。4.3 登录态与权限过滤器Session 的写法javaweb 的登录态控制行业标准做法是 Session Filter。登录成功时把用户对象放进session.setAttribute(patient, patient)之后每个需要登录的页面都经过一个过滤器检查 Session 是否存在不存在就重定向到登录页。这里有个常见误区只在 JSP 里判断 Session 不够因为用户可以直接请求 Servlet 接口跳过 JSP 页面。// AuthFilter.java public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; Object user request.getSession().getAttribute(patient); Object admin request.getSession().getAttribute(admin); // 静态资源和登录相关接口放行 String uri request.getRequestURI(); if (uri.contains(login) || uri.contains(register) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(request, response); return; } if (user null admin null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }!-- web.xml 关键映射 -- filter filter-nameAuthFilter/filter-name filter-classcom.demo.hospital.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/*/url-pattern /filter-mapping逻辑说明/*拦所有请求但放过 login、register 和静态资源。管理端和患者端共用一套 Session 的话上面代码里user null admin null的逻辑就是「只要登录了任意角色都放行」但后台管理页面要在 Servlet 里再判断admin是否为空防止患者直接访问adminServlet。两个过滤器分开管理会更干净做成PatientAuthFilter和AdminAuthFilter分别映射/patient/*和/admin/*是一种更细的做法大作业里用一对多过滤器更有解释空间。Session 超时时间在web.xml里配置session-configsession-timeout30/session-timeout/session-config表示 30 分钟。容易忽略的一点是req.getContextPath()它返回应用上下文路径比如/hospital用response.sendRedirect(req.getContextPath() /login.jsp)拼出来的地址才不会被部署路径差异影响——直接从 IDEA 部署到独立 Tomcat上下文路径可能变拼死了/hospital/login.jsp就得改代码。这个细节是「idea 运行 javaweb 项目配置」时很容易被绊住的地方后面避坑章节会再提。5. 踩坑清单IDEA 运行 javaweb 项目常见的 6 个翻车现场场景一启动 Tomcat 报 ClassNotFoundException: com.mysql.cj.jdbc.Driver现象Tomcat 启动时或第一次点预约时页面直接 500控制台一堆 ClassNotFoundException指向 MySQL 驱动类。原因两个典型来源。一是 Maven 工程里没在pom.xml引入 mysql-connector-java二是普通 Web 工程的 jar 包放在硬盘某个角落但没复制到WEB-INF/libIDEA 编译时靠手动 Add as Library 能找到但 Tomcat 运行时不会去读项目外的 jar。解决Maven 项目先确认dependency里驱动版本和数据库版本同系列普通项目把 mysql-connector-java-8.0.33.jar 和 druid-1.2.20.jar 直接放进web/WEB-INF/lib目录并在 Project Structure 里确认这两个 jar 被标记为 Library。改完最好 Rebuild Project 再启动。清单顺序是lib 目录 → Project Structure → Artifacts 里看输出是否包含 jar。场景二IDEA 部署后页面访问 404但文件明明在现象Tomcat 能启动但浏览器访问localhost:8080/项目名/xxx.jsp报 404IDEA 控制台也没有明显报错。原因最常见的是在 IDEA 里创建了普通 Java 项目而不是 Web 项目没有自动生成 Web Artifact或者 Artifact 的 output 目录没有把web目录带进去。另一类情况是访问http://localhost:8080/WEB-INF/jsp/scheduleList.jsp——JSP 放在 WEB-INF 下就会 404这是正常的保护行为不是 bug。解决打开 Project Structure → Artifacts确认有且仅有一个 Web Application Exploded/War Exploded 类型的 Artifact其 Structure 里Web Resource Directory指向web或src/main/webapp。Run 配置里 Deployment 选这个 ArtifactApplication context 填/hospital或留根路径/之后访问路径必须和这个 context 对应起来。场景三连接到数据库成功但中文全部乱码或时间差 8 小时现象页面表格里医生姓名显示å¼ ä¸或者排班日期显示昨天而系统日期是今天。原因数据库连接 URL 没有携带编码和时区参数。项目本身全链路都是 utf8mb4但 JDBC 驱动默认使用平台编码传输MySQL 8.0 驱动默认时区是 UTC两者叠加要么乱码要么日期错位。解决连接串统一改成jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。看过不少项目只在web.xml或 JSP 页面里加pageEncoding那只解决了 HTTP 层的编码JDBC 层没设置照样白搭。两个地方同时设才对另外 JSP 顶部% page pageEncodingUTF-8 %和contentTypetext/html; charsetUTF-8也要都写上缺一个页面标题就可能出乱码。场景四预约时偶尔会「约了号但订单没生成」或「订单在但号源没扣」现象先在另一个窗口手动取消一个订单然后在原窗口再次提交同一个排班发现页面提示成功但数据库里 appointment 新增了记录而 doctor_schedule 的 booked 字段没加 1。更玄学的是这个问题不是必现的只有两个请求几乎同时提交时才出现。原因代码里先SELECT余量再UPDATE扣减两步操作之间存在时间窗口没有用事务包住或者第二步的UPDATE失败了但前一步的INSERT已经提交。本质上就是「读判断 写操作」这两步没被拼成一个原子动作。解决按本文第 3.3 节的做法把检查放入UPDATE的WHERE条件里用一条条件UPDATE完成扣减并且在同一事务内插入订单。这个改动撬动的面不大但属于答辩时最容易被追问的技术点值得提前改好。另一个隐藏点conn.setAutoCommit(false)之后如果UPDATE影响行数为 0一定要手动rollback()不然这条连接回池子后自动提交状态可能不正确。场景五Tomcat 端口被占用启动直接失败现象IDEA 里启动 Tomcat 时日志报Port 8080 was already in use或者提示 Address already in use: JVM_Bind。原因上一个 Tomcat 实例没完全关闭或者本机其他程序占用了 8080。IDEA 里关掉 Run 窗口的停止按钮有时只是断开了调试连接实际 Java 进程还活着。解决最稳妥的是在 IDEA 的事件日志里找到线程 id或在命令行输入netstat -ano | findstr 8080查看占用进程 PID然后taskkill /PID pid /FWindowsmacOS/Linux 用lsof -i:8080查占用后kill -9。或者直接把 Tomcat 端口改成 8081改的是conf/server.xml里Connector port8080 ...。但注意改端口后所有前端跳转如果是拼接http://localhost:8080的硬编码地址也得同步改。我建议代码里所有跳转都用相对路径加ctxPath避免这个坑。场景六MySQL 8.0 连接报 Public Key Retrieval is not allowed现象系统之前跑得好好的换了一台电脑或重装 MySQL 后连接池初始化就抛异常提示 Public Key Retrieval is not allowed页面直接 500。原因MySQL 8.0 默认认证插件是 caching_sha2_password在 SSL 未启用的情况下客户端需要向服务器请求 RSA 公钥来做密码加密传输。JDBC 驱动为了安全默认不自动请求公钥需要显式打开。解决开发环境在 JDBC URL 上追加allowPublicKeyRetrievaltrue即可。这不是漏洞开关只是允许驱动获取公钥内网课设场景完全可接受。如果不想加这个参数可以把 MySQL 用户默认认证插件改回 mysql_native_password但这条链路要改用户并重启 MySQL反而更麻烦我一般直接改 URL 参数。6. 答辩现场实测一条预约数据从提交到落库的验收路径演示最有说服力的方式不是机械地把页面走一遍而是「制造异常再修复」。我每次交付前会跑一遍验证 SQL确认业务闭环没有断点。第一步重置演示环境把排班数据拨回到有余量状态UPDATE doctor_schedule SET booked_am 0, booked_pm 0, status 1 WHERE work_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY);第二步在页面上完成一次预约用demo / 123456登录选内科 → 张医生 → 明天上午 → 提交预约。第三步回数据库验证订单已生成、号源已扣减这两条 SQL 在答辩现场直接抛给评委看SELECT a.id, p.real_name, d.name AS doctor_name, s.work_date, a.visit_period, a.status FROM appointment a JOIN patient p ON a.patient_id p.id JOIN doctor_schedule s ON a.schedule_id s.id JOIN doctor d ON s.doctor_id d.id ORDER BY a.id DESC LIMIT 3; SELECT doctor_id, work_date, period, booked_am, am_quota, CONCAT(booked_am, /, am_quota) AS am_used FROM doctor_schedule WHERE doctor_id 1 AND work_date CURDATE() INTERVAL 1 DAY;这两条 SQL 一跑患者、排班、医生、订单四张表全部关联上业务闭环当场完整呈现。第四步再去「我的预约」页面取消这条订单回到数据库看appointment.status是否变成 2同时doctor_schedule.booked_am是否减一回落。取消预约同样要走事务先改订单状态再条件减号源顺序不能反。我自己的习惯是每次改完代码、交付前都重放一遍这条验收路径并且把演示用的排班日期做成「今天到未来三天」的动态生成。曾经有一次因为初始化数据写死了日期现场打开系统发现所有排班都在上周页面空白评委直摇头。后来所有排班相关的初始化统一改成基于CURDATE()的相对日期就再没出过这类低级事故。这个工程习惯配上事务、连接池、过滤器三位一体的实现课程设计想低于 90 分都难。希望帮到你。本文还有配套的精品资源点击获取
返回列表