SpringBoot+Vue医院挂号系统:毕设源码与核心业务解析
简介一套基于SpringBoot与Vue开发的医院挂号就诊管理系统毕设项目资源面向需要快速完成毕业设计的Java方向学生覆盖挂号管理、在线咨询、健康教育与公告管理等典型业务模块。系统采用前后端分离架构后端基于SpringBootMybatisMaven前端使用Vue与ElementUI配合MySQL数据库整体技术栈贴近企业实战也便于进一步扩展功能。压缩包共772个文件含Java源码、Vue组件、CSS样式、SQL脚本以及install/run/build批处理脚本资源包约35.77MB导入IDEA或Eclipse即可直接部署调试。系统内置管理员、医生、用户三种角色可完整演练登录授权、挂号流程、健康资讯维护、轮播图配置等核心功能同时附带毕业论文文档与数据库初始化脚本帮助理解从建表到前后端联调的完整实现思路。目前已有58人学习包内另含SVG图标、图片及演示音视频等辅助资源适合作为毕业设计参考、项目二次开发或岗位实训的基础。1. SpringBootVue医院挂号系统毕设源码包里到底有什么每年毕设季医院挂号就诊管理系统都是Java方向的高频选题。我看了不少同学发来的求助帖问题多数不是选题太难而是拿到手的项目要么结构混乱、要么缺数据库脚本、要么前后端接口对不上改两天还是跑不起来。这套基于SpringBootVue开发的医院挂号就诊管理系统正好把这条链路的坑先替你趟平了。它是一套完整的全栈项目前端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和排班IDService层去校验号源是否充足、扣减号源数量、生成订单事务控制在Service层完成。这种分层方式也方便后续扩展功能比如增加微信支付只需要在Service层加一段逻辑不用动Controller。2.3 数据库表设计与核心字段我一般拿到一个毕设项目第一件事就是看数据库脚本因为表结构决定了业务的边界。这套系统的核心表设计得比较规整用户表、科室表、医生表、排班表、挂号订单表、就诊记录表每张表的字段都贴合实际业务场景。用户表区分角色用role字段标记管理员、医生、患者三种身份这种设计比单独建三张用户表简单得多权限控制也容易实现。表名关键字段说明sys_userid, username, password, role, phone三种角色共用的用户表departmentid, dept_name, dept_desc科室基本信息doctorid, user_id, dept_id, title, intro医生信息user_id关联sys_userscheduleid, doctor_id, work_date, time_slot, total, remaining排班表remaining表示剩余号源appointment_orderid, order_no, user_id, schedule_id, status, create_time挂号订单表medical_recordid, 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的条件构造器可以少写很多SQLpublic ListDoctorVO getDoctorsByDept(Long deptId) { LambdaQueryWrapperDoctor wrapper new LambdaQueryWrapper(); wrapper.eq(Doctor::getDeptId, deptId); ListDoctor doctors doctorMapper.selectList(wrapper); // 批量查出关联用户信息组装VO返回 ListLong userIds doctors.stream().map(Doctor::getUserId).collect(Collectors.toList()); ListSysUser 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中的实现是自定义SQLselect idselectForUpdate resultTypecom.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-modelform.deptId changeloadDoctors placeholder请选择科室 el-option v-foritem in deptList :keyitem.id :labelitem.deptName :valueitem.id/el-option /el-select /el-form-item el-form-item label选择医生 el-select v-modelform.doctorId changeloadSchedules el-option v-foritem in doctorList :keyitem.id :labelitem.doctorName item.title :valueitem.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 :datascheduleList border stripe el-table-column propdoctorName label医生 width120/el-table-column el-table-column propworkDate label日期 width120/el-table-column el-table-column proptimeSlot label时间段 width100/el-table-column el-table-column propremaining label剩余号源 width100/el-table-column el-table-column label操作 width200 template slot-scopescope el-button typeprimary sizemini clickopenEdit(scope.row)编辑/el-button el-button typedanger sizemini clickhandleDelete(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_ciMySQL 5.7不认识。解决用文本编辑器打开SQL文件全局搜索替换utf8mb4_0900_ai_ci为utf8mb4_general_ci。如果脚本里还有utf8mb4_unicode_ci5.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里必须有useSSLfalse和characterEncodingutf8。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脚本和毕业论文配置都有拿到手记得按照第三章和第四章的代码思路通读一遍再动手改你会发现改起来顺手很多。希望帮到你。本文还有配套的精品资源点击获取