简介:一套基于SpringBoot与Vue开发的医院挂号就诊管理系统毕设项目资源,面向需要快速完成毕业设计的Java方向学生,覆盖挂号管理、在线咨询、健康教育与公告管理等典型业务模块。系统采用前后端分离架构,后端基于SpringBoot+Mybatis+Maven,前端使用Vue与ElementUI,配合MySQL数据库,整体技术栈贴近企业实战,也便于进一步扩展功能。压缩包共772个文件,含Java源码、Vue组件、CSS样式、SQL脚本以及install/run/build批处理脚本,资源包约35.77MB,导入IDEA或Eclipse即可直接部署调试。系统内置管理员、医生、用户三种角色,可完整演练登录授权、挂号流程、健康资讯维护、轮播图配置等核心功能,同时附带毕业论文文档与数据库初始化脚本,帮助理解从建表到前后端联调的完整实现思路。目前已有58人学习,包内另含SVG图标、图片及演示音视频等辅助资源,适合作为毕业设计参考、项目二次开发或岗位实训的基础。
1. SpringBoot+Vue医院挂号系统:毕设源码包里到底有什么
每年毕设季,"医院挂号就诊管理系统"都是Java方向的高频选题。我看了不少同学发来的求助帖,问题多数不是选题太难,而是拿到手的项目要么结构混乱、要么缺数据库脚本、要么前后端接口对不上,改两天还是跑不起来。这套基于SpringBoot+Vue开发的医院挂号就诊管理系统,正好把这条链路的坑先替你趟平了。它是一套完整的全栈项目,前端Vue负责交互页面,后端SpringBoot提供接口,MySQL负责数据落库,覆盖了科室管理、医生排班、预约挂号、就诊记录这些核心业务闭环,还自带了数据库初始脚本和毕业论文文档。它解决的痛点很直接:让你从拿到源码到能演示、能答辩,少走弯路。适合三类人看:正在选毕设题目的学生、想练手全栈项目的新手、以及需要一套可二次开发基座的在职工程师。
2. 架构与技术选型:为什么这套组合最适合当毕设底座
2.1 技术栈构成与选型理由
这套系统的技术选型是典型的"毕设稳妥组合":后端SpringBoot + MyBatis-Plus,前端Vue + Element UI + Axios,数据库MySQL。SpringBoot用内嵌Tomcat,不需要单独装服务器,双击main方法就能起服务,这对学生党来说非常关键,因为我见过太多人在配置独立Tomcat的步骤上翻车。MyBatis-Plus则是对新手最友好的ORM框架,单表CRUD不用写XML,那些复杂的多表查询也可以通过条件构造器搞定。
Vue全家桶选择Element UI作为UI组件库,不是因为它最新,而是因为文档齐全、组件覆盖度高,表格、表单、日期选择器这些都是现成的。管理员后台的排班管理页面,用el-table搭配el-dialog就能实现几乎全部功能。Axios负责前后端的数据通信,封装好请求拦截和响应拦截之后,接口调用代码会非常清爽。MySQL作为数据库,一是课程里学的就是它,答辩时老师问起来有话说,二是初始化脚本用Navicat导入就能用,不用配复杂的环境。
2.2 前后端分离结构解析
系统采用前后端分离架构,后端跑在8080端口,前端开发服务器跑在8081端口,两者通过HTTP接口通信。目录结构上,后端按controller、service、mapper、entity四层组织,前端按views、components、api、router四块划分。前端的api目录专门放所有接口请求函数,和views页面组件一一对应,这样改接口时不用满项目找代码。
工程结构清晰带来的直接好处是:答辩时老师问"你项目怎么分层的",你可以直接指着目录讲架构。
后端的业务逻辑分层是Controller接收参数并做简单校验,Service处理核心业务,Mapper负责数据库操作。比如挂号这个动作,Controller只接收用户ID和排班ID,Service层去校验号源是否充足、扣减号源数量、生成订单,事务控制在Service层完成。这种分层方式也方便后续扩展功能,比如增加微信支付,只需要在Service层加一段逻辑,不用动Controller。
2.3 数据库表设计与核心字段
我一般拿到一个毕设项目,第一件事就是看数据库脚本,因为表结构决定了业务的边界。这套系统的核心表设计得比较规整,用户表、科室表、医生表、排班表、挂号订单表、就诊记录表,每张表的字段都贴合实际业务场景。用户表区分角色,用role字段标记管理员、医生、患者三种身份,这种设计比单独建三张用户表简单得多,权限控制也容易实现。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| sys_user | id, username, password, role, phone | 三种角色共用的用户表 |
| department | id, dept_name, dept_desc | 科室基本信息 |
| doctor | id, user_id, dept_id, title, intro | 医生信息,user_id关联sys_user |
| schedule | id, doctor_id, work_date, time_slot, total, remaining | 排班表,remaining表示剩余号源 |
| appointment_order | id, order_no, user_id, schedule_id, status, create_time | 挂号订单表 |
| medical_record | id, appointment_id, diagnosis, prescription | 就诊记录,关联挂号单 |
排班表有个字段值得特别留意:schedule_time采用time_slot标记上午、下午、晚间三个时段。这里没有用复杂的时间数据结构,而是用枚举值区分,查询时直接按字符串匹配,简单又不容易出错。剩余号源字段remaining是挂号并发控制的关键,后面第三章会详细讲。
数据库脚本里还内置了一些基础数据,包括几个科室、医生账号、管理员账号,这让项目启动后不用自己手工造数据就能直接演示。医生登录密码和账号都在SQL文件中,整个项目跑起来后,医生角色登录进去就能看到自己的排班列表和待接诊患者。
3. 核心业务链路:挂号全流程的代码级拆解
3.1 登录鉴权:JWT如何贯穿整个请求链
系统采用JWT做身份认证。对比Session方案,JWT无状态、跨域友好,前端存Token,后端不用维护Session,这对前后端分离的项目很合适。登录接口逻辑并不复杂:前端传username和password,后端从sys_user表查出用户,用MD5加密比对密码,通过后生成携带userId和role信息的Token返回。
生成Token的代码在TokenUtil工具类里,核心逻辑是设置过期时间和密钥:
public String createToken(Long userId, String role) { String token = Jwts.builder() .setSubject(userId.toString()) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); return token; }这段代码的逻辑是:把用户ID写入主体,角色写入自定义声明,过期时间设为24小时,用HS512算法和配置的密钥签名,返回字符串Token。参数里关键在于secretKey,它必须全局唯一且不能泄露,我见过有人把密钥硬编码在代码里,这问题不大但答辩时最好改成配置项。过期时间设为两天是合理的,太短导致用户频繁重新登录,太长又存在安全隐患。
后端的拦截器会在每次请求前从Header中取出Token并校验,代码如下:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token.substring(7)).getBody(); request.setAttribute("userId", claims.getSubject()); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } }拦截器逻辑总结成一句话:没带Token直接拒绝,Token解析失败也拒绝,解析成功则把用户信息放到请求属性里供Controller使用。前端需要配合在Axios请求拦截器里加上Bearer前缀,这部分到第四章讲。这里有个需要注意的点:被放行的路径要单独配置,登录接口、注册接口、静态资源这些不需要认证的路径必须排除在外,否则会出现"用户名密码都对但提示未认证"的奇怪问题。
3.2 科室与医生查询:层级联动查询的设计
挂号的第一步是选科室,选完科室才能看到医生列表。这两个接口是一对联动查询。后端Controller接收科室ID,查出该科室下的所有医生返回前端,前端用el-select的change事件触发医生列表更新,这就是二级联动。
科室列表接口很简单,一个Mapper方法搞定。关键是医生查询接口需要关联两张表:查询doctor表的同时关联sys_user获取医生姓名,关联department获取科室名称。用MyBatis-Plus的条件构造器可以少写很多SQL:
public List<DoctorVO> getDoctorsByDept(Long deptId) { LambdaQueryWrapper<Doctor> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Doctor::getDeptId, deptId); List<Doctor> doctors = doctorMapper.selectList(wrapper); // 批量查出关联用户信息,组装VO返回 List<Long> userIds = doctors.stream().map(Doctor::getUserId).collect(Collectors.toList()); List<SysUser> users = userMapper.selectBatchIds(userIds); // 组装DoctorVO,包含医生姓名、职称、简介等信息 return doctors.stream().map(d -> { DoctorVO vo = new DoctorVO(); BeanUtils.copyProperties(d, vo); users.stream().filter(u -> u.getId().equals(d.getUserId())) .findFirst().ifPresent(u -> vo.setDoctorName(u.getRealName())); return vo; }).collect(Collectors.toList()); }这段代码展示了MyBatis-Plus最常用的三板斧:LambdaQueryWrapper构造查询条件、selectBatchIds批量查询、BeanUtils属性拷贝。需要注意的地方在于:Entity里不会存医生姓名,姓名在sys_user表里,所以需要额外查一次用户表做数据组装。如果你的项目把医生姓名直接冗余在doctor表里,就能省掉这次关联查询,但换来的是数据冗余和更新时的同步问题。
3.3 号源扣减与订单生成:并发场景下的事务处理
这是整个系统最核心的代码,也是答辩时老师大概率追问的地方。挂号动作本质上是两步操作:检查排班剩余号源、扣减号源数量,然后生成订单。如果这两步之间没有并发控制,会出现超卖问题——两个用户同时挂号,都查到剩余号源为1,都扣减成功,就卖出了两张票。
系统里用数据库行锁解决这个问题,核心是selectForUpdate,代码逻辑如下:
@Transactional public AppointmentOrder createOrder(Long userId, Long scheduleId) { Schedule schedule = scheduleMapper.selectForUpdate(scheduleId); // 锁定排班行 if (schedule.getRemaining() <= 0) { throw new BusinessException("号源已被约满"); } schedule.setRemaining(schedule.getRemaining() - 1); scheduleMapper.updateById(schedule); AppointmentOrder order = new AppointmentOrder(); order.setOrderNo(generateOrderNo()); // 用时间戳+随机数生成单号 order.setUserId(userId); order.setScheduleId(scheduleId); order.setStatus("已预约"); appointmentOrderMapper.insert(order); return order; }关键点有两个。第一是这个方法加了@Transactional注解,整个流程要么全部成功要么全部回滚,防止扣了号源但订单没生成的脏数据。第二是selectForUpdate对排班行加了悲观锁,其他事务在操作同一行时会阻塞等待,从数据库层面杜绝了超卖。
selectForUpdate在MyBatis-Plus中的实现是自定义SQL:
<select id="selectForUpdate" resultType="com.example.entity.Schedule"> select * from schedule where id = #{id} for update </select>注意selectForUpdate必须配合事务使用,所以Service层方法上的@Transactional不能漏。另外这里有个小的性能考量:悲观锁在并发量极大的场景下会影响吞吐,但对毕设项目或中小型医院系统来说完全够用。如果想向老师展示更优方案,可以提到乐观锁思路:在schedule表加version字段,更新时where条件带上version,但需要处理更新失败后的重试逻辑。
生成订单号的方式也值得留意。我用的是时间戳拼接随机数的方案,不依赖数据库自增,这样订单号不会泄露每天的业务量,也方便拼接业务前缀。真正上线时可以改成雪花算法,但毕设演示这个程度完全足够。
就诊记录的设计简单一点:医生从"待接诊"列表里选中一个挂号单,点击开始就诊后弹出病历填写框,填写诊断结果和处方,提交后更新挂号单状态为"已完成",同时插入一条medical_record记录。这个流程不涉及并发,普通insert和update即可,但要注意挂号单状态枚举的统一管理,不要散落魔法值在代码里。
4. 前端Vue工程:从路由到接口的联调细节
4.1 前端工程结构与路由配置
Vue前端的目录组织以views为页面层,每个页面一个.vue文件,components放复用组件,api放axios请求函数,router集中管理路由。这种结构和后端四层对应,新手拿到代码后可以快速定位:想改挂号页面就去views的Appointment.vue,想看接口定义就去api目录下的appointment.js。
路由配置里最需要注意的是导航守卫。因为系统要区分管理员、医生、患者三种角色,所以路由不能只做登录拦截,还要做角色权限控制:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!token && to.path !== '/login') { next('/login'); return; } if (token && to.path === '/login') { next('/'); return; } const role = localStorage.getItem('role'); if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403'); return; } next(); });导航守卫的逻辑分四段:没登录跳转到登录页、已登录访问登录页自动跳首页、路由配置了role限制但用户角色不匹配则跳403页面、其余情况放行。路由表里每个页面配置meta.roles数组,比如排班管理页的meta.roles是['admin'],挂号页是['patient'],这样权限配置一目了然。
4.2 Axios统一封装与请求拦截
前端所有接口请求都走同一个Axios实例,统一在request.js里配置baseURL、超时时间、请求拦截和响应拦截。这样做的直接好处是:后端接口返回401时,前端能统一跳到登录页而不是每个页面都写一遍错误处理。
const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { this.$message.error(res.msg || '请求失败'); return Promise.reject(new Error(res.msg)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.clear(); router.push('/login'); } return Promise.reject(error); } );请求拦截器在每次请求前自动从localStorage取出Token拼上Bearer前缀,解决所有请求都要手动带Token的重复劳动。响应拦截器统一检查返回体里的code字段,非200直接弹错误消息并拒绝请求,401则清理本地存储并跳登录页。后端接口统一返回格式是{code, msg, data},这个格式必须在后端Controller里约定好,我看到很多项目前段代码写好了,后端却部分接口返回裸数据,联调时莫名报错。
4.3 挂号页面的组件实现
挂号页面是患者角色的核心操作页,交互流程是:选科室下拉框、选医生下拉框、选日期、选时间段、确认挂号。页面用Element UI的form表单和select组件组合,后端返回数据通过onChange事件驱动下一级选项刷新:
<el-form-item label="选择科室"> <el-select v-model="form.deptId" @change="loadDoctors" placeholder="请选择科室"> <el-option v-for="item in deptList" :key="item.id" :label="item.deptName" :value="item.id"></el-option> </el-select> </el-form-item> <el-form-item label="选择医生"> <el-select v-model="form.doctorId" @change="loadSchedules"> <el-option v-for="item in doctorList" :key="item.id" :label="item.doctorName + ' ' + item.title" :value="item.id"></el-option> </el-select> </el-form-item>这里用@change事件驱动联动刷新,handleChange时先清空下一级的数据再加载新数据,防止旧数据残留。有个细节:选择医生导致排班列表更新时,要重置之前选中的日期和时间段,否则会出现医生换了但日期还是旧医生排班的逻辑错误。
提交挂号时把deptId、doctorId、scheduleId、patientName一起传给后端,后端只校验scheduleId是否有效,dept和doctor理论上可以根据scheduleId反查出来。传冗余字段的目的是前端方便展示,后端以scheduleId为准。
4.4 管理端页面:表格与弹窗的标准操作
管理员的排班管理页是典型的CRUD页面:表格展示排班列表、新增和编辑共用同一个弹窗、删除前有确认提示。用Element UI写这种页面有固定套路,el-table的data属性绑列表数据,操作列放编辑和删除按钮,新增编辑共用el-dialog组件:
<el-table :data="scheduleList" border stripe> <el-table-column prop="doctorName" label="医生" width="120"></el-table-column> <el-table-column prop="workDate" label="日期" width="120"></el-table-column> <el-table-column prop="timeSlot" label="时间段" width="100"></el-table-column> <el-table-column prop="remaining" label="剩余号源" width="100"></el-table-column> <el-table-column label="操作" width="200"> <template slot-scope="scope"> <el-button type="primary" size="mini" @click="openEdit(scope.row)">编辑</el-button> <el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table>管理端的权限控制在前端和后端都要做。前端用导航守卫挡掉非管理员访问,后端的Controller方法上加标注或者用注解拦截相同效果。后端控制才是真正的安全防线,前端隐藏按钮只是体验优化。
5. 避坑指南:六条高频事故的现场复盘
5.1 前端请求跨域被拦截
现象:前端页面能打开,但调用接口就报CORS错误,浏览器控制台显示Failed to fetch。原因:启动了两个服务,前端8081端口请求后端的8080端口,浏览器跨域策略阻止了请求。这是前后端分离项目最常见的第一道坎。解决:两个方案任选。后端在配置类里实现WebMvcConfigurer重写addCorsMappings,允许所有来源和所有请求头;或者前端在vue.config.js里配置devServer的proxy代理,把/api开头的请求转发到8080端口。我推荐用代理方案,上线后改个配置就行,不用动后端跨域逻辑。
5.2 数据库脚本导MySQL 5.7报错
现象:导入SQL文件时报错,提示Incorrect table definition或Unknown collation。原因:脚本可能包含了MySQL 8.x才支持的字符集排序规则,比如utf8mb4_0900_ai_ci,MySQL 5.7不认识。解决:用文本编辑器打开SQL文件,全局搜索替换utf8mb4_0900_ai_ci为utf8mb4_general_ci。如果脚本里还有utf8mb4_unicode_ci,5.7是支持的,不需要改。反过来,如果你本地是MySQL 8.x但脚本是5.7时代的,一般不会有兼容问题,8.x向下兼容。
5.3 前端页面刷新后404
现象:登录后进入首页一切正常,按F5刷新页面就白屏,控制台报404错误。原因:VueRouter用了history模式,路由是前端管理的,但服务器不知道这个路径,直接按物理路径找文件找不到,所以返回404。解决:开发环境在vue.config.js的devServer配置里加historyApiFallback: true。生产环境部署到Nginx需要在配置里加try_files指令,或者直接把路由模式改成hash模式,崩溃概率最小,代价是URL多一个#号,不怎么美观。我认为毕设项目用hash模式最省心,出问题的可能性最低。
5.4 MyBatis-Plus分页不生效
现象:调用selectPage方法时传了分页参数,但返回的结果还是全部数据,total字段为0。原因:MyBatis-Plus的分页插件需要手动配置拦截器,没有配置PaginationInnerInterceptor就不会拦截SQL生成limit语句。这是新手最容易漏掉的配置。解决:在MyBatisConfig配置类里注册MybatisPlusInterceptor,并添加分页拦截器:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; }登录接口连不上数据库现象:输入管理员账号密码点登录,页面转圈很久后提示"系统错误"或"服务不可用"。原因:大概率是数据库没启动或者application.yml里数据库连接配置不对。解决:先看控制台日志,SpringBoot启动时会打印数据源初始化信息,如果里面有Access denied或Communications link failure,就去检查MySQL服务是否启动、密码是否正确。重点检查application.yml里spring.datasource的url、username、password三项,MySQL 8.x的驱动和url标点也容易写错,url里必须有useSSL=false和characterEncoding=utf8。
5.6 修改了数据库表但启动报字段不存在
现象:自己加了个字段,重启后端后接口报Unknown column错误。原因:数据库表改了但Entity类没有同步加字段,或者代码里有SQL语句引用了旧字段。这类问题在多人协作时特别容易发生,自己改了表没同步给队友。解决:建议每次改表后全局搜索所有Entity类、Mapper XML文件、业务代码中对相关字段的引用。老实说这类问题到最后靠的是规范意识,把表结构变更记录到一个MEMBERS.txt文件里。
6. 部署验证与二次开发:让系统从能跑到可答辩
6.1 从源码到跑通三步走
拿到项目后按这三步启动,每一步搞定再进下一步,不要一起启动出了问题找不到方向。第一步启动数据库:打开Navicat新建数据库,字符集选utf8mb4,运行data.sql脚本。第二步启动后端:IDEA导入项目,等Maven依赖下载完成后修改application.yml的数据库密码为你本机的,然后运行XxxApplication的main方法,看到SpringBoot启动日志就成功了。第三步启动前端:在IDEA终端或VSCode里进入frontend目录,执行npm install安装依赖,再执行npm run serve启动开发服务器,浏览器打开localhost:8081。
6.2 验证核心主流程跑通
启动完成后别急着演示,先在系统里把主要流程走一遍。用管理员账号登录,检查科室管理、医生管理、排班管理页面能否正常增删改查。用医生账号登录,确认能看到自己的排班列表和患者订单。用患者账号注册新用户,走一遍选科室选医生下单挂号,然后切到医生账号去就诊。这套闭环验证下来,80%的问题都能暴露出来。
6.3 二次开发的三个方向
想拿高分的话,可以从三个方面扩展。第一是引入Redis缓存科室列表和排班信息,每次查询不用刷数据库,面试时聊缓存设计有话可说。第二是添加定时任务,每天晚上自动为未来七天生成医生排班,用Spring自带的@Scheduled就能实现。第三是加一个数据可视化看板,统计每日挂号量、科室热度排行,前端用ECharts画图表,效果很直观。
我曾经就这个系统做过一次大的重构,把挂号接口的并发控制从悲观锁改成了Redis分布式锁,当时没想到最简单的synchronized在集群环境会失效,意识到在分布式场景下锁必须脱离JVM内存才能起作用。从配置MySQL开始一步步跑通这套项目,慢慢对SpringBoot和Vue的联调方式有了整体认知,我才算真正理解了Web全栈项目的运转机制。之后每拿到一套新的源码,我都先看数据库表和接口文档,再启动项目实测主流程,这套习惯帮我避开了不少隐藏坑。这套项目的源码、SQL脚本和毕业论文配置都有,拿到手记得按照第三章和第四章的代码思路通读一遍再动手改,你会发现改起来顺手很多。希望帮到你。
本文还有配套的精品资源,点击获取