ARTICLE DETAIL

资讯详情

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

JavaWeb医院预约挂号系统:从数据库设计到事务与Session管理

JavaWeb医院预约挂号系统:从数据库设计到事务与Session管理 简介基于JavaWeb的医院预约挂号管理系统源码与数据库一同打包为一套可导入运行的高分课程设计项目主要面向Java期末大作业、毕业设计以及需要快速理解Web后台开发流程的初学者。系统覆盖科室管理、医生排班、在线预约、用户登录等典型业务模块采用JSP/Servlet为主体的JavaWeb技术栈搭配MySQL数据库前端引入layui等UI框架整体目录划分清楚便于定位代码与修改参数。压缩包共有343个文件其中包含60个Java源文件、11个XML配置、SQL建库脚本以及大量PNG、JPG图片样式和CSS、JS前端资源项目加数据库仅8.94MB本地部署轻量便捷。目前已有542人学习下载该项目经老师指导并获高分评价纯手打实现、小白实战无难度对需要冲刺高分的同学来说既能直接拿来演示验收也能从数据库设计到页面逻辑中梳理完整预约挂号的实现思路是一份完整性很高的参考素材。1. 基于 JavaWeb 的医院预约挂号系统这题到底考什么又到了交大作业的季节每年这时候都有一批人被「医院预约挂号系统」这类题目卡住。表面看它是个 CRUD 项目实际上它把 JavaWeb 课程里几乎所有能考的点全塞进去了Servlet 生命周期、JSP 页面渲染、JDBC 连接池、MVC 分层、Session 登录态管理再加上一张稍微像样点的数据库表设计。很多同学拿到题目先写代码写到用户表和外键关联时就乱了最后交上去的是一坨能跑但经不起问的东西。这个系统本质上是「挂号资源 用户状态」的管理系统核心难点不在增删改查而在三件事医生排班和号源余量的联动扣减、用户登录态与挂号记录的绑定、以及管理员对基础数据的维护。把这三条线理顺哪怕界面丑一点答辩时老师问一句你能答上来分数就不会低。这篇笔记按我当年做这类课设的完整路径拆开讲从表结构到部署排错照着走能少熬两个通宵。2. 表结构设计先画 ER 图再写代码别急着建表2.1 六张核心表从用户到挂号记录的关系梳理医院挂号系统的数据库至少要有用户表、医生表、科室表、排班表、挂号记录表和管理员表。我见过有人把医生和科室塞进同一张表省事是省事但院长想看「内科有几个医生」这类统计时就得写一串 substring 函数属于给自己埋雷。先看用户表的典型设计CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5后密码, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号挂号时用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user 是 MySQL 的保留字所以建表时我用反引号包住。密码字段存 MD5 摘要而不是明文这是答辩时老师常问的一个安全点哪怕你只是简单调用MD5Util.md5(password)也说明你考虑过这个问题。然后看科室和医生表这两张通过 dept_id 关联是一对多关系CREATE TABLE dept ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, description VARCHAR(200) ); CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(30) COMMENT 职称主任医师/副主任医师等, intro VARCHAR(500) COMMENT 个人简介, available TINYINT DEFAULT 1 COMMENT 是否出诊0停诊, FOREIGN KEY (dept_id) REFERENCES dept(id) );排班表是整个系统里最容易翻车的表。排班记录医生在某天某个时段出诊并且要维护号源余量。我建议把「总号源数」和「剩余号源数」拆成两个字段而不是只存一个余量这样就能说清楚「放出去 30 个号被挂了 12 个」这个事实CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, work_time VARCHAR(20) NOT NULL COMMENT 上午/下午时间区间, total_count INT DEFAULT 30 COMMENT 总号源, remain_count INT DEFAULT 30 COMMENT 剩余号源, register_fee DECIMAL(8,2) DEFAULT 10.00 COMMENT 挂号费, UNIQUE KEY uk_doctor_date_time (doctor_id, work_date, work_time) );最后是挂号记录表它同时关联 user 和 scheduleCREATE TABLE register_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, schedule_id INT NOT NULL, doctor_name VARCHAR(50) COMMENT 冗余医生名方便列表展示, dept_name VARCHAR(50) COMMENT 冗余科室名, visit_date DATE, status TINYINT DEFAULT 0 COMMENT 0已预约1已就诊2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id) );有些同学会问医生名和科室名为什么不直接关联查询非要冗余存一份答案是列表页和我的挂号记录页需要频繁展示这两个字段每次 join 两张表性能不差但 JSP 页面写起来多一层嵌套取值冗余字段能让页面代码短一大截。课设阶段用空间换代码清晰度是划算的。2.2 数据库连接池与 db.properties 配置JavaWeb 课设里最常见的数据库连接写法是DriverManager.getConnection每次请求都新建连接。这在并发测试下会直接暴露问题我见过有人用 JMeter 压了 50 个线程就把数据库打到 too many connections。正确做法是用连接池Tomcat 自带的 DBCP 或者阿里巴巴的 Druid 都行课设我更推荐 Druid因为它的监控页面能当答辩演示素材。在项目的 src 目录下建 db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxActive20 jdbc.maxWait60000写一个工具类读取配置并维护连接public class DBUtil { private static DruidDataSource dataSource; static { Properties props new Properties(); try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }注意连接池配置文件里serverTimezoneAsia/Shanghai这一项MySQL 8.x 驱动如果不加这个参数连接时报时区错误是必现的。另外连接池参数里maxActive20是上限你本地跑课设 5 个就够写 20 是为了应付老师现场用多台机器同时访问的演示场景。2.3 MySQL 8 驱动的坑com.mysql.jdbc.Driver 还是 com.mysql.cj.jdbc.Driver很多同学用的是学校机房装的 MySQL 5.7在自己电脑装了个 MySQL 8.x然后把课设代码原封不动拿过来跑结果启动就报 ClassNotFoundException。这是因为 MySQL 8 把驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver并且驱动 JAR 也要换成 mysql-connector-java 8.x 版本。常见做法是直接在 pom.xml 里换成 8.0.33dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency换完驱动后db.properties 里的 driver 和 url 也要同步改。我见过有人只换了 JAR 没改 driver 类名报错还是 ClassNotFound以为 JAR 没生效折腾了半天才发现是配置没同步。如果用的是旧版 5.1 驱动连 MySQL 8还会报 SSL 连接错误所以 url 里的useSSLfalse也不能省。3. 登录注册与 Session 管理把用户状态管住3.1 注册接口的完整流程表单校验到密码加密注册功能看起来简单但要做好校验逻辑。前端用 JavaScript 做非空校验后端 Servlet 里还必须再校验一次因为前端验证可以被绕过。我一般会在后端加一个统一校验方法WebServlet(/register) public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); String confirmPwd req.getParameter(confirmPassword); String realName req.getParameter(realName); if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { req.setAttribute(errorMsg, 用户名和密码不能为空); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } if (!password.equals(confirmPwd)) { req.setAttribute(errorMsg, 两次输入的密码不一致); req.getRequestDispatcher(/register.jsp).forward(req, resp); return; } UserService userService new UserService(); boolean success userService.register(username, password, realName); if (success) { resp.sendRedirect(req.getContextPath() /login.jsp); } else { req.setAttribute(errorMsg, 用户名已存在); req.getRequestDispatcher(/register.jsp).forward(req, resp); } } }这段代码的思路是先做非空和一致性校验不通过就往 request 里塞错误信息并 forward 回注册页通过后交给 UserService 处理业务成功就重定向到登录页。注意 forward 和 sendRedirect 的区别——forward 是服务端转发地址栏不变适合把错误信息带回表单页sendRedirect 是客户端重定向适合成功后跳转避免用户刷新页面时重复提交表单。UserService 里的 register 方法负责调用 DAOpublic boolean register(String username, String password, String realName) { UserDao dao new UserDao(); if (dao.findByUsername(username) ! null) { return false; } String md5Pwd MD5Util.md5(password); User user new User(); user.setUsername(username); user.setPassword(md5Pwd); user.setRealName(realName); return dao.insert(user) 0; }先查后插这个顺序在单线程下没问题但严格讲并发注册时会重复课设答辩一般是不会问这么深的。如果你想把正确性拉满可以把 username 字段的唯一索引作为兜底插入时捕获 DuplicateKeyException这也是生产环境常见的做法。3.2 Session 失效与注销上线前必测的两个场景登录成功后的标准动作是把用户对象塞进 Session再重定向到首页HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /index.jsp);setMaxInactiveInterval设置的是 Session 空闲超时时间单位是秒。30 分钟是网上常见的教学值但医院预约挂号这种系统用户可能一边挂号一边查资料30 分钟有点短建议改成 60 分钟。这个参数在 web.xml 里也能配session-config session-timeout60/session-timeout /session-config测试 Session 时最容易踩的坑是用户登录后在不关闭浏览器的情况下Session 过期了用户点击「我的挂号」时系统提示 500。原因很简单JSP 页面里直接用了${sessionScope.loginUser.realName}取值loginUser 为 null 时 EL 表达式还能静默处理但如果你在 Servlet 里User user (User) session.getAttribute(loginUser)后直接用 user.getId()就会抛 NullPointerException。解决方案是在 Servlet 入口统一判断HttpSession session req.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp?timeout1); return; }注销功能同理调用session.invalidate()让 Session 彻底失效再重定向到登录页。注意 invalidate 之后不能再往 session 里写属性否则报 IllegalStateException。4. 核心挂号流程号源扣减与事务控制4.1 挂号接口的设计先查状态再扣余量挂号是整个系统里唯一涉及「数据一致性」的操作。用户选定一个排班点击挂号服务器要做三件事检查用户是否已登录、检查该排班余量是否大于 0、把余量减 1 并插入挂号记录。这三件事必须在一个事务里完成否则就会超挂。我建议把事务边界放在 Service 层用最原始的conn.setAutoCommit(false)来管public boolean registerAppointment(int userId, int scheduleId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); ScheduleDao scheduleDao new ScheduleDao(); Schedule schedule scheduleDao.findById(scheduleId, conn); if (schedule null || schedule.getRemainCount() 0) { conn.rollback(); return false; } RegisterRecordDao recordDao new RegisterRecordDao(); boolean exists recordDao.checkExists(userId, scheduleId, conn); if (exists) { conn.rollback(); return false; } int updated scheduleDao.decreaseRemain(scheduleId, conn); if (updated ! 1) { conn.rollback(); return false; } recordDao.insert(userId, scheduleId, conn); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里有个细节值得注意编写 JDBC 事务时我把 DAO 方法都加了一个 Connection 参数所有步骤共用同一个连接。如果每个 DAO 自己再去连接池拿连接事务就会失效因为 MySQL 默认每条连接上执行的事务是独立的。decreaseRemain 的 SQL 建议写成原子操作UPDATE schedule SET remain_count remain_count - 1 WHERE id ? AND remain_count 0这样即使多个请求同时进来数据库行锁也会保证只有一个请求能成功更新。返回影响行数为 0 就说明余量已经没了这比先 select 再 update 更稳妥。4.2 重复挂号的检查同一患者同一排班只能挂一次排班表的联合唯一键uk_doctor_date_time能保证一个医生一个时间段只有一条排班记录但用户重复挂号需要另外检查。我在挂号事务里通过checkExists方法查挂号记录表SELECT COUNT(*) FROM register_record WHERE user_id ? AND schedule_id ? AND status ! 2这里用status ! 2把已取消的记录排除掉意思是用户取消过一次之后还能重新挂同一排班。如果去掉这个条件用户手滑取消后再想挂号就会被「已经挂过」拦截体验很差。其实还有一种更保险的做法就是给 register_record 表也加一个联合唯一索引把 user_id、schedule_id、status 三个字段绑在一起。但 status 是变化的加了索引后取消和重挂会撞索引不如靠代码逻辑控制。课设阶段用代码查一遍足够。4.3 取消挂号余量回补的时机问题取消挂号是挂号的反向操作需要把余量加回来同时把状态置为 2。有些同学只改了状态余量没加回去导致原本 30 个号变成只剩 29 个越挂越少最后系统里一堆号源凭空消失。标准的事务逻辑是public boolean cancelAppointment(int recordId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); RegisterRecordDao recordDao new RegisterRecordDao(); RegisterRecord record recordDao.findById(recordId, conn); if (record null || record.getStatus() 2) { conn.rollback(); return false; } ScheduleDao scheduleDao new ScheduleDao(); scheduleDao.increaseRemain(record.getScheduleId(), conn); recordDao.updateStatus(recordId, 2, conn); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }取消这个操作有个边界条件要想清楚用户已经「就诊完成」status1的记录还能不能取消按照医院业务就诊完成就不能取消了所以代码里只检查 status ! 2 是不够的应该改为 status 0 才允许取消。我在上面代码里写的是if (record null || record.getStatus() 2)这是阉割版实际你们写的时候把条件改成record.getStatus() ! 0会更符合真实业务。5. 前端页面与数据渲染JSP EL JSTL 的三件套用法5.1 首页科室列表与医生排班的 JSP 渲染医院预约挂号系统的首页一般分两栏左侧是科室列表右侧是选中科室后的医生列表。用 JSTL 的 forEach 循环渲染能省掉大量 Java 代码。科室列表的 JSP 片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classdept-list c:forEach items${deptList} vardept a href${pageContext.request.contextPath}/doctor/list?deptId${dept.id} classdept-item ${dept.deptName} span classdept-desc${dept.description}/span /a /c:forEach /div${pageContext.request.contextPath}是拿到项目上下文路径的标准写法比如项目部署名是hospital它返回/hospital。这样页面里的链接不会因为项目部署名变化而失效是课设里必须有的习惯。医生排班列表页渲染的关键是嵌套循环先遍历医生再遍历每个医生的排班c:forEach items${doctorList} vardoctor div classdoctor-card h3${doctor.name} small${doctor.title}/small/h3 p${doctor.intro}/p table tr th日期/th th时段/th th剩余号源/th th操作/th /tr c:forEach items${doctor.scheduleList} varsch tr td${sch.workDate}/td td${sch.workTime}/td td${sch.remainCount}/${sch.totalCount}/td td c:choose c:when test${sch.remainCount 0} a hrefjavascript:void(0) onclickdoRegister(${sch.id},${doctor.name})挂号/a /c:when c:otherwise span classfull已满/span /c:otherwise /c:choose /td /tr /c:forEach /table /div /c:forEach这个页面的核心逻辑在 c:choose 分支判断余量大于 0 才显示挂号按钮否则显示已满。你可能会问为什么余量检查要在页面做一遍Servlet 里做一遍数据库做第三遍因为页面判断是为用户体验、Servlet 判断是为业务校验、数据库 SQL 条件是最后的兜底防线三层缺一不可。5.2 Ajax 提交挂号请求局部刷新页面传统 JSP 表单提交会刷新整个页面用户挂完号回到列表需要重新滚动体验很差。我建议用 jQuery 的$.getJSON或原生fetch做局部请求function doRegister(scheduleId, doctorName) { if (!confirm(确认挂号 doctorName 的门诊号吗)) { return; } $.getJSON(ctx /appointment/register, { scheduleId: scheduleId }, function (data) { if (data.success) { alert(挂号成功); window.location.reload(); } else { alert(data.message); } }); }这里有个容易被忽略的点ctx 变量要在页面里提前定义否则${pageContext.request.contextPath}的值传不过来到 JS 里。在 JSP 页面的 head 区域加一行script var ctx ${pageContext.request.contextPath}; /scriptServlet 端返回 JSON 时用 Gson 或 Fastjson 序列化一个 Map 对象protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(application/json;charsetUTF-8); int scheduleId Integer.parseInt(req.getParameter(scheduleId)); HttpSession session req.getSession(); User user (User) session.getAttribute(loginUser); MapString, Object result new HashMap(); if (user null) { result.put(success, false); result.put(message, 未登录或会话过期); } else { boolean success appointmentService.registerAppointment(user.getId(), scheduleId); result.put(success, success); result.put(message, success ? 挂号成功 : 挂号失败号源已满或已挂过); } resp.getWriter().write(new Gson().toJson(result)); }注意resp.setContentType(application/json;charsetUTF-8)必须放在获取 writer 之前并且 charsetUTF-8 不能省否则前端收到的中文会乱码成问号。这是页面显示乱码最常见的元凶比 JSP 页面编码问题还要隐蔽。6. 避坑日记从控制台报错到页面白屏的 5 个典型问题6.1 现象Tomcat 启动正常但访问页面报 404原因项目没有部署成功或者访问路径里的上下文名不对。最常见的是把项目打成 WAR 包丢进 webapps 时Tomcat 解压后的目录名与你访问路径不一致或者用 IDEA 的 Artifacts 配置时Application context 设成了/hospital_war_exploded访问路径变成http://localhost:8080/hospital_war_exploded/跟预期完全对不上。解决在 IDEA 中打开 Run/Debug Configurations找到 Deployment 标签页把 Application context 改为/hospital然后重新运行。如果不走 IDEA 部署直接把整个项目文件夹放到webapps/ROOT下替代原 ROOT 目录访问路径就是http://localhost:8080/。6.2 现象JSP 页面显示源码而不是渲染后的页面原因项目配置里把 JSP 文件放在了 webapp 目录之外或者 web.xml 里没有配置 JSP Servlet 映射。另一个常见情况是浏览器直接访问了.jsp源文件路径比如从 IDEA 里点浏览器图标打开的地址带了本地磁盘路径导致 Tomcat 没经过 JSP 编译器。解决确认 JSP 文件放在src/main/webapp目录下且访问地址是以http://localhost:8080/hospital/开头的 URL。如果浏览器地址栏是file:///...或http://localhost:63342/...这是 IDEA 内置服务器就不是 Tomcat 在服务页面源码展示是正常的。6.3 现象连接数据库报 Access denied for user rootlocalhost原因MySQL 8 默认的 root 账号密码认证方式改成了 caching_sha2_password而项目里用的连接驱动是 5.x 版本不支持这种认证。或者密码确实写错了db.properties 里的密码与 MySQL 实际密码不一致。解决优先检查 db.properties 密码如果密码没问题考虑换用 8.x 版本的 mysql-connector-java 驱动。另一个方法是执行 SQL 把认证插件改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;6.4 现象挂号成功后刷新页面显示重复挂号成功原因前端用$.getJSON挂了号之后看一眼结果不刷新页面用户手抖又点了一次后端的事务里checkExists已经查过不存在第二次请求进来时第一次请求还没提交事务两个请求同时通过了检查都插入了记录。解决这是并发问题在本地低流量环境很难复现但答辩演示时一旦出现就是灾难。缓解办法是把挂号表的唯一索引加上让数据库兜底。执行 SQLALTER TABLE register_record ADD UNIQUE KEY uk_user_schedule (user_id, schedule_id, status);这个索引在 status0已预约时能完全挡住重复挂号但取消后 status 变成 2 再重新挂号时唯一键允许存在一条 status0 和一条 status2 的记录不会误伤。6.5 现象导出项目后在自己的电脑上运行数据库表里的中文全部变成问号原因建库时没有指定字符集或者 JDBC URL 里没加characterEncodingutf8mb4导致连接会话使用了 latin1 编码。项目在你同学电脑上是好的因为他 MySQL 默认字符集是 utf8mb4而你的 MySQL 5.7 默认是 latin1。解决创建数据库时显式指定字符集CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时确认 JDBC URL 里带characterEncodingutf8mb4。建表语句里每个 varchar 字段如果都写了编码那就更不会出问题。如果已经建好库并导入了数据需要先改库再改表ALTER DATABASE hospital CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;注意 convert 之后如果原有数据已经是乱码这个操作不会恢复乱码数据只能防止新写入的数据再乱。7. 答辩加分项用 Filter 做登录拦截与权限校验7.1 一个 Filter 搞定未登录拦截很多课设系统的未登录用户可以直接在地址栏输入内部页面 URL 绕过登录比如输入http://localhost:8080/hospital/appointment/list就能看到挂号记录这等于登录形同虚设。加一个 Filter 统一拦截是最省事的解法WebFilter(urlPatterns {/appointment/*, /user/*, /admin/*}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); User user session null ? null : (User) session.getAttribute(loginUser); String uri req.getRequestURI(); String path req.getContextPath(); // 放行登录和注册相关请求 if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.endsWith(/register.jsp) || uri.endsWith(/register)) { chain.doFilter(request, response); return; } // 管理员模块只放行 admin 角色 if (uri.startsWith(path /admin)) { if (user ! null admin.equals(user.getRole())) { chain.doFilter(request, response); } else { resp.sendRedirect(path /login.jsp?fromadmin); } return; } if (user null) { resp.sendRedirect(path /login.jsp?errornotlogin); return; } chain.doFilter(request, response); } }Filter 的逻辑分三层登录注册接口放行、管理员接口额外校验角色、普通用户接口只校验登录状态。req.getSession(false)里的 false 参数意味着如果当前没有 Session 就返回 null而不是新建一个这能避免绕过登录后服务器给每个请求都创建无意义的 Session。7.2 管理员模块的粗粒度权限控制管理员模块通常包括科室增删改、医生信息的编辑、排班管理、挂号记录查看。这些操作都在/admin/*路径下所以 Filter 里按 URL 前缀统一拦掉。真实项目会升级到 RBAC 权限模型但课设阶段用角色区分管理员和普通用户足够了。用户表里加一个role字段0 表示普通用户1 表示管理员。管理员账号可以在数据库里预置一条记录用户名 admin密码用 MD5 加密。答辩时老师问「管理员账号在哪创建」时直接回答 sql 脚本里预置了一条初始数据比现场用注册功能注册一个 admin 要显得更懂系统初始化流程。7.3 Filter 顺序的坑多个 Filter 时的执行链如果你的项目里同时有字符编码 Filter 和登录 Filter字符编码 Filter 必须注册在前面否则请求参数到达 LoginFilter 时中文已经是乱码状态。用WebFilter注解时Filter 的执行顺序不按类名排序而是依赖容器加载顺序这不是可靠的方式。我建议改用 web.xml 注册 Filter因为 XML 的 filter-mapping 顺序是精确的filter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-nameloginFilter/filter-name filter-classcom.example.filter.LoginFilter/filter-class /filter filter-mapping filter-nameloginFilter/filter-name url-pattern/appointment/*/url-pattern url-pattern/user/*/url-pattern url-pattern/admin/*/url-pattern /filter-mapping这个顺序意味着 EncodingFilter 最先执行设置 request 和 response 的编码为 UTF-8然后才轮到 LoginFilter 判断登录状态。即使 LoginFilter 拦截后重定向编码过滤器也已经生效了。这个细节平时看不出差别但一旦出现中文乱码很多人会在 Servlet 里各种转码其实是 Filter 顺序搞反了。8. 最后的自查清单提交前花 20 分钟跑一遍系统全部写完到提交之间我一般按这张清单过一遍能筛掉大部分扣分点。检查项操作方式预期结果注册功能用新手机号注册账号密码存储是 MD5 不可逆数据库里能看到加密串登录拦截不登录直接访问 /appointment/list跳转到登录页重复挂号用同一账号对同一排班挂两次第二次提示已挂过号源余量注册 3 个账号分别挂号余量递减正确取消挂号取消一条已预约记录余量回补、状态置为已取消管理员登录用 admin 账号访问 /admin/能看到管理菜单普通用户访问 /admin/跳转登录页而不是直接 500被 Filter 拦下字符集注册时输入中文姓名数据库里中文无乱码部署还原把项目拷贝到另一台电脑跑只改 db.properties 即可运行这个检查顺序是按照「从用户到管理员」的路径设计的每一步失败都能直接定位到具体模块。如果你有精力建议把数据库备份脚本也放在项目根目录的 sql 文件夹下用mysqldump导出这样老师拿到项目后导入数据就能看到预置的科室和医生记录印象分会高不少。最后说一句习惯我每次课设写完都会把 Tomcat 的 catalina.out 日志完整看一遍不是只看报错而是看有没有隐藏的 WARN 和 Exception 被打到了日志最后几行。很多答辩翻车现场都是老师操作时触发了某个你没测过的路径日志里早就有类似记录只是你没注意到。这条习惯保留到现在做线上项目也一直在用希望帮到你。本文还有配套的精品资源点击获取
返回列表