恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot医院挂号系统实战:号源设计、事务扣号与避坑指南
首页
资讯中心
/
SpringBoot医院挂号系统实战:号源设计、事务扣号与避坑指南
SpringBoot医院挂号系统实战:号源设计、事务扣号与避坑指南
发布时间:2026/10/7 18:20:20
简介本资源为基于SpringBoot的医院挂号就诊系统完整项目源码面向计算机专业学生、Java初学者及需要完成课程设计或毕业设计的人群帮助解决挂号、就诊信息管理等场景下的系统开发与学习需求。项目采用Java、SpringBoot、Vue、Ajax、Maven、MySQL、MyBatisPlus等技术栈前端使用ElementUI与B/S架构涵盖用户信息管理、图片素材管理、视频素材管理等功能模块并附有绪论、相关技术介绍、系统分析、系统设计、系统实现等完整文档章节。压缩包共770个文件包含101个Java源码、60个Vue组件、157个JavaScript脚本、50个CSS样式、33个HTML页面及大量svg、gif、jpg等图片素材另有xml配置、yml文件与bat启动脚本整体约34.42MB目录结构清晰便于按模块查阅与二次开发。目前已有162人学习下载适合需要完整项目参考、技术栈练手或毕设素材的读者。1. 从一张挂号单说起SpringBoot 医院挂号就诊系统到底在解决什么早上七点半门诊大厅已经排了三十多人窗口里护士一边翻纸质登记本一边喊名字患者拿着手写小票去对应科室结果科室门口又排一次队。这个场景里真正被浪费的不是患者的时间而是医院对号源、医生排班、就诊状态这三件事完全没有实时掌控。基于 SpringBoot 的医院挂号就诊系统要解决的就是把「号源」变成可被程序管理的资源哪个科室、哪位医生、哪个时间段、还剩几个号、谁抢到了、谁爽约了全部落库并可追溯。它适合正在做 Java 课程设计的学生、需要一套可二次开发门诊模块的初级工程师以及想用 SpringBoot 练手真实业务建模的后端开发者。热词里反复出现的 springboot、java、源码本质上都指向同一件事——你需要一套能跑起来、能改、能讲清楚数据流转的工程骨架而不是一个只能看不能动的演示页面。2. 号源、排班、订单三张表怎么设计才不返工2.1 先想清楚挂号系统的核心实体关系很多同学一上来就写 Controller结果写到第三天发现号源扣减对不上回头改表结构血泪经验。挂号就诊系统的数据模型其实只有三条主线医生排班决定「有哪些号可挂」号源记录决定「每个号当前状态」挂号订单决定「谁在什么时候占了哪个号」。这三者必须用外键或业务字段串起来否则并发一上来就是玄学超卖。我一般会先画实体关系再建表。核心表包括department科室、doctor医生、schedule排班医生日期时段、source号源排班下具体序号、registration挂号订单、patient患者。其中schedule和source是一对多source和registration是一对一一个号只能被一个有效订单占用。这个关系定死了后面扣号、退号、爽约释放才有依据。2.2 建表 SQL 与关键字段说明下面这段 SQL 是可直接在 MySQL 8 执行的精简版字段命名保持和 Java 实体一致方便 MyBatis-Plus 映射。-- 科室表 CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 科室名称, location VARCHAR(128) COMMENT 门诊位置 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生表 CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, title VARCHAR(32) COMMENT 职称, department_id BIGINT NOT NULL, KEY idx_dept (department_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表一个医生一天一个时段一条记录 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 1上午 2下午, total_source INT NOT NULL COMMENT 总号源数, remain_source INT NOT NULL COMMENT 剩余号源数, UNIQUE KEY uk_doc_date_slot (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号订单表 CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, source_no INT NOT NULL COMMENT 第几号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已挂号 2已就诊 3已取消 4爽约, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_schedule_source (schedule_id, source_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明schedule上的唯一键保证同一医生同一天同一时段不会重复排班registration上的uk_schedule_source是防超卖的兜底即使应用层判断失误数据库也会拒绝重复占号。参数上time_slot用 TINYINT 而不是字符串是为了后续按上午下午统计时索引更小status用数字枚举避免中文状态在代码里到处硬编码。提示remain_source是冗余字段目的是减少每次挂号都去 count 订单。它必须和registration在同一个事务里更新否则会出现「显示有号但下不了单」。2.3 用 MyBatis-Plus 生成实体与 Mapper热词里有人搜「mybatisplus根据java实体类生成创建表的sql语句」方向反了更常见的是先有表再生成实体。SpringBoot 项目里引入依赖后实体加注解即可。Data TableName(schedule) public class Schedule { TableId(type IdType.AUTO) private Long id; private Long doctorId; private LocalDate workDate; private Integer timeSlot; private Integer totalSource; private Integer remainSource; }逻辑说明TableName指定表名TableId声明自增主键。LocalDate对应 MySQL 的 DATEMyBatis-Plus 3.x 默认支持 JSR-310 类型不需要额外配置。参数上如果数据库字段是下划线而实体是驼峰开启map-underscore-to-camel-case: true即可不用每个字段写TableField。3. 挂号扣号接口从 Controller 到事务的完整落地3.1 扣号为什么必须放在一个事务里挂号的核心动作只有一句把schedule.remain_source减一同时插入一条registration。这两步如果分开提交中间任何一步失败都会导致号源和订单不一致。常见翻车场景是先减库存成功插入订单时唯一键冲突抛异常库存没回滚号就凭空少了一个。所以 Service 方法必须加Transactional并且异常要往外抛不能自己 catch 掉。3.2 可复现的挂号 Service 代码Service public class RegistrationService { Autowired private ScheduleMapper scheduleMapper; Autowired private RegistrationMapper registrationMapper; Transactional(rollbackFor Exception.class) public Long register(Long patientId, Long scheduleId) { // 1. 悲观锁锁定排班行防止并发扣减 Schedule schedule scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule null) { throw new BizException(排班不存在); } if (schedule.getRemainSource() 0) { throw new BizException(号源已挂完); } // 2. 计算本次挂到的序号 int sourceNo schedule.getTotalSource() - schedule.getRemainSource() 1; // 3. 扣减号源 schedule.setRemainSource(schedule.getRemainSource() - 1); scheduleMapper.updateById(schedule); // 4. 写入挂号订单 Registration reg new Registration(); reg.setPatientId(patientId); reg.setScheduleId(scheduleId); reg.setSourceNo(sourceNo); reg.setStatus(1); registrationMapper.insert(reg); return reg.getId(); } }逻辑说明selectByIdForUpdate对应 SQLSELECT ... FOR UPDATE在事务内锁住这一行排班其他并发请求会排队等待从而避免两个线程同时读到remain_source1都去扣。sourceNo用「总数减剩余加一」推算保证序号连续且不重复。参数上rollbackFor Exception.class确保受检异常也回滚这是很多人漏掉的一行。对应的 Mapper 方法Select(SELECT * FROM schedule WHERE id #{id} FOR UPDATE) Schedule selectByIdForUpdate(Param(id) Long id);注意FOR UPDATE必须在事务内才生效。如果 Service 没加Transactional这条 SQL 会退化成普通查询锁不住任何东西。3.3 并发压测时看什么指标本地可以用 JMeter 或ab对挂号接口发 200 并发观察三件事一是registration表有没有重复的(schedule_id, source_no)二是remain_source最终是否等于total_source减去有效订单数三是接口平均响应时间是否因为行锁飙升。如果响应时间从 20ms 涨到 800ms说明锁竞争严重这时候要考虑把号源按时间段拆细而不是继续加锁粒度。4. 排班与号源生成别让护士手工录一百条4.1 批量排班的常见做法真实门诊里医生排班是按周或按月制定的如果让护士在页面上一条条点基本没人愿意用。常见做法是提供一个「按周期生成排班」的接口输入医生、起始日期、结束日期、上午下午是否出诊、每时段号源数后端循环插入schedule记录。这里要注意跳过医生已有的排班否则唯一键会报错。4.2 批量生成排班的代码与边界处理Transactional(rollbackFor Exception.class) public int batchCreateSchedule(BatchScheduleDTO dto) { int count 0; LocalDate cursor dto.getStartDate(); while (!cursor.isAfter(dto.getEndDate())) { for (Integer slot : dto.getTimeSlots()) { // 已存在则跳过避免唯一键冲突 Long exists scheduleMapper.selectCount( new LambdaQueryWrapperSchedule() .eq(Schedule::getDoctorId, dto.getDoctorId()) .eq(Schedule::getWorkDate, cursor) .eq(Schedule::getTimeSlot, slot)); if (exists ! null exists 0) { continue; } Schedule s new Schedule(); s.setDoctorId(dto.getDoctorId()); s.setWorkDate(cursor); s.setTimeSlot(slot); s.setTotalSource(dto.getSourcePerSlot()); s.setRemainSource(dto.getSourcePerSlot()); scheduleMapper.insert(s); count; } cursor cursor.plusDays(1); } return count; }逻辑说明外层循环日期内层循环时段每次插入前先查重。参数上sourcePerSlot建议不超过 50超过这个数单个医生一上午也看不完属于业务不合理而非技术问题。timeSlots用 List 传入前端可以勾选上午、下午。4.3 号源序号是生成时定还是挂号时定这是一个容易争论的点。我的做法是挂号时定序号也就是第 3 章里的sourceNo推算逻辑。原因是排班生成时还不知道谁会来提前把 1 到 N 号写进source表虽然直观但退号后序号会出现空洞患者看到「3 号、5 号」会疑惑。挂号时按剩余量推算序号始终连续退号后下一个患者补上体验更顺。5. 避坑与排查挂号系统上线前必须过的五道坎5.1 号源显示有但下单提示已挂完现象列表页显示剩余 3 个号点进去提交却返回「号源已挂完」。原因通常是列表查询走了缓存或从库而扣号走主库两者有延迟。解决列表页的剩余号源直接读主库或者接受短暂不一致但在提交时以主库为准前端把「已挂完」作为正常业务提示而不是系统错误。5.2 同一患者重复挂号同一时段现象一个患者连点两次提交生成两条订单。原因是没有做患者维度的幂等控制。解决在registration上加(patient_id, schedule_id, status)的业务唯一约束或者在 Service 入口用 Redis 锁按patientIdscheduleId加锁提交前先查是否已有有效订单。5.3 退号后号源没有回滚现象患者取消挂号但remain_source没变号白白浪费。原因退号逻辑只改了订单状态忘了加回号源。解决退号 Service 同样要加Transactional先selectByIdForUpdate锁排班再把remain_source加一最后把订单状态改为 3。三步顺序不能乱。5.4 时间字段时区导致排班错一天现象本地测试正常部署到服务器后当天排班查不到。原因LocalDate和数据库时区、JVM 时区不一致。解决统一在application.yml里设置spring.jackson.time-zone: GMT8数据库连接串加serverTimezoneAsia/Shanghai并且排班日期只用 DATE 类型不要用 DATETIME 存日期。5.5 接口返回慢但数据库 CPU 不高现象挂号接口偶尔卡顿数据库监控却正常。原因FOR UPDATE锁等待某个事务长时间未提交导致后续请求排队。解决检查是否有事务里调用了外部接口或做了耗时计算把非数据库操作移出事务同时给schedule的(doctor_id, work_date)加索引减少锁扫描范围。6. 从能跑到好用给挂号系统加一层状态机与对账把系统跑起来只是第一步真正让医院愿意用的是状态可追踪。挂号订单的status不应该在代码里到处set而是收敛成一个状态机1 已挂号 → 2 已就诊 / 3 已取消 / 4 爽约。每次状态变更都走同一个方法方法内校验当前状态是否允许目标状态不允许就抛业务异常。这样做的好处是后面加「爽约三次限制挂号」这类规则时只需要在状态机入口加判断不用翻遍所有 Service。public void changeStatus(Long regId, int target) { Registration reg registrationMapper.selectById(regId); int current reg.getStatus(); // 只允许 1-2、1-3、1-4 if (current ! 1 || target 2 || target 4) { throw new BizException(当前状态不允许该操作); } reg.setStatus(target); registrationMapper.updateById(reg); }逻辑说明入口只接受从「已挂号」出发的变更已就诊的订单不能再取消已取消的不能再改成爽约。参数上target用常量类而不是魔法数字方便前端对齐。这个状态机不复杂但它是对账的基础。对账我一般写一个定时任务每天凌晨跑一次统计每个排班的total_source、有效订单数、remain_source三者对不上就告警。SQL 大致是SELECT schedule_id, COUNT(*) FROM registration WHERE status IN (1,2) GROUP BY schedule_id再和schedule表逐条比对。这个任务不写也行但一旦号源对不上没有对账就只能靠患者投诉来发现那代价就大了。最后说个我自己的习惯每次改完挂号或退号逻辑我都会手动在数据库里把remain_source改成 0再发一次挂号请求看系统是返回业务提示还是抛 500。如果抛 500说明边界没处理干净这种代码我不敢交给别人用。希望帮到你。本文还有配套的精品资源点击获取