news 2026/10/15 21:46:01

SpringBoot+Vue3+MyBatis医院门诊在线挂号系统实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3+MyBatis医院门诊在线挂号系统实战解析

1. 一套门诊挂号系统的核心价值:不是做个网页那么简单

我接触过不少医院信息化的项目,其中门诊挂号系统算是"看起来简单、做起来全是细节"的典型代表。很多人以为就是做个列表页加一个预约按钮,真到医院场景里跑一遍就会发现,挂号背后牵扯着科室排班、医生号源、患者档案、支付状态、取消规则、号源释放、防重复挂号和防超卖这一整条链路。我今天要分享的这套Java SpringBoot + Vue3 + MyBatis 前后端分离的医院门诊在线挂号系统,就是围绕这条完整链路从零搭建的一套可运行源码,数据库用的是 MySQL,前后端通过 RESTful API 通信,整个项目结构清晰,非常适合做毕业设计、课设以及入门企业级全栈开发的练手项目。

这套系统能解决什么问题?往小了说,它让患者不用再跑到医院窗口排队,直接在网页上按科室、按医生、按日期查号源并完成挂号;往大了说,它把门诊业务里最核心的"号源分配"这件事做了一个规范化的线上闭环。无论你是刚学完 SpringBoot 和 Vue 想找个综合项目练手的学生,还是工作中需要快速交付一套挂号类系统的开发者,这套代码的思路和实现细节都有直接参考价值。

我拿到这套项目的时候,第一反应是去扒它的模块划分和数据库设计,因为挂号系统最怕的就是表结构设计不合理、号源并发控制不到位。接下来我按自己的拆解顺序,把整套系统的亮点、难点和值得复用的经验逐一展开来说。

2. 需求拆解与模块划分:先弄明白挂号业务的数据流向

2.1 门诊挂号涉及哪些真实业务角色

在写代码之前,我习惯先把业务角色画清楚。这套系统里至少有三类角色:患者(前端用户)、医院后台管理员(科室/医生/排班维护者)、系统本身(号源分配与状态流转控制)。患者关心的是:有哪些科室、哪些医生、什么时间有号、怎么挂上、怎么取消。管理员关心的是:科室怎么维护、医生怎么排班、每个时段放多少号、挂号记录怎么看。

这套源码在角色设计上用了简单的role字段区分管理员和普通用户,权限控制通过后端拦截器校验。实际业务里,这三类角色对应的功能边界如果不清晰,代码很快就会变成一锅粥。

2.2 前后端分离架构下,模块边界怎么划

这套项目的前后端是完全分离的:后端 SpringBoot 只提供接口,前端 Vue3 通过 Axios 调用接口渲染页面,两者之间通过 JSON + Token 做鉴权。模块划分上,我梳理出了下面这些核心模块:

模块职责关键接口
用户模块注册、登录、个人信息维护/api/user/register、/api/user/login
科室模块科室列表、按科室查医生/api/dept/list、/api/dept/doctors
医生模块医生信息、按医生查排班/api/doctor/info、/api/schedule/list
排班模块医生出诊时段与号源总数/api/schedule/add、/api/schedule/available
挂号模块提交挂号、取消挂号、状态查询/api/registration/submit、/api/registration/cancel
订单模块挂号订单支付状态管理/api/order/list、/api/order/pay

这个划分基本覆盖了在线挂号的全部核心链路。特别要注意的是排班和挂号是两个模块,排班负责"生成号源",挂号负责"消费号源",二者通过schedule_id关联。这一点如果设计时混在一起,后续做号源释放和排班调整会非常痛苦。

3. 数据库设计是这套系统的命门:五张核心表的建模思路

3.1 表结构设计的核心关系

我打开这套源码的 SQL 脚本后,第一感受是表结构没有乱来,五张核心表(用户表、科室表、医生表、排班表、挂号订单表)之间的关系清晰,字段命名也比较规范。其中最关键的是排班表(schedule)和挂号记录表(registration),这两张表的设计直接决定了系统能不能撑住真实场景的挂号压力。

排班表的关键字段是doctor_id、schedule_date、time_slot(上午/下午时段)、total_count(总号源数)、available_count(剩余号源数)。挂号记录表的关键字段是patient_id、schedule_id、status(待支付/已支付/已取消/已就诊)、visit_time。这里有一个很多人会忽略的点:剩余号源数为什么要冗余存储在排班表里,而不是每次去统计挂号记录?

答案是性能。如果每次挂号都去count一次挂号记录来算剩余号源,在号源多、查询频繁的场景下数据库压力会成倍增加。把available_count直接冗余在排班表里,配合后续的事务控制和行锁,就能在性能和准确性之间取得平衡。这个设计思路在秒杀类系统里也是常见做法。

3.2 用户表与医生表设计的细节亮点

用户表里除了常规的username、password,我还注意到源码里存了id_card(身份证号)和phone。身份证号的唯一约束至关重要,因为医院场景要求一个患者对应一个档案,如果允许一个身份证号注册多个账号,后续挂号记录归集和就诊历史查询都会乱套。

医生表则通过dept_id关联科室,字段里包含了title(职称)、intro(简介)、avatar(头像地址)等展示字段。这里我建议入门开发者特别注意:医生表不要在数据库里存排班信息,排班是动态的、有时间维度的,应该单独放排班表。把动态数据和静态数据分开,是数据库设计里一条非常实用的原则。

3.3 MySQL 索引与字符集设置

病从口入,数据库的坑大多在起步阶段就埋下了。这套源码的建表语句里用了InnoDB引擎和utf8mb4字符集,前者支持事务和行级锁(对号源扣减至关重要),后者能正常存表情符号和生僻字(患者姓名里经常出现)。

索引方面,排班表的(doctor_id, schedule_date)联合索引和挂号记录表的(patient_id, status)索引都是性价比非常高的设计。我自己做项目时的经验是:先根据查询场景设计索引,而不是建完表再拍脑袋加。每个索引都是拿写性能和存储空间换读性能,该建的建,不该建的别乱加。

4. 排班与号源扣减:SpringBoot+MyBatis 实现中最需要较真的地方

4.1 MyBatis 的 XML 映射与动态 SQL 使用要点

这套源码的持久层用的是 MyBatis,Mapper 接口加 XML 文件的经典组合。我特别看了一下排班查询的 SQL,典型的动态 SQL 写法:

<select id="findAvailableSchedules" resultType="com.xxx.pojo.Schedule"> SELECT s.*, d.name AS doctor_name, d.title AS doctor_title FROM schedule s LEFT JOIN doctor d ON s.doctor_id = d.id <where> <if test="deptId != null"> AND d.dept_id = #{deptId} </if> <if test="scheduleDate != null"> AND s.schedule_date = #{scheduleDate} </if> AND s.available_count > 0 </where> ORDER BY s.schedule_date, s.time_slot </select>

动态 SQL 的好处是前端可以根据用户勾选的筛选条件灵活传参,后端一套 SQL 搞定多种查询组合。这里有一个新手很容易踩的坑:if标签判断的是入参是否为空,如果入参是Integer类型,判断条件写<if test="deptId != null">没问题,但如果入参是String,还得加上!= ''的判断,否则前端传空字符串会导致 SQL 带上了不该带的过滤条件。

4.2 防止号源超卖的核心:事务与行级锁必须配合

挂号系统的技术难点,不是 CRUD,而是并发场景下怎么保证"最后一个号不会同时卖给两个人"。如果直接写这种代码:

@Transactional public void submitRegistration(RegistrationDTO dto) { Schedule schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule.getAvailableCount() > 0) { scheduleMapper.decreaseCount(schedule.getId()); registrationMapper.insert(createRegistration(dto, schedule)); } }

并发一上来就出问题。两个请求同时读到availableCount = 1,都判断大于 0,都去执行decreaseCount,结果就是一张排班表里只剩 0 个号源,却插入了两条挂号记录——超卖了。

正确的做法是利用数据库的行锁,也就是SELECT ... FOR UPDATE:

@Transactional public void submitRegistration(RegistrationDTO dto) { Schedule schedule = scheduleMapper.selectByIdForUpdate(dto.getScheduleId()); if (schedule == null || schedule.getAvailableCount() <= 0) { throw new BusinessException("号源已满"); } scheduleMapper.decreaseCount(schedule.getId()); registrationMapper.insert(createRegistration(dto, schedule)); }

对应 Mapper 里的查询语句是:

<select id="selectByIdForUpdate" resultType="com.xxx.pojo.Schedule"> SELECT * FROM schedule WHERE id = #{id} FOR UPDATE </select>

FOR UPDATE会把这一行数据锁住,直到事务提交或回滚,其他事务只能排队等待。这样一来,并发请求进来后,同一个排班行的更新操作就串行化了,超卖问题从根上被杜绝。我在实际项目中还做过一个优化:把decreaseCount的 SQL 写成UPDATE schedule SET available_count = available_count - 1 WHERE id = #{id} AND available_count > 0,用更新影响行数再做二次兜底判断,这就是乐观锁的思路,双保险更稳。

4.3 取消挂号的号源释放逻辑

有挂号就得有取消。取消挂号的本质是状态反转:挂号记录状态从"已支付/待支付"改成"已取消",同时排班表的available_count要加一。这一步也必须在同一个事务里完成,否则就会出现"挂号取消了但号源没恢复"或者"号源恢复了但挂号记录还是已支付"的数据不一致。

源码里对取消场景的边界处理得很好:限制取消条件为"就诊日期当天凌晨之前",超过这个时间点就不能线上取消,必须走线下窗口。这一点很符合现实中的医院规则,也避免了一些恶意反复占号的捣乱操作。

5. 前端 Vue3 的实现思路:页面组件怎么组织,状态怎么管

5.1 路由与权限控制的落地方式

前端 Vue3 部分,源码用的是 Vue Router 4 + Pinia 的组合。路由结构上分为公开页面和需要登录的页面:科室列表、医生排班这些是所有人可见的,但提交挂号操作必须先登录。实现方式是路由守卫里检查本地 Token 是否存在,如果访问受保护的路由且没有 Token,就跳转到登录页并带上redirect参数。

这里我建议初学者不要在前端做太重的权限控制,前端权限只是体验层面的拦截,真正的安全校验永远在后端接口。前端就算能绕过路由守卫拿到页面,没有有效的 Token,后端接口也会直接拒绝。配套的设计是 Axios 请求拦截器统一附带Authorization: Bearer <token>,响应拦截器统一处理 401 状态并跳回登录页。

5.2 医院主页与排班展示的关键组件拆分

这套系统的前端页面整体风格走的是后台管理 + 用户端混合的路线。用户端核心页面包括:医院首页(科室导航)、医生排班列表页、挂号确认页、我的挂号页。后台管理端包括:科室管理、医生管理、排班管理、挂号记录查看。

以排班列表页为例,组件拆分的思路很典型:

<template> <div class="schedule-page"> <DeptFilter @change="handleDeptChange" /> <DatePicker v-model="selectedDate" /> <ScheduleTable :schedules="scheduleList" @book="handleBook" /> </div> </template>

筛选组件、日期组件、排班表格组件各管一块,父组件负责数据请求和事件流转。ScheduleTable里每个排班行显示医生姓名、职称、出诊时段、剩余号数,如果剩余号数为 0,挂号按钮置灰并显示"约满"。这类状态展示做得好不好,直接影响真实用户的使用体验。

5.3 挂号表单校验与提交状态处理

挂号确认页的表单逻辑不复杂,核心就两个字段:就诊人选择、就诊时间确认。但这里有一个容易忽略的点:提交按钮必须防重复点击。用户在弱网环境下点了一次"确认挂号"没反应,很可能再点一次,后端如果没有做幂等处理,就会产生两条重复的挂号记录。

前端的处理方式是提交后立即把按钮置为 loading 状态并禁用;后端的处理方式更关键——在业务逻辑里按(patient_id, schedule_id, status in '待支付/已支付')做唯一性校验,同一个患者对同一个排班只能有一条有效挂号记录。这算是我个人反复强调的一道防线:前端防呆只是体验问题,后端防重才是数据底线。

6. 前后端联调与部署环节:我踩过的几个真实坑

6.1 跨域配置与 JWT 刷新问题

前后端分离项目联调的第一步就是跨域。源码后端做了一层 CORS 配置,允许指定来源跨域携带 Token。如果用的是 SpringBoot 3.x,CORS 配置类的写法和 2.x 略有差异,但是整体逻辑一样,关键是allowedOriginPatterns不能写死成*,否则前端携带 Cookie 时会出问题,而且安全上也说不过去。

JWT 这块我要多说一句:这套源码里 Token 的过期时间设置的是 2 小时。实际使用中用户可能挂着页面很久不操作,Token 过期后请求会直接 401。一个很实用的优化方案是加一个"静默刷新"机制:后端返回access_token和refresh_token,前端检测到 access_token 即将过期且本地有 refresh_token 时,自动调用刷新接口换新 Token,用户无感续期。这套源码里没有实现这个功能,但我在真实项目里是必须加的,否则用户挂机回来就被迫重新登录,体验很糟糕。

6.2 本地联调时 MySQL 时区与数据库连接串的坑

有相当一部分人跑不起来这套项目,问题都出在数据库连接串上。MySQL 8.x 的驱动要求 URL 里必须带serverTimezone=Asia/Shanghai,否则报时区错误。还有一个是useSSL=false可以避免本地环境 SSL 证书校验失败。我拿到这套源码后,第一件事就是把application.yml里的连接串改成:

spring: datasource: url: jdbc:mysql://localhost:3306/hospital_register?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

很多新手卡在这一步就放弃了,实在可惜。很多时候不是代码问题,就是环境配置里的一个时区参数。

6.3 生产环境部署的 Nginx 反向代理与前端构建

前端npm run build之后得到dist目录,生产环境我用 Nginx 托管静态文件,同时配置反向代理把/api开头的请求转发到后端 SpringBoot 服务。Nginx 配置核心如下:

server { listen 80; server_name your-domain; root /opt/hospital-frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 前端路由使用 history 模式时,必须配置这个 location / { try_files $uri $uri/ /index.html; } }

最后那个try_files配置是 Vue Router history 模式的标配,漏掉它就会出现"刷新页面 404"的诡异问题。这也是前后端分离项目部署时最高频的坑之一,我在好几个项目里都帮人排查过,最后都是这一行配置的问题。

7. 挂号系统源码的进阶扩展方向:别停在能跑就行的阶段

这套源码整体质量在线,作为学习和二次开发的基础非常合适。但如果你打算拿它应对真实场景,我建议在三个方向上做增强。

第一是引入 Redis 做号源缓存。排班表的available_count虽然用了行锁保证准确,但每次挂号都打一次数据库行锁,在高并发下数据库连接会非常紧张。用 Redis 的DECR命令做号源扣减,配合 Lua 脚本保证原子性,能扛住比纯数据库方案高一个量级的并发。等到号源扣减成功后,再异步写挂号记录和排班表。

第二是支付流程的完整对接。现在源码里的支付是模拟流程,真实场景需要对接微信支付或支付宝支付,这时订单表要增加支付流水号、支付时间、支付渠道这些字段,还要处理支付回调通知的幂等性——回调可能重复到达,必须保证业务处理只执行一次。

第三是消息通知机制。挂号成功、取消成功、就诊提醒,这些场景都是短信或微信公众号模板消息的触发点。我建议把这套系统里的状态流转事件抽象成一个消息发送的接口,后续接什么渠道都不需要动业务代码。

8. 我实际跑这套项目的一些体会

从拿到源码到把整个挂号流程跑通,我大概花了半天的时间。最耗时的地方不是写代码,而是理解号源扣减和取消释放这两个事务闭环。把这两个场景理清楚后,整套系统的代码读起来就非常顺畅了。整套项目给我的整体印象是:业务闭环完整,核心并发处理有意识,代码风格统一,适合作为学习和二次开发的基础。

最后分享一个我个人的小习惯:拿到任何一套前后端分离的项目源码,先别看前端页面长什么样,直接从数据库表结构和后端事务方法入手。前端页面永远是可以随时重写的皮,数据模型和业务事务才是整套系统的骨架。这套挂号系统的骨架,值得你花时间认真拆一遍。

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

同花顺Level-2全推行情Python API实战:逐笔委托与成交解析及订单簿重建

简介&#xff1a;这份《同花顺level-2全推行情Python API操作手册》面向从事高频交易、量化投资及涨停板策略的证券开发者&#xff0c;系统讲解如何通过Python API获取并处理沪深市场Level-2高频行情数据。手册以PDF形式交付&#xff0c;压缩包内仅1个文件&#xff0c;体积约85…

作者头像 李华
网站建设 2026/10/15 21:44:01

机器学习大作业全流程实战:从数据清洗到模型部署的完整指南

简介&#xff1a;本资源为南京邮电大学通达学院《机器学习》期末大作业的完整设计报告&#xff0c;面向修读机器学习课程、需要完成医疗保健方向系统设计任务的高校学生。报告以膀胱癌诊断为例&#xff0c;围绕数据驱动的智能医疗系统展开&#xff0c;涵盖数据收集、特征提取与…

作者头像 李华
网站建设 2026/10/15 21:43:16

iOS自动化代理IPA挂掉怎么办?从判断到预防一次讲清

做 iOS 自动化的人&#xff0c;对“主控端 设备端代理”这套组合应该都不陌生。EasyClick 用来编排脚本&#xff0c;真正在手机上干活的则是设备端那个代理组件&#xff0c;它被打包成 IPA 时&#xff0c;就是最常见的安装形态。用着用着几乎都会撞上一次&#xff1a;代理 IPA…

作者头像 李华
网站建设 2026/10/15 21:42:48

AI翻译神器 高效跨语言沟通必备实用工具推荐

研一新生必装 五个让你少走三年弯路的科研神器 刚进实验室的研一新生最容易陷入迷茫。不知道怎么确定研究方向&#xff0c;看不懂海量外文文献&#xff0c;不会设计合理的实验方案&#xff0c;数据处理和绘图一窍不通&#xff0c;写第一篇学术论文更是无从下手。别慌&#xff…

作者头像 李华
网站建设 2026/10/15 21:42:37

软件测试18个难题全解析:从用例设计到质量评估的实战指南

1. 软件测试为什么这么难&#xff1a;从一个老兵的视角说起软件测试这件事&#xff0c;外行看热闹&#xff0c;内行看门道。很多人觉得测试就是点点按钮、看看页面能不能打开、提交表单有没有报错&#xff0c;但实际上&#xff0c;真正做过几年测试的人都知道&#xff0c;这个领…

作者头像 李华
网站建设 2026/10/15 21:42:36

Python粒子系统实战:从爱心建模到物理仿真与光晕渲染

简介&#xff1a;本资源是一份面向Python初学者与趣味编程爱好者的爱心粒子动画实现教程&#xff0c;聚焦粒子系统、贝塞尔曲线绘图与Pygame动画控制三大核心技能&#xff0c;解决从零构建动态爱心视觉效果的实践难题。压缩包为1个39KB的Word文档&#xff08;.docx&#xff09;…

作者头像 李华