在准备Spring Boot毕业设计的时候,很多人会被“医疗挂号管理系统”这类项目劝退,觉得又是增删改查,没什么技术含量。但实际上,把挂号系统的核心逻辑吃透——比如号源怎么扣减、排班怎么设计、医生和科室怎么关联、患者怎么在高峰期不把系统打崩——这里面能挖的东西远比想象中多。这个基于Spring Boot的医疗挂号管理系统(项目编号11680)就是一个典型的全栈实战项目,前端走Vue、后端走Spring Boot,覆盖了从患者注册登录、科室医生查询、在线预约挂号,到后台管理员维护科室、医生排班、号源管理的完整闭环。
如果你正在找毕设选题,或者想用一个真实业务场景来检验自己Spring Boot的学习成果,这篇内容会很有参考价值。我会把这套系统的设计思路、数据库建模、后端核心接口、前端联调细节、常见坑点全部拆开讲,按“你能直接照着抄”的标准来写,也顺便聊聊那些文档里不会写、但实际开发中一定会遇到的糟心事。
1. 项目整体设计与技术选型思路
1.1 业务需求拆解与角色划分
医疗挂号系统听起来很简单,无非就是患者选科室、选医生、选时间、提交挂号。但真正落到业务上,它至少要拆出三类用户角色,每类角色看到的界面和能做的操作完全不一样。
患者端是系统的流量入口。患者需要先注册账号,登录后浏览科室列表,查看某个科室下的医生排班信息,选择有空余号源的时间段进行预约挂号。挂号成功后,患者要能查到自己的挂号记录,必要时还要能取消挂号,把号源释放出来。
医生端相对简单一些,医生登录后可以查看自己当天的排班信息和已经挂到自己名下的患者列表,有些版本还会扩展出门诊病历填写、患者历史挂号记录查询等模块。但核心还是围绕“排班—接诊”这个主线。
管理员端是整个系统的控制中心。管理员负责维护科室信息,比如内科、外科、儿科这些基础数据;负责管理医生账号,包括录入医生基本信息、所属科室、职称等;最重要的是排班管理——管理员要为每个医生设置一周的出诊时间段,并设定每个时间段的号源总数。没有排班,患者端就是空的。
这三个角色对应的权限边界必须在设计初期就划清楚,否则后期接口权限校验会改到怀疑人生。我的建议是直接用三张用户表或者一张用户表加角色字段,配合拦截器做菜单级和接口级的双重鉴权。
1.2 为什么选Spring Boot作为技术底座
这个项目选择Spring Boot,不光是因为它流行、好写代码,而是因为它确实贴合这类管理信息系统的开发节奏。医院挂号系统的特点是业务规则清晰、并发量有一定要求但不算极端、需要快速迭代交付——Spring Boot的自动装配机制、Starter生态和内置Tomcat,恰好让开发者把精力集中在业务逻辑而非繁琐的配置上。
很多人在选版本的时候会纠结,热词里也经常能看到“springboot版本太高”这种抱怨。我给的建议非常直接:不要盲目追新。如果你用的是JDK 8,就老老实实选Spring Boot 2.7.x系列;如果JDK 17及以上,可以考虑Spring Boot 3.x。很多报错在网上搜不到答案,不是因为你代码写错了,而是版本组合太新,遇到问题连社区都还没来得及填坑。
持久层框架我推荐MyBatis-Plus而不是纯MyBatis。挂号系统里大量操作是单表查询和简单的条件查询,比如按科室查医生、按医生查排班、按患者查挂号记录,MyBatis-Plus的BaseMapper和LambdaQueryWrapper能把这些琐碎的CRUD压缩到极短,而且代码可读性比XML里堆SQL好太多。真正复杂的SQL在这个项目里其实没几条,完全可以用注解或XML单独处理。
1.3 前端技术与前后端交互方式
前端选择Vue,这是目前毕设项目和中小型管理系统的主流搭配。Vue生态成熟、上手成本低,配合Element UI或者Element Plus能快速搭建出像模像样的管理后台。如果你对前端不太自信,直接用Vue CLI创建一个标准工程,页面就围绕登录、挂号台、后台管理三个部分展开。
前后端交互推荐走RESTful API,统一返回JSON格式。这里强烈建议定义一个统一的响应体类,比如Result,包含code、msg、data三个字段。这样前端拦截器可以统一处理状态码,比如登录失效时返回401,前端跳转登录页;业务异常时返回500加提示信息,前端直接弹MessageBox。不要每个接口返回不同的结构,否则前端联调时会疯掉。
还有一点是关于前后端分离部署和打包的问题。很多同学做毕设答辩时不想单独启动两个服务,希望一个Java进程搞定一切。这个完全可以做到,让Vue执行npm run build之后,把dist目录里的静态资源复制到Spring Boot的src/main/resources/static下,同时把前端的请求后端地址改成相对路径。这样打包出来的Jar包就自带页面,双击就能跑,演示非常省事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构规划
2.1 核心业务表的拆分逻辑
医疗挂号系统的数据模型是整个项目的灵魂。表结构设计得合理,后面写代码就是一路顺畅;设计得有缺陷,比如把排班和挂号混在一张表里,后面改需求时你会想骂人。
我建议至少拆出这几张核心表:
用户表负责存储登录账号和角色标识,包含用户ID、用户名、密码加密后的密文、手机号、真实姓名、角色类型。角色可以简单用整数区分,1代表管理员、2代表医生、3代表患者,这样登录后根据角色跳转到不同的首页。
科室表负责维护科室基础信息,包含科室ID、科室名称、科室位置、科室简介。科室是医生和排班的顶层归属,没了科室,医生不知道挂在哪,患者也不知道去哪看病。
医生表负责存储医生的执业信息,包含医生ID、所属科室ID、医生姓名、职称、擅长领域、简介、头像地址。医生表通过科室ID关联科室表,形成两级联动的查询链路:患者选科室,然后看该科室下的医生列表。
排班表是整个系统的核心。它负责记录某个医生在某个时间段内有几个号可以挂,包含排班ID、医生ID、排班日期、出诊时间段、号源总数、剩余号数。这里一定要把“号源总数”和“剩余号数”拆成两个字段,不要直接算,因为后续做取消挂号释放号源时,剩余号数要能单独回滚。
挂号记录表负责记录患者每一次成功的预约操作,包含挂号ID、患者ID、排班ID、挂号时间、就诊状态。就诊状态可以细分:0代表已预约未就诊,1代表已完成就诊,2代表已取消。这个状态字段在后台统计、医生接诊、患者取消挂号的场景下都要用到,非常关键。
2.2 核心表的建表SQL参考
直接给一份可以落地执行的MySQL建表语句,字段命名采用下划线风格,和后端实体类的驼峰命名通过MyBatis-Plus的map-underscore-to-camel-case自动映射,省去大量手写映射的麻烦。
sql复制CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`role` tinyint NOT NULL COMMENT '角色:1-管理员 2-医生 3-患者',
`status` tinyint DEFAULT '1' COMMENT '状态:1-正常 0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
CREATE TABLE `department` (
`id` bigint NOT NULL AUTO_INCREMENT,
`dept_name` varchar(50) NOT NULL COMMENT '科室名称',
`dept_location` varchar(100) DEFAULT NULL COMMENT '科室位置',
`dept_intro` text COMMENT '科室简介',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科室表';
CREATE TABLE `doctor` (
`id` bigint NOT NULL AUTO_INCREMENT,
`dept_id` bigint NOT NULL COMMENT '所属科室ID',
`user_id` bigint NOT NULL COMMENT '关联用户表ID',
`doctor_name` varchar(50) NOT NULL COMMENT '医生姓名',
`title` varchar(50) DEFAULT NULL COMMENT '职称',
`expertise` varchar(255) DEFAULT NULL COMMENT '擅长领域',
`intro` text COMMENT '简介',
PRIMARY KEY (`id`),
KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生表';
CREATE TABLE `schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`doctor_id` bigint NOT NULL COMMENT '医生ID',
`schedule_date` date NOT NULL COMMENT '排班日期',
`time_slot` varchar(50) NOT NULL COMMENT '时间段,如08:00-09:00',
`total_count` int NOT NULL COMMENT '号源总数',
`remain_count` int NOT NULL COMMENT '剩余号数',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_doctor_date_slot` (`doctor_id`, `schedule_date`, `time_slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生排班表';
CREATE TABLE `registration` (
`id` bigint NOT NULL AUTO_INCREMENT,
`patient_id` bigint NOT NULL COMMENT '患者用户ID',
`schedule_id` bigint NOT NULL COMMENT '排班ID',
`doctor_id` bigint NOT NULL COMMENT '医生ID',
`reg_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '挂号时间',
`visit_status` tinyint DEFAULT '0' COMMENT '就诊状态:0-待就诊 1-已完成 2-已取消',
PRIMARY KEY (`id`),
KEY `idx_patient_id` (`patient_id`),
KEY `idx_schedule_id` (`schedule_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='挂号记录表';
一个容易忽略的细节是schedule表的时间段字段。不要试图用两个datetime字段存开始和结束时间,虽然这样也能做时间范围判断,但业务上挂号系统展示的就是“09:00-09:30”这种固定字符串片段,患者选择时也是整段选择。用一个字符串字段存时间段,管理端排班时也按固定片段录入,查询排序时再按开始时间排序,逻辑最清晰。
3. 后端核心功能实现与接口设计
3.1 登录鉴权与JWT Token机制
登录这块我直接用Spring Boot整合JWT来做无状态鉴权。用户的密码用BCrypt加密存储,登录时校验密码,通过后生成一个包含用户ID、角色、过期时间的Token返回给前端。前端把Token存在localStorage里,每次请求在拦截器里加上Authorization头,后端用一个拦截器统一解析Token并放行或拦截。
需要注意的是密码加密,千万不能明文存储。这个项目虽然只是毕设,但一个真实可用的系统永远不该有明文密码。Spring Security里自带的BCryptPasswordEncoder可以直接用,单独引入spring-security-crypto这个轻量依赖就行,不必把整个Spring Security的过滤器链都引进来,那样配置复杂,反而干扰项目主线。
拦截器里做角色鉴权时,建议用自定义注解配HandlerInterceptor。比如在管理端的Controller方法上加一个@RequireRole("ADMIN"),拦截器里判断当前登录用户的角色是否匹配。这样比在每个接口方法里手写if判断要优雅得多,代码也集中。
3.2 预约挂号的核心业务逻辑
预约挂号是整个项目里最值得细写的业务逻辑。它的核心难点在于并发控制——两个患者同时看到同一个医生同一个时间段还剩最后一个号,同时点了提交挂号,系统必须保证只有一个人能成功。
一种朴素的做法是:先查询剩余号数,判断大于0,然后执行插入挂号记录,再执行剩余号数减一。但这个流程在并发下是错的,三步之间存在时间窗口,两个请求都能查到剩余1个号,然后都执行成功,号源就超卖了。
解决超卖有两种常用方案。一种是数据库层面加锁,用select ... for update把排班记录行锁住,然后执行判断和更新;另一种是乐观锁,在schedule表加一个version字段,update时带上where version = ?,若影响行数为0说明数据已被别人修改过,再返回错误提示“号源已被抢完”。
我实际写这类系统时更倾向于乐观锁,因为行锁会让高并发请求排队,虽然数据安全,但用户体验会变差。乐观锁的SQL可以写成这样:
sql复制UPDATE schedule SET remain_count = remain_count - 1, version = version + 1
WHERE id = #{scheduleId} AND remain_count > 0 AND version = #{oldVersion}
如果update返回的影响行数是1,说明扣减成功,接下来插入挂号记录;如果返回0,说明号源已经被别人扣掉,这次挂号直接失败。整个过程建议放到一个事务里,用@Transactional保证要么都成功,要么都回滚。记得先扣号源再插入挂号记录,顺序不要反。
取消挂号相对简单一些,把挂号记录的visit_status改成已取消,同时把对应排班的remain_count加回1。这里也要小心并发,理论上加回1不会有超卖风险,但搭配乐观锁处理更稳妥。
3.3 接口清单与参数约定
前后端联调时接口一定要清爽,我列一份核心接口清单,路径和参数是实践中常用的RESTful风格:
| 功能 | 请求方式 | 路径 | 参数说明 |
|---|---|---|---|
| 患者注册 | POST | /api/user/register | username、password、realName、phone |
| 用户登录 | POST | /api/user/login | username、password |
| 科室列表 | GET | /api/dept/list | 无 |
| 科室下医生列表 | GET | /api/doctor/list/ | 路径传科室ID |
| 医生排班列表 | GET | /api/schedule/list/ | 路径传医生ID |
| 提交挂号 | POST | /api/registration/submit | scheduleId |
| 我的挂号记录 | GET | /api/registration/myList | 请求头带Token |
| 取消挂号 | PUT | /api/registration/cancel/ | 路径传挂号记录ID |
| 管理员新增排班 | POST | /api/schedule/add | doctorId、scheduleDate、timeSlot、totalCount |
| 管理员排班列表 | GET | /api/schedule/adminList | 可分页查询 |
分页查询建议直接用MyBatis-Plus的Page对象,前端传current和size两个参数,接口统一返回IPage结构,前端就能直接渲染表格。不要自己手写LIMIT加COUNT的套路,除非你想让代码看起来像十年前的写法。
3.4 管理端排班与数据看板
管理端的排班功能在答辩环节很加分。管理员选择一个医生,选择一个日期,设定时间段和号源总数,提交后生成排班记录。实际开发中这里可以做得更细致一些:校验选中的日期是否是未来日期,校验同一天同一个医生是否已经有该时间段的排班记录(利用schedule表的唯一索引兜底),校验号源总数不能为0。
除了排班,管理端最好加一个简单的数据看板——比如今日挂号总数、各科室挂号分布、近7天挂号趋势。这类的实现并不需要引入复杂的大数据组件,用几条GROUP BY的SQL就能完成。这不是为了炫技,而是让项目在视觉上更有“系统感”,答辩时老师看到图表,会默认你的项目有较高的完成度。
我见过不少同学的毕设,功能做完了,但登录进去就几个干巴巴的表格,没有一点业务氛围。加上看板后,整个系统的完成度瞬间提升一个档次。
4. 前端页面搭建与Vue打包部署细节
4.1 页面结构划分与路由配置
前端用Vue 2 + Element UI(如果用Spring Boot 3 + Vue 3,则换成Element Plus),页面划分基本可以照抄这个思路:
患者端的页面包括注册页、登录页、首页科室列表、医生列表页、排班选择页、挂号记录页。排班选择页是核心交互页面,展示某个医生在某一天的排班列表,每个排班项显示时间段、剩余号数和挂号按钮,号数为0时按钮置灰并显示“约满”。
管理端的页面包括登录页、Dashboard看板、科室管理页、医生管理页、排班管理页、患者挂号记录查询页。路由配置用Vue Router,在路由的meta字段里标记需要的角色,在全局前置守卫里根据本地存储的角色信息做跳转拦截。
4.2 前端请求封装与Token携带
Vue项目的axios封装是必须做的一件事。我习惯创建一个request.js,实例化axios时设置baseURL和超时时间,在请求拦截器里从localStorage取Token并塞到请求头,在响应拦截器里统一处理后端返回的code状态。
响应拦截器里有一条非常重要的经验:如果后端返回code为401,比如Token过期或未登录,前端不能只弹个错误提示就完事,必须清空本地用户信息和Token,然后用router.push跳转回登录页。很多同学在这里只做了提示没做跳转,用户留在当前页面,后续所有请求都报401,体验很割裂。
后端的Controller层建议把跨域问题也一并处理掉。虽然有Nginx反向代理可以解决跨域,但作为毕设项目,直接在WebMvcConfigurer里重写addCorsMappings方法是最省事的方案。用allowedOriginPatterns("")配合allowedMethods(""),本地开发时前端用localhost:8080端口访问后端8080端口就不会再拦了。
4.3 打包集成与Jar包运行
前端开发调试时用npm run serve,联调没问题后npm run build。构建产物默认生成在dist目录,你要做的就是把这个目录下的index.html和static文件夹拷贝到Spring Boot的src/main/resources/static里。
这里有个细节值得注意:Vue路由如果启用了history模式,直接部署到Spring Boot的静态目录时,刷新页面会出现404。因为Spring Boot的静态资源映射没有对应前端路由,比如访问/api页面会先查找static目录下的api文件,找不到就404了。最简单的方式是改成hash模式,路径上多个#号,刷新不会404,虽然不美观但教学项目完全够用。
然后直接mvn clean package打成Jar包,java -jar启动。由于页面和接口都在同一个进程里,部署和答辩演示都很方便。如果想重新开发前端,把static里的旧文件删掉再拷贝新的即可——我一开始不知道这个细节,每次还得重新打包后端,浪费了不少时间。
5. 常用配置与典型问题排查记录
5.1 配置文件里的关键参数设置
Spring Boot的application.yml里有几项设置对这类管理项目非常关键,直接贴出来:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
数据库连接串里serverTimezone必须设置,否则新版MySQL驱动会报时区错误。mybatis-plus的log-impl建议开启,开发时能在控制台看到完整SQL,排查问题效率提升非常明显。
JWT的密钥和过期时间放在配置里,比如yaml里加jwt.secret和jwt.expire两个自定义项,用@Value注解注入到工具类里。不要在代码里写死,万一答辩时老师要求你改动有效期,改配置永远比改代码安全。
5.2 高频报错与解决思路
Spring Boot版本和JDK版本搭配问题绝对是出现频率最高的坑。很多人一上来就下载了最新版的Spring Boot和JDK 21,结果引入旧版依赖时各种NoSuchMethodError和ClassNotFoundException。我先说结论:JDK 8配Spring Boot 2.3-2.7,JDK 17配Spring Boot 3.0-3.2,这是目前社区里踩平了的安全组合。
IDEA里配置启动端口也是个常被问到的问题。你只需要在resources目录下的application.yml里改server.port即可,还不行的话检查是否在VM options里手动加了-Dserver.port参数,那个参数优先级高于配置文件,会出现“我改了配置怎么没生效”的诡异局面。
用IntelliJ IDEA导入Maven项目时,如果右侧没有出现Maven工具窗口,检查一下pom.xml是否被IDEA识别为Maven项目。你可以右键pom.xml,选择Add as Maven Project。这个操作基本能解决90%的依赖树不加载和启动类找不到的问题。
5.3 定时任务清理过期数据
挂号系统里有一个真实业务场景特别适合用Spring Boot的定时任务处理:把患者超过一段时间未就诊且未取消的挂号记录自动标记为过期,同时释放号源。虽然毕设里不要求把这个做得非常完善,但写上这个功能会显得业务思考更周全。
在启动类上加@EnableScheduling,然后在专门的定时任务类里用@Scheduled(cron = "0 0 2 * * ?")标注一个方法,每天凌晨两点执行一次扫描。方法逻辑也很简单:查询visit_status为0且reg_date在昨天之前的挂号记录,把每条记录的状态改成已取消,同时把对应的排班记录剩余号数加回。
定时任务和普通Service调用事务容易踩一个坑——非事务方法调用同类中的事务方法时事务不生效。写定时任务时最好直接把业务逻辑放在定时任务方法内部,或者注入另一个Service类来调用事务方法,避免this调用导致的事务代理失效。
6. 项目答辩亮点与功能扩展建议
6.1 可以从哪几个角度展示你的项目
答辩时如果只是演示登录、点几下菜单,老师会很快失去兴趣。但换一个思路引导,效果完全不同。
第一个值得展示的亮点是数据库设计。重点讲schedule表为什么要单独设计,为什么不把排班信息直接放挂号记录里。你从一对一、一对多的关系角度解释清楚,老师会认为你真正理解了业务。
第二个亮点是并发控制。很多毕设项目根本没有这个意识,你能说出“用乐观锁解决号源超卖”这个方案,并且现场用两个浏览器同时挂号演示各自的结果,这已经是超出毕设平均水平的完成度了。
第三个亮点是JWT鉴权和角色权限。你可以现场展示患者Token访问管理端接口被拦截器拒绝,然后切到管理员账号正常访问。这个演示直观、效果强,而且实现成本不高。
6.2 扩展方向:往真实生产系统靠拢
如果做完基础功能还有余力,我建议在这些方向上做一点扩展:
日志记录方面,可以用Spring Boot自带的拦截器记录每个请求的路径、耗时和操作人。不需要引入AOP做太复杂的切面,简单过滤器就能让项目具备基础审计能力。
缓存方面,把科室列表和医生列表这类高频读取且变化频率低的数据放进Redis缓存,设置5分钟过期。引入Redis后技术权重会明显提升,而且Spring Boot整合Redis只需要加一个依赖和配置连接信息。不过得注意,如果电脑没装Redis,又不想花时间在环境配置上,那就先跳过,不要让它变成部署时的负担。
接口文档方面,引入Knife4j或者Spring Doc,启动后访问本地的doc地址就能看到每个接口的说明和测试页面。这个对答辩帮助很大,老师不用去看代码,直接看接口文档就能了解系统能力。
我在实际开发这套系统时,最深的感受是:不要被“又是增删改查”的刻板印象束缚住。同样的技术栈,业务逻辑设计得扎实——号源并发扣减、角色权限边界、排班与挂号的联动——呈现出来的系统质感是完全不同的。很多人毕设挂在“能跑但很虚”,而掩盖虚的最好办法就是把每个核心功能背后的业务约束想清楚、写明白。这套挂号系统你照着敲一遍,再往管理端加一个数据看板,答辩时的底气和效果都会好很多。
