news 2026/9/16 14:46:10

Java病人挂号系统网站开发:基于Spring Boot的数据模型与并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java病人挂号系统网站开发:基于Spring Boot的数据模型与并发控制

简介:面向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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='挂号记录表';

注意time_slotTINYINT而不是VARCHAR,是因为时间段是固定可枚举的取值,用数字做比较和索引都比字符串高效。idx_patient_dateidx_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者表';

user_id关联登录账号,id_card允许为 NULL 并用唯一索引保证“填写了就唯一,没填也能建档”。这个设计兼顾了实名患者和临时患者的挂号场景。

提示:不要在appointment表里直接存患者身份证和医生手机号。需要显示时通过patient_iddoctor_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/appointments,DispatcherServlet 会根据@PostMapping找到对应 Controller 方法,再把 JSON 请求体反序列化成 Java 对象。这也是为什么需求文档里所有“功能需求”最终都能映射到一个或一组接口上的原因。

3.2 预约挂号接口的分层实现

先看 Controller 层。这个类只做三件事:接收请求、调用 Service、返回统一结构:

@RestController @RequestMapping("/api/appointments") public class AppointmentController { @Autowired private AppointmentService appointmentService; @PostMapping public Result<Long> create(@RequestBody @Valid BookingRequest request) { Long appointmentId = appointmentService.book(request); return Result.ok(appointmentId); } }

BookingRequest是一个 DTO,字段包含scheduleIdpatientIdappointmentDatetimeSlot。用@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,说明号源已经被扣完或排班被改过,直接返回“号源不足”。这种方法不需要锁表,性能上更好,我一般推荐在演示项目里用乐观锁,既能说明并发问题,又不会因为事务锁等待导致页面超时。

提示:UPDATEWHERE条件里带上remaining > 0是防超卖的最后一道防线。无论业务代码怎么绕,只要这条 SQL 存在,数据库层面就不会出现负数余量。

3.4 统一响应体和异常兜底

前后端约定一个统一的返回结构,能省掉大量联调时间。这个项目里最简单的做法是这样:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "success"; r.data = data; return r; } public static <T> Result<T> fail(String message) { Result<T> 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 class="form-group"> <label>科室</label> <select id="departmentSelect" class="form-control"> <option value="">请选择科室</option> </select> </div> <div class="form-group"> <label>医生</label> <select id="doctorSelect" class="form-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;charset=UTF-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?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai

第二,JDK 环境问题。项目如果基于 JDK 8 开发,本地却装了 JDK 17,编译时可能出现旧库不适配的报错。按照 Java 环境变量配置的要求,把JAVA_HOME指到 JDK 8 安装目录,PATH加上%JAVA_HOME%\bin,然后用java -version确认版本一致再启动。

第三,静态资源 404。如果项目用了 Thymeleaf 或 JSP,检查controller返回的视图路径是否和templateswebapp下的文件层级对应。Spring Boot 默认静态资源目录是classpath:/static/,把 CSS、JS 放到src/main/resources/static/,页面里引用时去掉static前缀。

5.3 看日志判断“号源扣了但预约没生成”

排查这类问题我一般用一条命令先确认事务是否回滚:

java -jar hospital-system.jar --debug > app.log 2>&1

启动后复现一次预约操作,打开app.log搜索Rolling back,如果看到Transaction rolled back,说明book方法中途抛了异常,挂号记录没插入,号源扣减也被回滚了。这时重点检查两点:decreaseRemaining的影响行数是不是 0;appointmentMapper.insert的字段有没有遗漏非空列。

如果日志里没有回滚记录,但挂号记录表里确实没有数据,那就要看t_appointment表的主键是否冲突、schedule_id外键是否指向了不存在的排班。另一个有用的技巧:在application.yml里加一行logging.level.com.example.mapper=debug,把 MyBatis 的 SQL 打出来,逐条核对UPDATEINSERT的参数值。这样配合演示视频里的操作步骤,基本十分钟内就能定位问题出在业务代码还是 SQL 语句。

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

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

微信小程序超市购物系统代码使用指南:从解压到跑通全流程

简介&#xff1a;这套基于微信小程序的超市购物系统代码&#xff0c;面向正在学习小程序开发、需要完成课程设计或搭建线上购物场景的开发者与研究者。系统覆盖商品浏览、搜索、购物车、订单生成等核心流程&#xff0c;并涉及用户管理、支付接口、订单跟踪等扩展特性&#xff0…

作者头像 李华
网站建设 2026/9/16 14:41:06

GC3909S一芯双驱:中小功率运动控制的集成化新范式

1. 为什么这颗“一芯双驱”的GC3909S正在悄悄改变中小功率运动控制的底层逻辑你有没有遇到过这样的场景&#xff1a;给一台桌面级3D打印机加装第二路Z轴同步升降&#xff0c;结果发现主控板上那块DRV8825已经占满IO口&#xff0c;再塞一块就得改PCB&#xff1b;或者调试一台轻型…

作者头像 李华