医疗挂号系统毕业设计实战:SpringBoot+Vue+MySQL全流程开发与并发控制

发布时间:2026/10/18 21:19:06
医疗挂号系统毕业设计实战:SpringBoot+Vue+MySQL全流程开发与并发控制
1. 毕设选题的定位医疗挂号系统到底在解决什么问题很多同学在选毕业设计题目时第一反应是做个管理系统。但管理系统和管理系统之间差距很大图书管理、仓库管理、员工考勤这类题目做出来容易答辩时却很难讲出技术深度。医疗挂号系统之所以值得做恰恰是因为它不是一个单纯的增删改查而是带着状态流转、并发控制、角色权限、业务流程闭环这些真实业务难点每一个都能在论文和答辩里变成实打实的亮点。我在做这个项目时下面所有内容都基于我实际开发的一套SpringBootVueMySQL医疗挂号管理系统最大的体会是挂号系统的核心不是挂号这一个动作而是患者在系统中的全流程状态——从注册登录、选择科室和医生、查看排班、提交挂号、生成号票、进入排队队列再到医生叫号、填写病历、完成诊疗、收费结算。任何一个环节断了整个系统就假了。所以做这个题目的第一步不是写代码而是把这条流程线画清楚。从毕业设计的评审角度来看这个选题有四个天然优势第一业务场景真实不需要编造复杂的伪需求第二技术栈主流SpringBoot Vue MySQL是当前企业级开发最常见的组合第三功能边界清晰患者端、医生端、管理员端三端分开工作量可控第四存在明确的技术难点比如号源并发、排队去重、排班冲突检测这些都是评委最爱追问的地方。也就是说只要你把流程做完整、把难点讲明白这就是一个上限很高、下限也不低的题目。1.1 需求梳理患者、医生、管理员三方角色到底各需要什么做需求分析时不要一上来就画用例图虽然论文里需要先用最朴素的方式列清单。我当时花了三天时间把需求分成了三张表开发时发现这套清单几乎覆盖了所有后期改动需求患者端需求注册登录手机号 密码后期可以扩展验证码但我建议基础版先用账号密码否则还要接短信服务毕设阶段容易卡在资质审核上查看科室列表与医生排班信息在线挂号选择科室 - 选择医生 - 选择日期和号源时段 - 确认挂号查看我的挂号记录待就诊 / 已完成 / 已取消取消挂号仅限未就诊的号医生端需求查看排班日期和当前队列开始叫号、结束就诊填写/修改就诊病历和诊断建议查看历史接诊记录管理员端需求科室管理增删改查医生管理医生账号创建、所属科室、职称排班管理给指定医生生成未来N天的排班表并生成对应号源挂号记录查看与统计按科室、医生、日期统计挂号量公告发布可选有的思路里不包含但我做了因为医院有停诊通知需求这套需求最关键的设定是排班驱动号源。也就是说患者能看到的号不是凭空生成的而是管理员先为医生生成排班排班再自动展开成一个个号源比如某医生上午放30个号每个号对应一个时间段。系统必须保证医生没有排班时患者无法挂号号源被挂完时患者只能选其他时段。1.2 非功能需求毕设做出来的系统为何要强调可用性评委不会只看功能列表更关注的是系统扛不扛得住真实场景。医疗挂号系统的典型特征是瞬时高并发读——早上8点放号时段大量患者同时在刷新号源如果代码不加控制很容易出现一个号被多人同时挂走的情况。另外数据一致性要求高挂号记录不能重复、取消挂号后号源必须恢复、医生接诊后挂号状态必须流转到下一状态。所以我在设计之初就把几个非功能性指标定下来了数据库连接池用HikariCPSpringBoot默认配置maximum-pool-size20号源扣减使用MySQL悲观锁或Redis分布式锁本地调试用数据库锁论文里讲两种方案的取舍状态流转不允许跳变比如待就诊不能直接变已完成必须经过就诊中所有接口统一返回Result对象前端通过code字段判断业务成功与否这些细节看起来零碎但到写论文的时候你会发现每一处都有东西可写。比如为什么不用乐观锁而是用悲观锁可以从MySQL默认隔离级别、行锁机制、高并发下的冲突概率、Redis不可用时的降级方案等多个角度展开。2. 数据库设计挂号业务的核心是状态流转数据库设计是整个项目的地基。我第一版表结构只设计了五张表用户、科室、医生、挂号记录、排班结果做到医生端就诊记录时发现病历不知道存哪里做统计功能时发现查询逻辑极其别扭后来重构到了七张表。这个重构过程本身就是论文里数据库设计迭代优化的素材。最终的七张核心表如下表名关键字段核心作用userid, username, password, role, real_name, phone统一用户表用role区分患者/医生/管理员departmentid, dept_name, description科室表内外妇儿等doctorid, user_id, dept_id, title, schedule_enabled医生信息表关联user和departmentscheduleid, doctor_id, dept_id, work_date, period_type, total_slots, max_slots排班表period_type区分上午/下午/全天registrationid, patient_id, doctor_id, dept_id, schedule_id, reg_date, slot_time, status, visit_no挂号记录表整个系统的核心medical_recordid, registration_id, diagnosis, prescription, create_time病历表挂号记录一对一生效announcementid, title, content, create_time公告表停诊/更新通知2.1 用户表与医生表的耦合设计一开始我把用户表设计成一张表全搞定后来发现医生有科室归属、职称、排班状态这些属性患者有医保卡号之类的属性全部塞进一张user表会让字段大量冗余。于是改成user表只存账号归属和角色doctor表通过user_id外键关联存医生专属属性。这个设计在写后端时非常舒服。比如管理员创建医生账号业务逻辑是先在user表插入roleDOCTOR的记录再在doctor表插入对应信息事务包裹两步同时成功或失败。而在查询医生排班时前端需要的不是doctor_id而是医生姓名、科室名称、职称等展示字段后端可以用一条连表查询搞定SELECT d.id, d.title, u.real_name AS doctorName, dp.dept_name FROM doctor d LEFT JOIN user u ON d.user_id u.id LEFT JOIN department dp ON d.dept_id dp.id WHERE d.dept_id #{deptId}2.2 挂号状态机设计不要只用一个status字段草草了事挂号记录表的status字段是整个系统状态流转的核心我用的是字符串枚举常量。状态流转必须严格遵循下面的顺序CANCELED已取消患者主动取消或管理员后台取消WAITING待就诊挂号成功进入排队队列IN_TREATMENT就诊中医生叫号后置为就诊中COMPLETED已完成医生结束就诊生成病历可选扩展NOSHOW爽约超时未就诊自动标记设计这个状态机时最重要的原则是禁止跳变。例如不允许一条挂号记录直接从WAITING跳到COMPLETED必须经过IN_TREATMENT因为医生还没有给出诊断结果时完成这个动作是不成立的。后端的校验可以在Service层做一个状态流转映射表private static final MapString, ListString STATE_TRANSITIONS new HashMap(); static { STATE_TRANSITIONS.put(WAITING, Arrays.asList(IN_TREATMENT, CANCELED)); STATE_TRANSITIONS.put(IN_TREATMENT, Arrays.asList(COMPLETED)); STATE_TRANSITIONS.put(COMPLETED, Collections.emptyList()); STATE_TRANSITIONS.put(CANCELED, Collections.emptyList()); }每次更新状态前先查当前状态、取合法的下一个状态集合、做校验。能过校验才执行UPDATE。这个设计在论文里可以单独写一节基于有限状态机的业务状态管理属于软件工程里的经典模型应用到实际业务的案例答辩时有得聊。2.3 索引设计查询慢就加索引没那么简单毕设阶段数据库量不大但你要是说实话我没建索引评委可能会觉得你对数据库不够敏感。我在三张表上建了索引都有实际依据registration表(patient_id, status)患者查询我的挂号记录时条件必然包含这两个字段registration表(schedule_id, status)管理员查看某个排班下的挂号列表以及统计剩余号源schedule表(doctor_id, work_date)医生端查看排班联合索引有一个容易被忽略的点字段顺序。索引(a, b)和索引(b, a)的使用场景完全不同。拿(patient_id, status)举例它能同时支撑这两类查询WHERE patient_id 1 AND status WAITING以及只带patient_id的查询。但如果你的查询条件是WHERE status WAITING而不带patient_id这个联合索引是用不上的。我在实际调试时用EXPLAIN验证过这也成了论文里截图素材的一部分。3. 后端实现挂号、队列与医生工作台的关键代码逻辑后端是整个系统的中枢。我采用分层架构Controller - Service - MapperDTO和VO分离——接收参数用DTO返回前端用VO实体Entity只在Service和Mapper之间流转。有同学觉得毕设不用搞这么复杂但到后期改需求时你会感谢这个分层。比如某天你想在返回前端的医生信息里加一个今日剩余号数字段你只需要改VO和Service的组装逻辑数据库表结构和Mapper都不用动。3.1 项目初始化与统一响应设计SpringBoot项目结构我习惯这样组织src/main/java/com/hospital/ ├── common/ // 统一响应、全局异常、常量 ├── config/ // 跨域配置、拦截器注册 ├── controller/ // 患者端、医生端、管理员端控制器 ├── service/ // 业务接口与实现 ├── mapper/ // MyBatis-Plus的Mapper层 ├── entity/ // 数据库实体 ├── dto/ // 接收参数对象 └── vo/ // 返回视图对象统一响应体是我的第一个重点封装。客户端只认code、message、data三段式public class ResultT { private Integer code; private String message; private T data; }全局异常处理器捕获业务异常和系统异常避免把500堆栈直接抛给前端。比如挂号失败时抛出BusinessException(当前号源已被挂完)前端拿到code500就弹出提示框拿到code200才走成功逻辑。3.2 挂号接口的并发控制这是全项目最关键的15行代码挂号接口的业务流程前端传入scheduleId、patientId。后端先查排班是否有号再查患者是否重复挂号再扣减号源最后插入挂号记录。听起来很简单但高并发下三个步骤之间会有时间窗口。最简单的数据库方案是给排班记录加行级锁Transactional public Registration register(RegisterDTO dto) { // 悲观锁锁定排班行防止超卖 Schedule schedule scheduleMapper.selectByIdForUpdate(dto.getScheduleId()); if (schedule null) { throw new BusinessException(排班不存在); } if (schedule.getUsedSlots() schedule.getTotalSlots()) { throw new BusinessException(号源已满); } // 防止同一患者重复挂号结合unique约束 int count registrationMapper.countByPatientAndSchedule( dto.getPatientId(), dto.getScheduleId()); if (count 0) { throw new BusinessException(请勿重复挂号); } // 扣减号源插入挂号记录 scheduleMapper.increaseUsedSlots(schedule.getId()); Registration reg buildRegistration(dto, schedule); registrationMapper.insert(reg); return reg; }selectByIdForUpdate是MyBatis-Plus自定义SQL底层就是SELECT ... FOR UPDATE事务提交时释放行锁。这段逻辑解决了超卖问题不过要注意两点第一这个方法必须在Transactional事务内执行第二FOR UPDATE锁的是索引行所以查询条件必须走主键或索引否则会锁全表。讲完数据库锁方案论文里还可以加一段为什么不用乐观锁或Redis锁。乐观锁适合冲突少的场景而放号时段是典型的高冲突场景CAS重试会导致大量请求空转Redis锁性能好但引入了额外中间件单机部署毕设项目时反而增加了复杂性和运维成本。我的结论是单体应用 MySQL场景下悲观锁是正确性优先的最优解。这句话在答辩时要敢说并且能解释为什么。3.3 排班生成与号源释放机制排班生成是管理员端最核心的功能。实际问题在于管理员选择医生和时间范围后系统需要自动生成排班并展开号源。我的实现思路是使用一个循环生成多个Schedule记录然后为每个Schedule创建对应数量的号源记录。因为前端需要展示剩余号数所以我在schedule表里加了used_slots字段初始为0挂一个号加1剩余号数total_slots-used_slots。取消挂号的号源释放同样要小心。患者取消挂号后需要将对应排班的used_slots减1并更新挂号记录状态为CANCELED。这里必须用事务包住两步操作——先改状态再减号源否则可能出现号源减了但记录没取消或状态取消了但号源没恢复的情况。我在实际操作中发现这两个操作的顺序看起来无所谓但如果你把它们分散在Service和Mapper里的不同方法中就很容易漏掉事务注解。建议把退款/退号这类操作写成一个独立方法命名清晰内部逻辑保持原子性。4. 前端Vue实现三种角色的页面组织与交互前端我用的是Vue 2 Element UI现在的毕设项目也很多开始用Vue 3 Element Plus但如果你是基于某套现成源码二次开发Vue 2依然稳定。Vue CLI脚手架搭建通过Axios封装请求登录后把token存到localStorage路由守卫中进行登录拦截。如果你用的是vite vue3核心思路完全一致只是API形态略有差异。我在实际开发中用的是Vue 2但下面的设计思路对Vue 3同样适用。4.1 路由权限与动态菜单前端不能只靠按钮隐藏前端权限最常见的坑是菜单隐藏了就以为没权限了。实际上隐藏菜单只是用户体验层面的处理真正的安全控制在后端接口权限。但前端至少要做到不同角色登录后只能看到自己的页面。我在路由设计里用了一个简单的做法在路由表里给每个页面添加meta.roles数组路由器守卫中判断当前用户角色是否在允许列表中router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); return; } const roles store.state.user.roles; if (to.meta.roles !to.meta.roles.some(r roles.includes(r))) { next(/403); return; } next(); });菜单栏的渲染也可以根据角色动态生成避免用户手动输入URL访问无权限页面。因为后端接口也做了角色校验所以这套双端校验逻辑在论文里可以描述为前后端双重访问控制。4.2 患者端挂号流程的交互细节患者端挂号页面我仔细想过的交互流程是这样的先进入科室列表页卡片展示每个科室的医生数量点击科室进入该科室的医生列表点击医生进入医生详情展示医生的排班计划选择一个排班日期和时段点击挂号这里有一个容易被忽视但实际很重要的点排班展示需要实时计算剩余号数。前端不能只展示剩余30号这个静态值而是在加载时请求后端接口拿到当前used_slots实时计算剩余。否则患者看到有号点击挂号却被提示号已挂完体验很差。我在后端专门写了一个接口返回医生排班信息的同时附带剩余号数{ scheduleId: 101, doctorName: 某医生, deptName: 心血管内科, workDate: 2025-06-10, periodType: 上午, totalSlots: 30, remainingSlots: 12, status: AVAILABLE }前端拿到数据后如果剩余号数为0挂号按钮直接置灰并显示约满。从交互层面提前避免无效操作也能减轻后端压力。这个设计思路写进论文里也很加分标题可以用基于前端预校验与后端并发控制的号源一致性保障。4.3 医生端叫号与票号显示医生工作台的设计我参考了现实中医院排队叫号系统的逻辑。医生登录后首页展示今天的排班和待就诊列表。点击叫下一号后端逻辑是从当前排班下状态WAITING的记录中按visit_no就诊序号升序取第一条将这条记录的状态改为IN_TREATMENT返回该记录的患者信息前端大屏展示请某某患者到3号诊室就诊这里前端需要实现一个当前叫号的轮询。我用的是setInterval定时器每5秒请求一次当前正在就诊的号码。这个小功能在整个系统里看起来不起眼但却是流程完整性的点睛之处也是答辩时演示体验比较好的功能之一。我当时演示时从患者挂号、到医生工作台看到队列、到叫号、到填写病历整个链路一气呵成评委一眼就能看出系统不是纸老虎。5. 部署、论文撰写与答辩准备的实战要点源码、数据库、论文、部署文档是毕业设计交付的四个部分。很多同学把精力全砸在编码上到最后一周才开始写论文和部署结果手忙脚乱。我的建议是时间分配尽量做到4:4:2——一半时间编码调试四成时间写论文做图两成时间部署和准备答辩。5.1 本地部署与打包细节部署最大的坑是环境差异。我在本地跑得好好的拿到一台干净的服务器上一启动就报错最后排查发现是MySQL版本不同导致SQL语法不兼容。所以部署文档里一定要写明前置环境逐个验证之后再启动项目。实际部署方式有两种开发环境直接IDE启动适合演示场景最快最省事生产环境jar包部署mvn clean package打成可执行Jar包配合nohup运行后端打包命令mvn clean package -DskipTests nohup java -jar hospital-admin-server-0.0.1-SNAPSHOT.jar --server.port8080 前端我在Vue配置里设置了devServer代理将/api路径转发到后端8080端口。开发环境用代理解决跨域生产环境用Nginx反向代理同时托管前端静态资源和后端API路由。如果你用的是一台服务器Nginx配置大致是这样server { listen 80; server_name your_domain_or_ip; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /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; } }注意try_files的配置Vue是单页应用如果刷新一个子路由路径比如/admin/doctorNginx找不到对应文件会404必须通过try_files回退到index.html让前端路由接管。数据库部署也有讲究不要把本地开发数据直接带到演示环境。我当时准备了一套干净的初始化数据——5个科室、20个医生、未来7天的排班、若干测试患者账号。演示的时候从患者注册开始走完整流程数据干净清晰不会露出之前调试时的脏数据。5.2 论文框架与图表制作论文本质上是对项目的复盘。我的目录结构大致是这样第一章 绪论背景与意义、国内外研究现状、主要工作第二章 相关技术SpringBoot、Vue、MySQL、MyBatis-Plus第三章 系统分析可行性分析、需求分析、用例图、数据流图第四章 系统设计总体架构、功能模块设计、数据库设计ER图、表结构、状态机设计第五章 系统实现关键模块的实现过程 页面截图 核心代码片段第六章 系统测试功能测试用例表格 并发测试结果第七章 总结与展望画图工具我用的是对应领域的在线绘图软件统一风格导出高清图再插入论文。用例图、ER图、系统架构图、时序图这四类图是评委最常看的。有一个写论文的重要心得截图不要用整个页面要裁剪关键区域。论文打印出来是黑白的你截一张深色导航栏的页面打印后一片黑。我当时裁剪了页面的核心表单区、列表区、图表区每张配一两句说明清晰又精炼。5.3 答辩中容易被追问的技术点准备答辩时我把项目里每个可能被追问的技术点都列了一遍然后逐一准备了自己能讲2分钟的答案。这些追问点包括为什么选MySQL而不是其他数据库单表数据量不大、事务支持稳定、配套工具成熟数据库事务隔离级别默认是什么RC和RR在并发量上有什么差别项目用默认RR锁开销可控密码存的是明文吗BCrypt加密论文里要写如果一个排班同时只剩最后一个号两个患者同时提交挂号会发生什么FOR UPDATE行锁一个等锁一个执行不会超卖前端token过期了怎么办Axios拦截器统一跳登录页项目有没有什么不足如果上生产环境需要怎么改单机部署限制、缺少缓存、无操作日志、密码找回没做其中第七个问题尤其重要我建议答辩前一定要准备好。主动承认项目的局限性然后给出可行的优化方向会给评委留下这个学生思路完整的印象。想都不用想评委几乎必问这个问题目的是看你是只做了功课还是真正掌握了背后的技术判断力。5.4 我踩过的三个印象深刻的坑第一个坑是跨域问题。本地前端跑在8081后端跑在8080前端请求后端接口直接CORS报错。我一开始在后端写了CrossOrigin注解能解决开发环境的问题但打包部署后通过Nginx同域代理又遇到了新的问题。最后我在生产环节直接让Nginx同时托管前端和后端把跨域问题从根上去掉了。这个先后过程让我理解了一个道理开发时方便自己的方案和生产运行时方便用户的方案经常不是同一个方案。第二个坑是无人值守时的会话过期。演示时前端页面停留太久登录token过期了后端返回401前端没做统一处理导致点击任何功能都没反应。查了半小时才意识到是token过期。后来我在Axios响应拦截器里加了401跳转逻辑所有接口的未授权状态统一处理service.interceptors.response.use( response response, error { if (error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );第三个坑是一开始没有想清楚的状态机跳变。之前为了功能演示方便在测试环境直接通过修改数据库状态来模拟流程结果有一次把一条WAITING记录直接改成了COMPLETED后面统计已完成就诊数时数据就对不上了。后来我强制所有状态变更都走Service层方法禁止绕过业务逻辑直接改库。这也是为什么我前面建议在代码里维护一张合法的状态流转映射表等于给系统上了一道保险。6. 最后再分享一点体会整个项目从画流程图到最终答辩前后花了大约六周时间。有同学问我这个题目简单吗我的回答是任何管理系统单独拆开都不难难的是把流程串起来、把并发问题处理掉、把状态流转做严谨。如果你只是做一个能挂号的demo那两周就够了但要做一个答辩时有内容讲的项目就必须在这些细节上多花心思。目前这套系统的扩展空间还很大比如接入微信小程序、增加体检预约、加入Redis缓存热点科室排班信息、以及引入MQ异步处理叫号通知。如果你在毕设基础上还想更进一步可以挑其中一到两个点真正实现而不是停留在论文的展望里。从我的经验来说一个毕业设计最重要的不只是完工那一刻的成就感而是你在每一个坑里想明白的那些为什么这些东西在面试时才是最扎实的底气。