ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

医院预约挂号系统Java源码解析:号源并发与防超卖设计

医院预约挂号系统Java源码解析:号源并发与防超卖设计 简介这是一套基于Java的医院预约挂号系统设计源码面向需要完成课程设计、毕业设计或JavaWeb开发练习的高校学生与初级开发者聚焦传统排队挂号耗时、就医体验差等痛点覆盖在线查询医生排班、预约专家门诊、查看预约状态等核心环节并支持医院工作人员管理预约信息、调整排班逻辑完整且贴近实际业务场景。压缩包内含47个文件以22个Java源文件为主配合8个XML配置、数据库SQL脚本、Java工程配置文件及9张项目截图整体约499KB目录划分清晰便于导入IDE后直接运行与二次开发。资源附带完成截图和任务分解图可直观对照界面效果与功能模块readme说明文件也提供了基本使用引导。目前已有382人学习浏览适合作为医院预约挂号类项目的完整参考可快速搭建基础框架理解预约业务流转与数据表设计。1. 医院预约挂号系统的源码难在“号”而不在“挂”“医院预约挂号的源码外行看页面内行看号源。”预约挂号系统区别于普通管理系统的唯一硬点是“号”的状态流转放号、锁定、退号释放、停诊作废每一步都牵扯数据一致性。一个医生半天能接诊的总量是固定的系统不能让两个患者拿到同一个号这决定了不能靠简单地插入一条预约记录就算完事。这篇内容要拆的就是围绕这个“号”展开的Java系统设计从Spring Boot的分层架构、MySQL表结构到核心预约接口的并发实现再到上线前验证不超卖。适合医疗信息化方向的Java开发也适合准备拿这套源码做二次开发的技术负责人。2. 技术选型与分层Java预约挂号系统的架构成型2.1 单体优先为什么预约挂号不盲目上微服务医院预约挂号系统的访问模型是突发型而非持续型平时每分钟几十个请求但放号那一刻几百上千个请求会集中在几秒内打进来。这种模式决定了架构的第一诉求是“峰值扛得住、坏了修得快”而不是“弹性扩展”。大多数医院部署环境是院区机房资源有限运维习惯是一个Tomcat和一个MySQL实例。单体架构在这个场景下优势很明显事务边界清晰不会出现跨服务的分布式一致性问题。微服务很容易把“退号扣库存”拆成两次调用一旦中间某个服务超时一致性隐患就埋下了。现网多数能稳定运行的预约挂号系统入口层和业务层仍是单体Redis作为缓存、消息队列兜底。技术栈通常落在Spring Boot 2.7.x加MyBatis加MySQL 8.0JDK版本跟着Spring Boot走JDK 8对应2.7JDK 17对应3.x。预约动作本身尽量直接落库不要引入Redis扣减否则缓存与数据库的一致性会成为新痛点。Nginx放在前面做反代和静态资源MySQL独立在一台机器或云RDS上Tomcat只承担业务计算。这个部署形态足够覆盖一家三甲医院的日常放号量。2.2 分层结构一个能支撑二次开发的包划分拿到源码先看包结构规范的分包一眼能看出业务边界。下面是这个标题下最常见的工程布局com.hospital.registration ├── controller // HTTP接口只做参数校验和结果包装 │ ├── AppointmentController.java │ └── ScheduleController.java ├── service // 业务规则与事务边界 │ ├── AppointmentService.java │ └── ScheduleService.java ├── mapper // MyBatis数据访问层 │ ├── DoctorMapper.java │ └── AppointmentSourceMapper.java ├── entity // 与数据库表对应的实体 │ ├── Doctor.java │ └── AppointmentSource.java └── common // 统一返回体、异常、工具类 ├── Result.java └── BizException.java这个结构里最容易被写偏的是controller。不少源码的controller里直接注入Mapper业务逻辑写在HTTP层导致事务注解形同虚设。我一般会要求controller里不出现任何Mapper引用所有数据操作从service进入service方法的命名体现业务动作例如createAppointment()、cancelAppointment()而不是updateSource()这种纯数据术语。后续接微信小程序或第三方平台时controller可以重写service直接复用这是分层带来的最大红利。提示不要在controller上加Transactional。Spring的事务代理只对通过代理进入的public方法生效自调用和controller直接调service时事务经常不触发。2.3 连接池与超时参数放号场景的HikariCP配置HikariCP是Spring Boot的默认连接池默认配置能跑通开发环境但放号场景必须手动调。下面是一组参考配置spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 max-lifetime: 1800000参数含义分别是连接池最多30条连接空闲保持10条等待获取连接超过30秒抛出异常连接最大生命周期30分钟。连接数不是越大越好超过CPU核数的4倍以后线程切换成本会吞掉性能收益。放号高峰期如果锁等待频繁先查SHOW PROCESSLIST看是不是连接都在updating状态再决定是否调大连接数不要盲目加池。3. 数据库设计排班、号源与预约记录怎么建模3.1 五张核心业务表的字段与关系预约挂号领域简化后是五张表科室表、医生表、排班表、号源表、预约记录表。科室与医生是一对多医生与排班是一对多排班与号源是一对多号源与预约记录是一对一。下面这个表格把核心表的职责列清楚表名核心字段作用departmentid, name科室维度关联医生doctorid, department_id, name医生主数据scheduleid, doctor_id, work_date, time_slot某医生某天某半天出诊放号总量appointment_sourceid, schedule_id, seq_no, patient_id, status每一个具体的号状态机载体appointment_recordid, source_id, patient_id, status预约行为落库与号源一一对应其中排班、号源、预约记录三张表是重点。排班表记录“上午还是下午、总共放多少号”号源表把总量拆成一排物理行每个行代表一个号预约记录表负责留存患者和号源的关系。核心建表语句如下CREATE TABLE schedule ( id INT AUTO_INCREMENT PRIMARY KEY, doctor_id INT NOT NULL COMMENT 医生ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, total INT NOT NULL COMMENT 总号数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停诊, UNIQUE KEY uk_doctor_date (doctor_id, work_date, time_slot) ) ENGINEInnoDB COMMENT医生排班表; CREATE TABLE appointment_source ( id INT AUTO_INCREMENT PRIMARY KEY, schedule_id INT NOT NULL COMMENT 排班ID, seq_no INT NOT NULL COMMENT 第几号从1开始, patient_id INT DEFAULT NULL COMMENT 占用该号的患者, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约 1锁定 2已约 3退号, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本, UNIQUE KEY uk_schedule_seq (schedule_id, seq_no) ) ENGINEInnoDB COMMENT号源表; CREATE TABLE appointment_record ( id INT AUTO_INCREMENT PRIMARY KEY, source_id INT NOT NULL COMMENT 号源ID, schedule_id INT NOT NULL COMMENT 排班ID冗余, patient_id INT NOT NULL COMMENT 患者ID, status TINYINT NOT NULL DEFAULT 1 COMMENT 1有效 0取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_source (source_id), UNIQUE KEY uk_patient_schedule (patient_id, schedule_id) ) ENGINEInnoDB COMMENT预约记录表;逻辑说明uk_schedule_seq保证同一排班下不会生成重复的序号uk_source保证一个号只能对应一条预约记录uk_patient_schedule防止同一个人反复预约同一排班。业务层做染色校验只是第一道防线真正的硬约束在这三个唯一索引上。3.2 号源为什么要单独拆表而不是在排班上放一个剩余数最简单的方案是在schedule表上直接放remain字段每次预约给remain-1。这样做代码少但暴露三个问题第一用户想选具体几号时没有对象可选第二审计时无法追溯“这个号之前被谁占用过”第三退号时无法精确释放某一条记录。拆成appointment_source以后每个号有独立状态患者可以在前端看到还剩几号、某一段是否被约满退号时只需把对应source记录的状态改回0逻辑非常直白。排班停诊或改期时批量更新号源也方便一条SQL把某个schedule_id下所有status0的号源改成停用不需要反向计算remain。这个表结构还有一个隐含价值在二次开发时如果要增加“指定医生、指定号段”的预约锁号功能直接在source表上加一个锁定字段就可以不必反推业务逻辑。3.3 索引设计别让放号查询变成全表扫描放号的核心查询是按医生、日期、时段查可约号源。appointment_source表上除了唯一索引还需要在schedule_id上加普通索引因为查询通常从排班定位号源。实际项目中更常见的SQL是先查schedule拿到排班ID再查source所以两级索引都要建。如果后续要支撑“按科室查医生排班”的列表页给doctor表的department_id建普通索引即可。不需要把每个字段都加上索引索引写得多插入和更新成本会成倍增长预约高频场景会更明显。4. 核心预约流程的Java实现查询、占号与并发兜底4.1 号源查询接口的Mapper与Service实现预约的第一步是查号。前端传医生ID、日期、时段后端返回该时段所有可约号源。Mapper用注解方式实现即可避免XML文件在二次开发时被误改Mapper public interface AppointmentSourceMapper { Select( SELECT s.id, s.schedule_id, s.seq_no FROM appointment_source s JOIN schedule d ON d.id s.schedule_id WHERE d.doctor_id #{doctorId} AND d.work_date #{workDate} AND d.time_slot #{timeSlot} AND s.status 0 ORDER BY s.seq_no ) ListSourceVO listAvailable(Param(doctorId) Long doctorId, Param(workDate) LocalDate workDate, Param(timeSlot) Integer timeSlot); }逻辑说明查询条件里的status 0是核心筛选条件只把当前可预约的号放出来锁定中的号不进入候选列表。ORDER BY seq_no保证前端展示顺序稳定。参数说明timeSlot用整数而不是字符串避免上午、下午这类文字在前后端传参时出现编码问题。这个接口只读不写在高并发下可以放心加Redis缓存缓存键设计成doctorId:workDate:timeSlot退号或预约成功后再主动删除缓存。4.2 占号动作条件更新是防超卖的关键预约接口是整个系统风险最高的地方Java面试里常说的乐观锁、CAS语义不一定非要给实体加version字段在UPDATE条件里把status写进去实现的就是同一个效果。先看核心代码Mapper public interface AppointmentSourceMapper { Update( UPDATE appointment_source SET status 2, patient_id #{patientId}, version version 1 WHERE id #{sourceId} AND status 0 ) int occupySource(Param(sourceId) Long sourceId, Param(patientId) Long patientId); }Service public class AppointmentService { private final AppointmentSourceMapper sourceMapper; private final AppointmentRecordMapper recordMapper; Transactional(rollbackFor Exception.class) public Long createAppointment(Long sourceId, Long patientId) { // 条件更新返回0说明这一行状态已经不是可约 int affected sourceMapper.occupySource(sourceId, patientId); if (affected 0) { throw new BizException(号源已被预约或锁定请重新选择); } // 写预约记录 AppointmentRecord record new AppointmentRecord(); record.setSourceId(sourceId); record.setPatientId(patientId); record.setStatus(1); recordMapper.insert(record); return record.getId(); } }逻辑说明occupySource是单条UPDATEMySQL在更新时会对这一行加行锁两个并发请求同时更新同一行时只有一个能等到锁并成功执行。WHERE id #{sourceId} AND status 0相当于把“状态是否为可约”这个判断和“扣号”合并成一个原子操作中间没有空隙所以不需要先SELECT再UPDATE。version version 1在这里用于审计和后续可能的乐观锁比对不参与超卖判断status条件本身已经完成并发控制。参数说明事务注解放在service方法的public入口上rollbackFor Exception.class确保自定义业务异常也能触发回滚。号源UPDATE和预约记录INSERT在同一个事务里前者成功后者失败时整体回滚不会出现“号被锁但记录没有”的脏数据。隔离级别保持MySQL默认的REPEATABLE READ即可不需要调低。4.3 锁等待与事务瘦身放号高峰期容易出现Lock wait timeout exceeded异常原因多半是事务里塞了太多无关操作。上面这段代码的优化原则是事务内只做必须同步完成的数据库动作把权限校验、短信发送、消息推送全部挪到事务外面。注意Transactional只对public方法生效同一个类内部方法互相调用时事务不拦截这是源码里最常见的失效场景。发现锁等待频繁时先用SHOW ENGINE INNODB STATUS看事务持有锁的trx再考虑是否缩短事务时间。4.4 定时清理过期锁定锁定状态的收尾机制如果业务模型包含“先锁定后支付”锁定状态需要有释放机制。常见做法是定时任务扫描超时记录Component public class SourceLockReleaser { private final AppointmentSourceMapper sourceMapper; Scheduled(fixedRate 60_000) Transactional public void releaseExpiredLocks() { // 找10分钟前仍是锁定状态的号源 ListLong ids sourceMapper.listExpiredLock(10); if (!ids.isEmpty()) { sourceMapper.releaseLockByIds(ids); } } }逻辑说明fixedRate 60_000表示每60秒触发一次扫描把超过10分钟仍处于锁定状态的号源重置为可约并把patient_id清空。参数说明扫描间隔和过期时间要可配置因为不同医院对支付超时时长的要求不一样。这个定时任务适合挂在Spring Boot里任务量不大不需要额外引入分布式调度框架。5. 并发验证与上线排错清单5.1 用ab做一次号源并发验证验证系统不超卖最直接的方法是用ab压测并对同一号源发起并发请求ab -n 100 -c 50 -T application/json -p book.json \ http://127.0.0.1:8080/api/appointment/book-n 100表示总请求数100-c 50表示同时50个并发book.json中固定传同一个sourceId。跑完后检查两点ab报告中的Failed requests应为0且数据库里该号源对应的预约记录只能是1条SELECT COUNT(*) FROM appointment_record WHERE source_id 1001;返回结果必须是1同时appointment_source中该行的状态应该是2patient_id是最后成功那个请求的ID。如果COUNT结果大于1说明事务边界或条件更新没有生效回到第4章的代码重新核对。5.2 三个高频坑位与检查清单坑位症状处理方式连接池过小放号高峰出现connection is not available调大HikariCP的maximum-pool-size并检查MySQL的max_connections事务注解失效号源被锁但预约记录缺失确认Transactional在service公开方法上且不是同类内部调用定时任务误释放已支付患者号源被回收释放前对比记录表状态只处理10分钟内无关联有效记录的锁定源三个坑在真实源码里出现的频率由高到低依次是连接池、事务失效、定时任务误伤。排查顺序建议先看日志有没有异常再看SHOW PROCESSLIST确认数据库连接是否堆积最后检查事务方法的调用链。这套源码想在本地跑通还需要注意一个细节book.json里的JSON字段名要与Controller层的RequestBody对象字段严格一致否则ab发起的所有请求都会变成参数绑定错误压测数据没有参考价值。用curl -X POST -H Content-Type: application/json -d book.json先验证一次单发成功再跑并发。本文还有配套的精品资源点击获取
返回列表