做医院挂号系统这事儿,看着简单,真做起来其实挺折腾人的。科室、医生、排班、号源、退号、就诊状态,业务逻辑一环套一环,前端界面要友好,后端并发和事务也得拿捏到位。我这两年一直在搞Java Web相关的全栈项目,SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0这套组合也打磨得比较熟了。这次分享一个我实际搭过、能跑通的医院挂号就诊系统,从技术选型思路到数据库设计,再到前后端联调和部署,把整个从零到一的过程摊开聊。这套系统的完整源码和文档我整理得挺清楚,适合正在做课设、毕业设计或者想入门全栈开发的Java同学拿去参考,也适合团队里想快速搭建医疗类业务原型的人抄作业。
1. 技术栈选型与整体设计思路
很多初学者一上来就纠结选什么框架、用什么版本,其实对于一个业务系统来说,技术选型的核心逻辑就一句话:稳住开发效率,同时兼顾后期维护。这套系统后端用了SpringBoot2,前端用了Vue3,持久层用MyBatis-Plus,数据库是MySQL8.0,这套搭配不是随便拼出来的。
1.1 前后端分离架构的核心优势
前后端分离已经被验证过很多年了,尤其对于医院挂号这种需要同时面对患者端和管理端的系统,天然适合拆开做。前端跑Vue3单页应用,负责页面渲染和交互,后端只做RESTful API,通过JSON交换数据。
我在这套系统里把前端拆成了两个入口:患者端和管理端。患者端关注挂号和就诊流程,界面不用复杂,信息清晰最重要;管理端涉及医生排班、科室维护、号源统计,组件密集、交互状态多。两个入口共用同一套后端API,只是登录角色不同,权限控制落在后端,前端做路由守卫配合按钮级校验。
前端用到Vue3最新语法和生态组件,包括Vite构建工具、Vue Router、Pinia状态管理。Vite在开发环境启动速度非常快,改完代码浏览器热更新基本是瞬时生效,比起老的Webpack配置省心太多。Pinia对比Vuex代码量少了很多,配合Composition API写起来非常顺手。
1.2 SpringBoot2+MyBatis-Plus解决什么问题
选SpringBoot2不是因为版本旧,而是因为它生态最成熟、资料最多,绝大多数的坑都已经有人踩过并公开了解决方案。SpringBoot2的自动配置机制把这套系统里大量的样板代码都省掉了,我只需要关注业务本身,DataSource、事务管理、Jackson序列化这些交给框架处理。
持久层选MyBatis-Plus是这套系统开发效率能拉满的关键。它就是MyBatis的增强工具,内置了通用的增删改查方法,单表操作完全不用手写SQL。它的条件构造器LambdaQueryWrapper写起来非常直观,代码可读性也高。比如查询某个医生某天的排班,直接链式调用就行。配合分页插件,写一个分页接口只需要几行代码。
通用CRUD服务这块,我基于MyBatis-Plus的IService接口封装了一个BaseService,把当前登录用户ID、创建时间、更新时间这些公共字段统一处理,每个业务Service只需要继承它。这样新增表的时候,Service层几乎没什么重复劳动,直接复用公共逻辑。
1.3 MySQL8.0为什么是数据库首选
MySQL8.0相比5.7版本,窗口函数、公共表表达式、默认字符集utf8mb4这些能力都很成熟了。医院挂号系统涉及到的排班统计、挂号趋势分析、号源消费明细这些查询,在MySQL8.0里写起来更顺手。
MySQL8.0对JSON类型的支持也更好。比如医生排班表里,可以用JSON字段存某个时间段已锁定的号位编号,查询和更新都灵活。用传统方案要拆关联表,用JSON字段则能大幅减少表数量,开发速度能提上来。
这套系统对数据库的依赖很重,事务处理是核心诉求。挂号操作涉及订单表和号源表两个核心表的更新,必须放在同一个事务里处理,任何一个环节失败都得全部回滚。MySQL8.0的InnoDB引擎在事务隔离级别和行锁机制上表现稳定,默认的REPEATABLE READ级别完全满足挂号类业务需求。
2. 系统功能模块设计与核心业务逻辑
医院挂号就诊系统的功能模块比很多人想象的要多。除了最基础的登录注册和科室医生浏览,还要管好排班、挂号、退号、叫号、就诊记录和后台运营数据。模块划分得清楚,开发才能不打架。
2.1 双端功能模块划分
患者端的功能我按就诊动线来拆:登录注册、首页科室导航、医生列表和详情、排班日历、在线挂号、订单管理、退号申请、就诊记录查询、个人信息维护。
管理端则是围绕运营和配置来拆:后台首页的统计看板,科室管理,医生账号管理,排班管理,号源查看与锁定,挂号订单处理,就诊状态更新(待就诊、已就诊、已过期),退号审批。
这里有个细节值得单独说:患者端和管理端的UI组件是分开的,但API层是共用的。前后端联调时,同一套接口,患者端走患者身份,管理端走管理员身份,后端通过登录用户的角色做数据权限隔离。比如“查询排班列表”这个接口,患者端只能查状态为“开放”的排班,管理端可以查全部排班,包括已关闭和已过号的。
2.2 排班和号源状态机设计
医院挂号系统最核心的业务状态其实是排班和号源,这个状态机设计好,后面所有功能都会顺。每个医生每天的排班记录,包含上午时段、下午时段,每个时段又由号源表来承载具体的号位数量。
我设计的排班状态有四种:草稿、开放、锁定、关闭。草稿是管理员配置好还没发布,开放是患者可以正常挂号,锁定表示号已满或者临时停诊,关闭是当天就诊结束或者取消。
号源状态则更细,每一位号都有独立状态:空闲、已锁定、已挂号、已就诊、已过号、已退号。这六个状态的流转关系是这样的:
- 空闲到已锁定:患者在提交挂号订单时预占号位,支付超时或者主动取消则回退到空闲。
- 已锁定到已挂号:支付成功或后台确认后订单正式生效。
- 已挂号到已就诊:医生端标记就诊完成。
- 已挂号到已退号:患者退号成功。
- 已挂号到已过号:到了就诊时段后用户没来,系统自动标记过期。
- 已退号的号位回退到空闲,管理员可以重新开放。
2.3 基于JWT的登录认证与角色权限
这套系统的登录认证我用的是JWT方案。用户登录成功后,后端签发一个带过期时间的Token,前端存在localStorage里,每次请求通过HTTP请求头带过去,后端通过Spring Security过滤器链校验。
JWT的有效期分为两个维度:Token本身的过期时间12小时,Redis里再做一层会话管理。如果用户在常用设备上操作频繁,刷新Token可以延续登录态;如果用户改密码或管理员强制下线,直接删Redis的会话键就行,Token本身失效。
权限控制上,我针对管理端接口和患者端接口分别做了注解校验。比如管理端的排班新增、号源释放这类接口,@PreAuthorize("hasRole('ADMIN')")会拦截非管理员请求,患者的个人信息接口则必须校验当前登录用户ID和资源归属ID一致才行。
3. 数据库设计与核心表结构拆解
数据库设计是整个系统的地基。表结构设计得合理,开发时少走弯路,上线后性能也不会有太多幺蛾子。我落地这套系统时,核心表一共8张,配合4张字典和关联表,完整覆盖挂号就诊业务链路。
3.1 核心业务表及其字段设计要点
用户表用的是一张通用设计,既承载患者也承载医生和超级管理员,通过角色字段区分。这张表的字段我都做得比较克制,用户名、密码、手机号、姓名、头像、角色、状态,没有过度设计。密码存储用的是BCrypt加密,不可逆,安全Job能抗住字典攻击。
科室表比较简单,科室名、科室编号、科室简介、排序、状态。医生表挂在科室表下面,一对一关联用户表,同时存职称和擅长领域。排班表是整个系统的核心枢纽,字段我重点说明几个:
- doctor_id:关联医生。
- schedule_date:排班日期。
- period_type:时段类型,上午还是下午。
- total_count:该时段总的号源数量。
- remain_count:剩余可挂数量。
- status:排班状态。
号源表更细,每位号位一行记录,包含号位序号、对应排班ID、就诊人ID、状态、挂号订单号。这样做的好处是号状态可以独立追踪,也能精准统计每个时段的号源消耗情况。
挂号订单表、退号申请表、就诊记录表则是业务流水侧的三张表。订单表记录了挂号行为本身,退号表记录了退号的前后状态和原因,就诊记录表承载医生的诊断结果、用药建议和病历描述。
3.2 索引设计、唯一约束与关键SQL
数据库表建好了,索引和约束必须跟上,否则数据量一大,查询性能会肉眼可见地变差。
排班表上,我建了联合索引(doctor_id, schedule_date, period_type),这是排班查询最核心的检索条件,加上唯一约束可以防止同一医生同一天同一时段重复配置排班。
号源表上的联合索引(schedule_id, status)用来快速统计某排班下各状态号位的数量,挂号时也要走这个索引去锁定空闲号位。
MySQL8.0的窗口函数我用在了后台统计看板里。比如统计最近7天每天挂号量趋势,一条SQL就能搞定:
SELECT DATE(create_time) AS create_day, COUNT(*) AS total_count, SUM(CASE WHEN order_status = 1 THEN 1 ELSE 0 END) AS paid_count FROM t_hospital_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time)
挂号订单金额统计、时段号源消耗率排名这类报表,也都能靠着窗口函数在SQL层面完成,完全没有必要在Java代码里二次聚合。
3.3 事务边界与并发控制实操
并发挂号的场景是这套系统的硬骨头。同一时刻可能有多个用户抢同一个医生同一个时段的号,如果不做并发控制,就会出现超卖问题,也就是发出的号位比实际多。
我用的方案是先乐观锁再事务。具体来说:
- 在排班表加上乐观锁版本号字段。
- 更新剩余号数时,WHERE条件里带上version = 当前版本号。
- 更新成功说明拿号成功,更新失败说明号被抢走了,提示用户号源紧张。
查询剩余号数和生成订单这两个操作放在同一个事务里,用@Transactional注解包裹。事务里先查号源状态判断是否空闲,再通过UPDATE语句做实际的号位状态流转,这个更新自带行锁,保证同一号位的并发操作是串行化的。
这套思路下来,即使100个用户同时抢同一个号位,最终也只有一个人能成功,剩下的99个人全部拿到乐观锁失败的提示。
4. 后端核心实现与前端联调实录
前后端都能跑通、数据能串起来,才算真正“做出来”。这一步最容易出幺蛾子,跨域、Token失效、字段格式不统一,哪个环节不顺都能卡你半天。我把核心实现的套路和一些实战中踩过的坑整理在下面。
4.1 SpringBoot2后端关键代码落地
后端项目结构我建议按照功能模块分包,而不是按技术层次分包。也就是说,把医生相关的Controller、Service、Mapper放在同一个包下,而不是所有Controller堆在一个包、所有Mapper堆在另一个包。功能分包在多人协作和后期维护时,找代码的成本低很多。
登录接口我推荐引入Sa-Token而不是直接裸写JWT,它的代码量小,文档也接地气,支持登录认证、权限认证、踢人下线、账号封禁这些Spring Security要写一大坨才能搞定的事。患者端和管理端的登录接口复用同一个认证逻辑,登录成功后在Sa-Token会话里打上不同的角色标识。
MyBatis-Plus的分页实现是配合PaginationInnerInterceptor来做的。一旦配置好,Controller里只需要返回Page类型,框架自动拦截SQL拼接分页参数。这里有个特别容易踩的坑:分页插件必须在MybatisPlusConfig配置类里显式注册,否则分页查询返回的total永远是0。
4.2 Vue3前端请求封装与动态路由实现
Vue3前端这边,我把Axios请求封装成了一个独立模块,统一处理BaseURL、请求拦截、响应拦截和错误码。请求拦截器里自动从Pinia Store取Token,没有Token就跳转登录页。响应拦截器里统一处理后端返回的code字段,非200状态码直接弹出错误提示,不用在每个页面重复写错误处理逻辑。
动态路由是这套系统前端的亮点。管理端的路由菜单不是死写在路由配置文件里的,而是登录后根据用户角色动态生成。后端登录接口返回PermsList,前端用addRoute方法动态追加对应用户的路由表,未授权的页面路径直接404,前端路由层面就能拦截一大半越权访问。
表格分页、表单弹窗、日历排班这些高频组件,我先抽成了公共组件,患者端和管理端共用。比如排班日历组件,传入医生ID和月份,组件内部请求排班接口,用不同颜色标识可挂号和已挂满的状态。
4.3 前后端联调经验与常见报错处理
联调阶段最常遇到的坑是跨域和JSON字段命名风格不一致。跨域问题后端一个@CrossOrigin或者全局CorsFilter就能解决,但要注意把允许的请求头和方法配置完整。字段命名问题上,我统一在后端JSON序列化配置里开启了驼峰转下划线,数据库user_name对应前端userName,前端不需要做任何额外处理。
另一个高频坑是Vue3组合式API下,页面组件加载后在onMounted钩子里请求接口,但此时Pinia可能还没有完成初始化。解决办法是请求动作放到async函数里,await store初始化完成再去请求。用起来效果很稳定。
报错方面,403权限不足多半是后端接口没匹配上@PreAuthorize规则,401是Token过期或缺失,排查时先用浏览器开发者工具看请求头里有没有Token,再检查后端拦截器有没有放行OPTIONS预检请求。这一步不处理好,前端跨域请求会直接卡在浏览器端。
5. 运行环境部署与文档使用指南
系统能跑起来,只是第一步。很多人在环境配置和部署环节消耗的时间甚至比写代码还多。我把这套SpringBoot2+Vue3的部署流程整理成了一套标准作业流程,照着做基本不会有大问题。
5.1 Docker一键启动MySQL8.0环境
对于本地开发,我非常推荐用Docker跑MySQL8.0,而不是在Windows或Mac上直接安装数据库服务。Docker方案有两个优势:环境隔离,随时删掉重建不污染宿主机系统;版本切换方便,今天用8.0明天想试5.7,换个镜像参数就行。
常用启动命令我在项目文档里写得很细。挂载宿主机目录解决数据持久化问题,指定MySQL8.0的数据目录权限,初始化SQL脚本通过docker-entrypoint-initdb.d目录自动执行。账号号密码、数据库名这些参数放在启动命令里统一管理,团队里其他人拉取仓库后一条命令就能把数据库起起来。
本地开发时,连接MySQL8.0最容易遇到的坑是认证插件问题。MySQL8.0默认的caching_sha2_password认证插件,某些老版本的驱动不支持,连不上数据库。解法是在启动命令里显式指定认证插件为mysql_native_password,如果你是直接用Docker镜像而不是自定义配置,也可以在Navicat或命令行里执行ALTER USER语句修改认证方式。
5.2 项目本地启动完整流程
这套系统的后端是基于Maven构建的,本地启动链路非常清晰。第一步确认JDK版本是8或11,Maven是3.6以上,然后拉取项目代码。第二步启动MySQL并用初始化脚本建库建表。第三步修改application.yml中的数据库连接信息。第四步运行主类启动后端,看到日志输出端口监听成功就说明后端起来了。
前端部分按照package.json里的scripts命令操作,先npm install安装依赖,安装失败时优先检查Node.js版本和镜像源配置。dev命令启动Vite开发服务器,Vite会默认监听5173端口。开发模式下前端代理配置指向后端8080端口,这样前端请求不会产生跨域问题。
需要注意一个细节:如果后端启动时端口被占用,优先检查是不是上次启动的后端进程没有退出。Linux下用lsof -i:8080查找进程PID之后结束掉,避免两个进程同时监听同一端口导致服务起不来。
5.3 项目文档里应该包含什么
这套系统的完整文档,我在交付时做了两个版本:一个给开发者看的技术文档,一个给使用方看的用户手册。
技术文档部分包含系统架构图、数据库ER图、接口文档、部署手册和二次开发指南。数据库ER图我直接用工具从建表SQL自动生成,标注核心字段说明,后端同学拿到就能快速理解表关系。接口文档用Swagger自动生成,在线就能调试,还配了一份Postman导出的接口集合。
用户手册则是给直接使用系统的人看的,包含系统功能清单、操作流程示例和运维FAQ。比如管理员第一次登录后台怎么改默认密码、患者怎么在线退号、医生排班满了怎么临时加号,这些高频操作都配了图文步骤。
6. 常见问题现场与排查思路实录
开发这套医院挂号系统的过程中,我翻了几个车的现场,这里把最典型的几类问题和排查逻辑记录下来,信息密度高,直接抄作业就行。
6.1 MySQL8.0时区与连接报错问题
系统在部署到云服务器上时,数据库连接一直报错Server returns invalid timezone. MySQL连接串里的serverTimezone参数配置错了。MySQL8.0默认的时区可能和本地环境不一致,在JDBC连接串后面加上serverTimezone=Asia/Shanghai就能解决。
还有一次是密码包含特殊字符导致连接串解析失败。密码里有@符号,直接拼在URL里被误认为是参数分隔符。解决办法是连接串里对特殊字符做URL编码,或者干脆把密码改成一个不含特殊字符的强密码。这种问题排查起来花时间,但原因其实特别简单。
6.2 MyBatis-Plus分页失效和逻辑删除冲突
分页插件配置过程中最容易漏掉的就是Interceptor的注册。很多入门教程只写了引入依赖,没提配置PaginationInnerInterceptor,结果分页查询出来的total始终是0,数据也只返回第一页的记录。
逻辑删除功能也遇到过坑。如果在一个表上同时配了逻辑删除和唯一约束,逻辑删除的记录仍然占用唯一索引,再次插入相同数据时会违反唯一约束。项目的号源表就遇到了这种情况,解决方法是建立复合唯一索引,把逻辑删除字段也纳入索引。
6.3 前后端联调跨域与会话失效
前端在浏览器调试时,模拟患者端和管理端同时登录,Token会把后者的Token覆盖,导致A端页面的请求全部401。原因是两个前端入口共用了同一个localStorage键名。解决方法是把Token按角色分键存储,或者后端校验时直接读取最新Token。项目文档里我推荐按角色分键存储,简单直接。
最后分享一个我在实际开发中养成的习惯:每次改完数据库表结构,第一时间更新ER图和接口文档。前后端联调最怕的就是“后端改了字段名前端不知道”,这种情况处理一次就能学会教训。这套系统的源码和文档我已经打包整理好了,拿到手以后建议先跑通初始化脚本,再从前端登录页开始走一遍完整流程,大概半天时间就能对整个系统的数据流转有清晰的认知。后面如果你们要在这个基础上做二期开发,比如接入在线支付、增加消息推送或者对接公众号预约,核心模块的扩展点我都已经留好了。