
简介面向Java初学者与Web开发者的病人挂号系统网站项目包含完整源码、需求文档和演示视频可一站式学习Java Web开发全流程。项目基于Java技术栈涉及Spring Boot、Spring MVC、MySQL及前端技术适合课程设计、毕业设计或项目实训。包体共5个文件含2个docx文档需求说明与项目报告、1个sql数据库脚本、1个wmv演示视频和1个rar源码包共21.41MB。已有151人学习浏览适合需要从零构建医疗预约系统的开发者。对照需求文档阅读源码可理解MVC分层、数据库表设计、用户认证及挂号业务逻辑配合演示视频能直观看到注册、登录、预约挂号、医生查询等功能的实际效果既能巩固理论也能提升调试排错能力。1. 一个Java病人挂号系统网站里比“挂号”更重要的是什么早上八点放号两分钟后某个热门科室的号源全部变成“约满”这几乎是每一套病人挂号系统网站上线后最先暴露的问题。表面上看到的是页面卡顿、余号刷新不及时实际原因往往不在前端而在后端的事务边界、SQL 扣减方式和并发控制上。这个基于 Java 技术栈实现的病人挂号系统网站正好把这个问题完整摊开源码负责把你带进 Spring Boot 的工程结构需求文档负责解释每个模块为什么存在演示视频则把“患者注册—选择科室—预约医生—确认挂号”的完整操作路径录了下来。适合正在学 Java Web 的开发者做课程设计也适合想快速搭一套医疗预约业务原型的工程师直接改造复用。2. 从需求文档到核心业务表病人挂号系统的数据模型设计2.1 需求文档先解决“谁在用、怎么用”的问题拿到这个资源不要急着解压源码跑起来先把那份《2020122702_病人挂号系统网站》需求文档从头翻一遍。需求文档里最值钱的部分不是功能列表而是角色定义和状态流转。这个系统里至少有三类角色患者负责注册、登录、挂号、取消预约医生负责查看自己的排班和接诊记录管理员负责维护科室、医生信息和号源配额。挂号状态是整个数据模型的灵魂。我习惯在数据库里用status字段配合一组枚举值表达0待就诊、1已完成、2已取消。这里不建议用字符串直接存“待就诊”这类中文值一来占空间二来写WHERE status 待就诊时容易因为全角半角问题查不出数据。需求文档里如果出现了“预约成功但未就诊”“爽约”这类描述那至少要再加一个状态而不是简单用已取消代替。把角色和状态理清后数据表的边界基本就出来了用户表、患者表、医生表、科室表、排班表、挂号记录表。这六张表是这个项目的核心其他如管理员表、操作日志表都属于外围支撑。读需求文档时重点标记“每个角色能做什么、不能做什么”这直接决定后面接口要不要做权限校验。2.2 一张能支撑在线挂号的 MySQL 表结构挂号记录表是整个系统里最值得细看的表。一个合格的病人挂号系统网站挂号记录不仅要能查到“哪个患者挂了哪个医生”还要能支撑同一天的排班冲突检测。下面这张表结构是从常见教学项目里提炼出来的简化版字段和索引都可以直接套用CREATE TABLE t_appointment ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, patient_id BIGINT NOT NULL COMMENT 患者ID关联t_patient, doctor_id BIGINT NOT NULL COMMENT 医生ID关联t_doctor, schedule_id BIGINT NOT NULL COMMENT 排班ID关联t_schedule, appointment_date DATE NOT NULL COMMENT 就诊日期, time_slot TINYINT NOT NULL COMMENT 时间段1上午 2下午, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待就诊 1已完成 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_patient_date (patient_id, appointment_date), KEY idx_doctor_date (doctor_id, appointment_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT挂号记录表;注意time_slot用TINYINT而不是VARCHAR是因为时间段是固定可枚举的取值用数字做比较和索引都比字符串高效。idx_patient_date和idx_doctor_date这两个联合索引分别服务于两类高频查询患者查“我挂了哪些号”医生查“某天谁挂了我的号”。这张表里有个容易被忽视的设计点appointment_date是日期而不是时间戳。挂号业务天然按就诊日聚合使用DATE类型可以让WHERE appointment_date 2025-06-01这类查询直接走索引。如果用DATETIME前端传参时稍微带点时分秒就会导致索引失效这是我在实际项目中踩过的坑。2.3 常见设计误区不要把身份证号当主键学生项目里最常见的错误是把id_card当主键理由是患者唯一。真实业务里身份证号会变比如升位、更正更重要的是挂号系统需要支持临时建档——有些患者没带身份证先登记姓名手机号就得能挂号。主键一旦是业务字段后面改也不是不加冗余字段又查不动。正确做法是主键用自增BIGINT或雪花 ID身份证号单独建普通字段并加唯一索引CREATE TABLE t_patient ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 患者ID, user_id BIGINT NOT NULL COMMENT 关联登录用户, name VARCHAR(50) NOT NULL COMMENT 患者姓名, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号可空, phone VARCHAR(20) NOT NULL COMMENT 手机号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者表;user_id关联登录账号id_card允许为 NULL 并用唯一索引保证“填写了就唯一没填也能建档”。这个设计兼顾了实名患者和临时患者的挂号场景。提示不要在appointment表里直接存患者身份证和医生手机号。需要显示时通过patient_id、doctor_id联表查询或者只在挂号记录里冗余“科室名称、医生姓名”这种几乎不变的历史快照字段敏感信息一律不落订单表。3. Spring Boot Spring MVC挂号接口是怎么拆出来的3.1 为什么选 Spring Boot 而不是传统 SSM现在做 Java Web 项目基本不会再从 XML 配置开始搭 SSM 工程。这个病人挂号系统网站选用 Spring Boot 的原因很实在内嵌 Tomcat一个java -jar就能起服务spring-boot-starter-web自动搞定 Spring MVC 和 Jackson 配置通过application.yml集中管理数据源、端口、日志级别对教学项目和中小型医疗站点都够用。整个后端按经典的三层结构拆Controller 层只做参数接收和结果返回Service 层写业务规则Mapper 层用 MyBatis 或 Spring Data JPA 操作数据库。Java 的跨平台优势在这个场景里也能直接感受到——源码在 Windows 上开发部署到 Linux 服务器跑只要 JDK 版本一致行为不会有差异。Spring MVC 在这个项目里的核心职责是 URL 映射和参数绑定。比如一个预约请求从浏览器发到/api/appointmentsDispatcherServlet 会根据PostMapping找到对应 Controller 方法再把 JSON 请求体反序列化成 Java 对象。这也是为什么需求文档里所有“功能需求”最终都能映射到一个或一组接口上的原因。3.2 预约挂号接口的分层实现先看 Controller 层。这个类只做三件事接收请求、调用 Service、返回统一结构RestController RequestMapping(/api/appointments) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping public ResultLong create(RequestBody Valid BookingRequest request) { Long appointmentId appointmentService.book(request); return Result.ok(appointmentId); } }BookingRequest是一个 DTO字段包含scheduleId、patientId、appointmentDate、timeSlot。用Valid触发参数校验这样前端传了空值或非法格式时在进入业务逻辑之前就会被拦截。Service 层的book方法才是挂号的核心Transactional(rollbackFor Exception.class) public Long book(BookingRequest request) { // 1. 锁定排班记录防止并发下超卖 Schedule schedule scheduleMapper.selectByIdForUpdate(request.getScheduleId()); if (schedule null) { throw new BizException(排班不存在); } // 2. 检查余量 if (schedule.getRemaining() 0) { throw new BizException(号源已约满); } // 3. 扣减余量 scheduleMapper.decreaseRemaining(schedule.getId()); // 4. 创建挂号记录 Appointment appointment new Appointment(); appointment.setPatientId(request.getPatientId()); appointment.setDoctorId(schedule.getDoctorId()); appointment.setScheduleId(schedule.getId()); appointment.setAppointmentDate(schedule.getScheduleDate()); appointment.setTimeSlot(schedule.getTimeSlot()); appointment.setStatus(0); appointmentMapper.insert(appointment); return appointment.getId(); }这里有几个关键点。第一方法上加Transactional意味着“扣减号源”和“插入挂号记录”要么都成功、要么都回滚。第二selectByIdForUpdate是悲观锁写法在第 3.3 节会详细讲。第三业务异常用自定义的BizException抛出由全局异常处理器统一转成友好提示而不是把堆栈直接抛给前端。3.3 并发抢号这行 update 才是关键科室号源就那么几个两个患者同时点预约如果代码是“先查余量再 update”就可能出现两个事务都读到remaining 1然后都执行扣减、都创建了挂号记录号源却变成负数。这个现象叫超卖在医疗挂号场景比电商更严重——号源超卖意味着医生得加号或者患者白跑一趟。解决思路有两种。第一种是用SELECT ... FOR UPDATE锁住排班行事务提交后其他事务才能继续读这行适合号源量不大的单体项目。第二种是乐观锁在t_schedule表里加version字段用一条 SQL 完成“检查并扣减”UPDATE t_schedule SET remaining remaining - 1, version version 1 WHERE id #{scheduleId} AND remaining 0;执行后判断影响行数如果等于 0说明号源已经被扣完或排班被改过直接返回“号源不足”。这种方法不需要锁表性能上更好我一般推荐在演示项目里用乐观锁既能说明并发问题又不会因为事务锁等待导致页面超时。提示UPDATE的WHERE条件里带上remaining 0是防超卖的最后一道防线。无论业务代码怎么绕只要这条 SQL 存在数据库层面就不会出现负数余量。3.4 统一响应体和异常兜底前后端约定一个统一的返回结构能省掉大量联调时间。这个项目里最简单的做法是这样public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.message success; r.data data; return r; } public static T ResultT fail(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }配合一个RestControllerAdvice全局异常处理器把BizException、参数校验异常、数据库异常分别映射成不同 code。这样 Controller 里不用写try-catch前端只需要判断code 0就能确定接口是否成功排查问题的时候直接看message字段比抓 HTTP 状态码直观得多。4. 页面交互与登录会话挂号流程在前端怎么落地4.1 挂号页的科室—医生联动演示视频里最常出现的画面就是挂号页先选科室再选医生然后看号源余量。这个页面用 HTML Bootstrap 配合少量 JavaScript 就能实现核心是一个级联下拉框。下面给出一个可运行的简化版本div classform-group label科室/label select iddepartmentSelect classform-control option value请选择科室/option /select /div div classform-group label医生/label select iddoctorSelect classform-control disabled option value请先选择科室/option /select /div script document.getElementById(departmentSelect).addEventListener(change, function () { const deptId this.value; if (!deptId) return; fetch(/api/departments/ deptId /doctors) .then(res res.json()) .then(data { const doctorSelect document.getElementById(doctorSelect); doctorSelect.innerHTML ; data.data.forEach(doc { const opt document.createElement(option); opt.value doc.id; opt.textContent doc.name 剩余号源 doc.remaining ; doctorSelect.appendChild(opt); }); doctorSelect.disabled false; }); }); /script这里fetch请求的是医生列表接口返回每个医生的剩余号源。把余量直接展示在下拉框文本里可以减少用户“选中医生后才看到约满”的挫败感。实际项目中还应该在提交预约时再向后端确认一次余量因为展示数据在用户停留期间可能已经变化。4.2 登录拦截器与 Session 会话这个项目是单体网站用 Session 做登录态比 JWT 合适得多。原因很简单服务端渲染页面需要知道当前登录用户是谁Session 天然和 Tomcat 容器绑定HttpSession直接拿用户信息JWT 适合前后端分离、需要跨域共享登录态的场景放在这个课程设计里反而增加复杂度。拦截器做登录校验只拦截需要认证的路径public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } return true; } }注册拦截器时用addPathPatterns(/**)拦截所有请求再用excludePathPatterns(/login, /register, /static/**)放行登录注册页面和静态资源。这里的关键是放行/static/**否则 CSS、JS、图片全被拦截页面样式会全部丢失。4.3 日期时间坑时区偏移和 LocalDate 传参前端选择就诊日期时最容易踩的坑是时区偏移。new Date()在浏览器里生成的是本地时间但通过 JSON 序列化时会转成 ISO 字符串并带Z后缀UTC 时间服务端收到后如果不做处理日期会差 8 小时。解决方式很直接前端统一用YYYY-MM-DD格式传字符串后端用LocalDate接收public class BookingRequest { NotNull(message 就诊日期不能为空) JsonFormat(pattern yyyy-MM-dd) private LocalDate appointmentDate; }JsonFormat(pattern yyyy-MM-dd)指定 JSON 反序列化格式LocalDate本身不带时间彻底绕开时区问题。如果直接用java.util.Date前端传2025-06-01会被解释成2025-06-01 00:00:00跨时区部署时就会出现挂号记录落库比预期早一天或少一天的诡异问题。5. 运行验证与部署排错把演示视频变成可验收的用例5.1 用演示视频反向梳理验收链路演示视频是现成的验收用例。打开视频对照下面的表格逐条验证比对着源码猜流程高效得多功能点操作步骤预期结果注册填写用户名、手机号、密码注册成功后跳转登录页登录输入账号密码跳转首页显示当前用户名选择科室首页点击“预约挂号”展示科室列表选择医生点击科室后加载医生列表显示医生姓名、职称、号源余量提交预约选择日期时间后确认提示预约成功生成挂号单号取消预约在“我的挂号”中取消状态变为已取消号源余量加回未登录拦截直接访问预约接口返回 401其中“取消预约后号源加回”是最容易漏掉的功能。源码里如果只有扣减没有回补说明需求文档里的“取消预约”没有落到实际业务逻辑上。号源回补同样要放在事务里否则用户取消成功但号源没恢复下一次放号前余量就一直少一个。5.2 三个高频翻车点第一MySQL 连接串不带时区参数启动后抛The server time zone value йʱ is unrecognized。在application.yml里显式配置spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai第二JDK 环境问题。项目如果基于 JDK 8 开发本地却装了 JDK 17编译时可能出现旧库不适配的报错。按照 Java 环境变量配置的要求把JAVA_HOME指到 JDK 8 安装目录PATH加上%JAVA_HOME%\bin然后用java -version确认版本一致再启动。第三静态资源 404。如果项目用了 Thymeleaf 或 JSP检查controller返回的视图路径是否和templates、webapp下的文件层级对应。Spring Boot 默认静态资源目录是classpath:/static/把 CSS、JS 放到src/main/resources/static/页面里引用时去掉static前缀。5.3 看日志判断“号源扣了但预约没生成”排查这类问题我一般用一条命令先确认事务是否回滚java -jar hospital-system.jar --debug app.log 21启动后复现一次预约操作打开app.log搜索Rolling back如果看到Transaction rolled back说明book方法中途抛了异常挂号记录没插入号源扣减也被回滚了。这时重点检查两点decreaseRemaining的影响行数是不是 0appointmentMapper.insert的字段有没有遗漏非空列。如果日志里没有回滚记录但挂号记录表里确实没有数据那就要看t_appointment表的主键是否冲突、schedule_id外键是否指向了不存在的排班。另一个有用的技巧在application.yml里加一行logging.level.com.example.mapperdebug把 MyBatis 的 SQL 打出来逐条核对UPDATE和INSERT的参数值。这样配合演示视频里的操作步骤基本十分钟内就能定位问题出在业务代码还是 SQL 语句。本文还有配套的精品资源点击获取