资讯动态

SpringBoot+Vue医院挂号就诊系统设计与实现全解析

发布时间:2026/9/16 3:29:38 来源:尧图企业网站定制
兄弟后台私信一直有人问全栈项目怎么入门、怎么设计刚好我手里这套SpringBootVue的医院挂号就诊系统完整源码还在抽了个周末把它重新梳理了一遍。这篇文章我会把整个系统的设计思路、数据库表结构、核心接口逻辑、前端页面联调包括中间踩过的那些坑全部摊开讲清楚。项目本身不复杂但该有的功能都有——用户注册登录、科室医生管理、在线挂号、就诊记录、后台管理属于典型的Java全栈实战项目适合正在学SpringBoot、Vue的初学者也适合拿来当毕业设计参考或者直接改改拿去接外包。我把能说的不能说的细节都写在下面了跟着走一遍基本能跑通。1. 项目全景与选型思路1.1 这个系统到底解决什么问题医院挂号这个场景经历过线下排队的人应该都有感触。早上五六点去医院窗口排队挂不上号就只能白跑一趟更别说专家号这种紧俏资源。线上挂号系统要做的事本质上是把医院有限的号源搬到线上让患者提前预约、按时间点就诊同时让医院管理层能够实时掌握排班和就诊数据。这套系统的核心角色分三类普通患者在线注册、浏览科室、预约挂号、查看就诊记录、医生查看排班、处理问诊、管理员管理科室、医生信息、号源配置。基于这个业务划分前端页面大概需要八九个后端接口二十个左右量级不大但胜在功能完整。我见过不少类似的系统做成“只有挂号没有就诊记录”“只有前台没有后台”的半吊子这套不是你要的闭环它都有。从实际开发角度说选这个项目练手有一个很大的好处业务场景足够熟悉不需要花时间理解业务逻辑就能把精力全部放在技术实现上。预约挂号的“先查排班、再选时段、改状态防止超卖”这个流程跟电商系统的“先查库存、锁定库存、提交订单”本质上是同一套逻辑学会了一个另一个也就通了。1.2 技术选型为什么不用纠结先说后端。SpringBoot 2.x是目前Java Web开发事实上的标准约定优于配置这一点在中小型项目里优势太明显了——不需要写一堆XML配置启动类一跑就完事。配合MyBatis做持久层SQL自己控制比JPA那种“帮你生成SQL但关键时候总给你整活”的框架省心得多尤其像挂号系统里有很多多表关联查询和条件动态查询MyBatis的where标签配合if标签几行代码就写完了。前端Vue 2 Element UI是经典组合Vue 2到现在生态依旧成熟Element UI的表单组件、表格组件、弹窗提示拿过来就能用UI界面做得干净大方。虽然Vue 3 Element Plus是趋势但对于刚接触前端的Java开发来说Vue 2的学习资料最多、踩坑案例最全遇到问题搜索引擎一搜就有答案。数据库MySQL 5.7没有悬念。这套系统的数据量完全在MySQL的舒适区事务支持可靠后面我会把关键表结构和索引设计拆开讲。整套选型下来没有一个组件是“高射炮打蚊子”的杀鸡用牛刀行为每个选型都对应了项目规模和团队能力。你要非用Redis做分布式会话、用RabbitMQ做异步放号、用Elasticsearch做全科室搜索那当然也行但那叫过度设计不是做项目该有的姿态。2. 数据库设计与核心表结构2.1 三张基础表放倒一切数据库设计是整个系统的地基地基不稳后面全是泪。先把基础表列出来注释我都加在SQL里了。第一张是用户表sys_user统一存登录账号信息。我见过一些人把患者、医生、管理员分三张表设计结果权限联动的时候麻烦得要命。这套系统的做法是张统一表靠role字段区分1是患者2是医生3是管理员。如果是医生角色额外关联一个doctor_id字段指向医生信息表这样登录后才能拉到排班等业务数据。CREATE TABLE sys_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 密码(MD5加密), role tinyint NOT NULL DEFAULT 1 COMMENT 角色:1患者 2医生 3管理员, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, doctor_id int DEFAULT NULL COMMENT 关联医生ID仅医生角色使用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;第二张是科室表department。字段不多科室名称、科室位置、科室简介、科室状态就够用了。这里建议加一个sort_order排序字段管理员维护科室顺序的时候会非常方便需求方如果某天说“把内科排到外科前面”改动就是改个数字的事不用动代码。第三张是医生表doctor。医生信息单独成表而不是直接塞在用户表里是因为医生有自己专属的字段——所属科室、职称、擅长领域、个人简介这些跟登录凭证没半毛钱关系。医生表里department_id关联科室表title字段存职称主任医师、副主任医师、主治医师等挂号列表页要根据职称筛选这个字段必须有索引。2.2 排班表与挂号记录表怎么设计才不打架排班表schedule是系统的核心业务表挂号能不能正常工作全看这张表。每个医生每天可以排多个时段每个时段有总号数和已约号数两个关键字段。CREATE TABLE schedule ( id int NOT NULL AUTO_INCREMENT, doctor_id int NOT NULL COMMENT 医生ID, department_id int NOT NULL COMMENT 科室ID冗余存储方便查询, schedule_date date NOT NULL COMMENT 出诊日期, start_time varchar(10) NOT NULL COMMENT 开始时间 如08:00, end_time varchar(10) NOT NULL COMMENT 结束时间 如08:30, total_count int NOT NULL COMMENT 总号源数, appointed_count int NOT NULL DEFAULT 0 COMMENT 已预约数, status tinyint NOT NULL DEFAULT 1 COMMENT 状态:0停诊 1正常, PRIMARY KEY (id), KEY idx_department_date (department_id,schedule_date), KEY idx_doctor_date (doctor_id,schedule_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;为什么排班表要冗余一个department_id字段因为前端页面最核心的展示就是“选科室 → 看该科室的排班列表”如果不冗余每次展示都要先根据医生表查科室再关联排班多一次关联查询不说SQL也写得更绕。这个冗余字段的代价就是排班表多占一点存储空间但换来的查询性能提升非常值。挂号记录表appointment记录每次挂号行为。每个患者可能给家人挂号所以除了患者用户ID还需要冗余一个patient_name字段存就诊人姓名。这个表里最关键的是appointment_no字段也就是取号码规则是日期医生ID三位流水号比如20241215005001患者拿着这个号到医院签到就诊。挂号表设计的时候有一个容易忽略的坑status字段必须区分“已挂号”“已完成”“已取消”三种状态。取消要原因可选完成要关联诊断记录。很多半成品系统只有“已挂号”一种状态导致后端统计已就诊人数时完全无法实现。3. 后端SpringBoot核心功能实现3.1 项目结构与MyBatis使用细节后端项目结构按标准的三层架构来分包controller、service、mapper再加上entity、dto、config、common几个包。我见过不少人一上来就把所有类堆在一个包下面项目小的时候没问题但一旦业务扩展找文件找到怀疑人生。com.hospital.registration ├── controller # 接口层 │ ├── AuthController.java │ ├── DepartmentController.java │ ├── ScheduleController.java │ └── AppointmentController.java ├── service # 业务层 │ ├── AuthService.java │ ├── ScheduleService.java │ └── AppointmentService.java ├── mapper # 数据访问层 │ ├── UserMapper.java │ ├── DepartmentMapper.java │ ├── ScheduleMapper.java │ └── AppointmentMapper.java ├── entity # 实体类 ├── dto # 请求响应对象 ├── config # 配置类 └── common # 统一返回结果、异常处理等MyBatis配置上有一个细节很多人不注意map-underscore-to-camel-case这个配置项。数据库字段是下划线风格如doctor_idJava实体类是驼峰风格如doctorId不开启自动映射的话ResultMap要写到手抽筋。在application.yml里加上mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xmlmapper-locations指定了XML文件位置我习惯把所有SQL写在XML里而不是用注解理由很简单动态SQL一复杂注解写法就是一场灾难。Select注解里拼script标签写动态SQL维护起来比XML难受十倍。3.2 挂号接口的并发控制挂号接口是整个系统里技术含量最高的一环。很多人第一次实现挂号就是直接插入一条订单记录完全不考虑号源超卖问题。同一个时段有10个号100个人同时请求最后可能卖出110个号这就是超卖。解决方案是用乐观锁更新排班表时加上appointed_count total_count这个条件。update idincreaseAppointedCount UPDATE schedule SET appointed_count appointed_count 1 WHERE id #{scheduleId} AND appointed_count lt; total_count /update这段XML里的lt;是XML转义的小于号。UPDATE语句先判断已约数是否小于总数只有条件成立才更新MySQL的行锁保证了这条更新的原子性。返回的影响行数int如果等于0说明号满或者排班不存在直接返回“号源已满”的提示不需要额外加Redis分布式锁。挂号业务完整流程三步走校验参数检查排班是否存在、日期是否已过、患者信息是否完整。尝试占用号源执行上面的update语句影响行数为0直接返回失败。插入挂号记录号源占用成功后插入预约记录生成取号码。三个步骤里最关键的是第二步必须发生在第三步之前如果先插入记录再更新号源计数高并发下就会出现“记录存在但排班没扣数”的数据不一致。事务注解Transactional要加在service层方法上保证三步要么全部成功要么全部回滚。3.3 登录鉴权与拦截器这套系统没有引入Spring Security这种重量级框架原因很简单业务里只有三种固定角色权限模型非常清晰用拦截器自定义注解就能搞定。用户登录成功后后端生成一个随机Token字符串存到Redis里Key是token:{uuid}Value是用户ID并设置两小时过期。前端每次请求在Header里带上Authorization字段后端拦截器通过Token查出用户ID再查询用户信息放进ThreadLocal里方便业务方法随时获取。拦截器注册到WebMvcConfigurer里设置哪些路径放行、哪些需要登录Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/department/list, /api/schedule/list); }科室列表和排班列表是游客也能看的所以放行。挂号、后台管理等接口必须登录才能访问。这里还有个细节管理员接口和医生接口也得做角色校验单纯有Token不够。我自定义了一个RequireRole注解标注在Controller方法上拦截器里如果发现当前用户角色不匹配直接返回403。3.4 统一返回结果与全局异常处理接口风格必须统一否者前后端联调就是一场“你说返回{code: 0}我说返回{success: true}”的灾难。我定义了一个Result类统一返回格式public class ResultT { private Integer code; // 200成功 400业务异常 401未登录 403无权限 private String message; // 提示信息 private T data; // 业务数据 }Controller层所有接口都返回ResultT业务异常直接抛出BizException全局异常处理器RestControllerAdvice统一捕获转成对应的code和message返回给前端。这种写法能省掉Controller里大量的try-catch代码简洁度不止一个档次。4. 前端Vue页面与联调4.1 页面结构与路由设计前端用Vue CLI创建项目UI框架Element UI按需引入。组件目录划分src ├── api # 接口请求封装 ├── views # 页面组件 │ ├── login │ ├── register │ ├── home │ ├── department │ ├── schedule │ ├── appointment │ ├── mine # 个人中心 │ └── admin # 后台管理 ├── router # 路由配置 └── store # Vuex状态管理路由设计上要做权限控制普通患者不能进后台管理页管理员不用走挂号流程。实现方案是路由元信息加全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.role to.meta.role ! store.state.user.role) { next(/403) } else { next() } })to.meta.role在路由定义里写死比如管理页路由的meta: { role: 3 }每个路由可访问角色在配置文件里一眼就能看全比在代码里写一堆逻辑判断清晰得多。4.2 接口请求封装与关键交互前端请求用Axios封装一个request工具类所有请求自动携带Token统一处理后端返回的错误码service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) } if (res.code ! 200) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { Message.error(网络请求异常) return Promise.reject(error) } )这套封装有两个关键点一是401时自动跳登录页用户登录过期后体验不会断层二是所有业务错误码统一在这里弹提示页面组件里不需要每个接口都写一遍错误处理。挂号页面是最核心的交互。用户选中科室后进入排班列表排班列表展示医生头像、姓名、职称、擅长时间段和剩余号数。剩余号数为0的排班卡片置灰不可点击。点击“预约”按钮弹确认框确认后调用挂号接口成功后显示取号码。这里有一个细节处理排班查询接口返回的数据里包含起始时间、结束时间、总号数和已约数前端计算剩余号数直接total_count - appointed_count就行不要在排班列表接口里再单独返回一个remain_count字段前端算和计算器算的都是同一个数据没必要两个地方维护。4.3 Vue打包后布局异常问题Vue项目打包部署后最常见的三个问题这套系统一个不落全碰到了第一个刷新404。前端路由用的是HTML5 History模式mode: history本地开发没问题但部署到Nginx后用户直接访问/schedule刷新页面就会404。原因是Nginx默认找不到对应的物理文件。解决办法在Nginx配置里添加try_files回退location / { try_files $uri $uri/ /index.html; }把找不到的路径一律回退到index.html由前端路由自行处理。第二个图片路径错误。打包部署到子路径后静态资源加载404白屏。问题出在vue.config.js里没有配置publicPath默认是根路径/。如果是部署到子路径/hospital下必须改成module.exports { publicPath: process.env.NODE_ENV production ? /hospital/ : / }第三个Element UI的字体文件打包丢失。构建后字体文件加载404图标全部变成小方块。这个通常是url-loader的limit设置问题大字体文件没被正确打包成base64处理方式在vue.config.js里调整chainWebpack配置把element-ui/lib/theme-chalk/fonts的字体文件排除过滤。5. 常见问题与排查技巧实录5.1 前端跨域问题从入门到放弃开发环境最常见的坑前端跑在localhost:8080后端跑在localhost:8081两个端口不同浏览器会直接拦截跨域请求。控制台报错No Access-Control-Allow-Origin header is present这是必踩的坑。开发阶段最简单的处理是配置Vue CLI的代理。在vue.config.js里devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这样前端请求/api/schedule/list会被代理转发到后端http://localhost:8081/api/schedule/list浏览器看到的还是同源请求跨域问题就消失了。生产环境一般是Nginx做反向代理前端和后端通过同一个域名的不同路径区分比如/走静态页面/api/转发到后端服务location ^~ /api/ { proxy_pass http://127.0.0.1:8081/; }proxy_pass后面有没有斜杠区别很大——带斜杠会把/api前缀去掉再转发不带斜杠则完整保留。不搞清楚这个细节生产环境接口404排查半天。5.2 MyBatis的坑缓存、大于小于号、参数问题后端开发最容易踩的MyBatis坑我一次性盘点清楚。MyBatis缓存。MyBatis自带一级缓存Session级和二级缓存Mapper级很多人不管三七二十一把所有查询接口都开了二级缓存结果系统上线后出现“修改数据后列表不刷新”的灵异事件。我的建议是这套系统关掉二级缓存靠MySQL自身的查询效率和合理索引撑住并发量。缓存这个东西加之前要权衡数据一致性不是所有场景都适合。XML里的大于小于号不能直接写。在if标签里写appointedCount lt; totalCount这种条件时直接写会被XML解析器当成标签开始直接报错。解决办法是用转义字符lt;表示小于gt;表示大于。或者![CDATA[ ]]把SQL语句包住。单个字符数字比较。MyBatis里判断单个数字字符时容易出问题比如if teststatus 1如果status是String类型必须用1.equals(status)或者双引号形式status 1来比较。status 1在OGNL表达式里会解析成字符和数字比较结果永远为false导致动态SQL少拼接了条件。5.3 数据库连接与SQL执行问题SpringBoot整合MySQL经常碰到的一个问题是时区报错。默认连接串如果没有指定serverTimezone启动时会报The server time zone value异常。解决方式在JDBC连接串里加参数spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai注意characterEncoding和characterEncodingutf8mb4有些老教程写的是utf8MySQL 8.0下中文存储会出现乱码必须用utf8mb4才能完美支持特殊字符。另一个典型的数据库问题是连接池溢出。系统跑一段时间后接口突然变慢日志报Connection is not available, request timed out。排查思路检查代码里有没有未关闭的数据库连接特别是getConnection()后没有finally块释放的写法检查连接池配置的max-active是否过小。用HikariCP的话spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 300005.4 微信登录还是账号密码登录不少人在评论区问为什么不做微信扫码登录。原因是这个项目最核心的是业务逻辑实现——挂号、排班、号源控制——而不是对接第三方平台的“外挂功能”。微信登录需要申请开放平台账号、配置回调域名个人开发者不一定有资质。这套系统采用最经典的账号密码登录注册、登录、Token鉴权这一整套流程实现一遍后将来接微信登录也就是增加一个OAuth2授权回调接口的事原理相通。5.5 部署上线与运行维护项目本地跑通后部署上线也是一门学问。最简单的部署方式后端用Maven打成Jar包mvn clean package -DskipTests生成的Jar直接扔到服务器上跑nohup java -jar hospital.jar hospital.log 21 。前端打包生成静态文件npm run builddist目录扔到Nginx的html目录下。数据库SQL脚本直接导入服务器MySQLmysql -u root -p hospital hospital.sql。服务器上运行Java应用有一个内存注意点默认JVM堆内存是物理机器的四分之一如果服务器内存只有2G一个SpringBoot应用就可能吃掉500M内存。可以用启动参数限制java -Xms256m -Xmx512m -jar hospital.jar-Xms设置初始堆大小-Xmx设置最大堆大小避免JVM抢占过多服务器内存。6. 扩展方向与二次开发建议做完这套系统如果还想继续深入有几个方向值得花时间第一把挂号模块升级成“分时段预约线上支付”。现在这套是预约后线下缴费如果想接微信支付、支付宝支付挂号的业务链路会重构一遍。重点是学习支付回调、订单状态机、对账这三个环节比单纯写增删改查练习含金量高得多。第二加入Redis做缓存。科室列表、排班列表这些热点数据完全可以用Redis做缓存缓存失效时间五到十分钟热点数据查询不走MySQL数据库压力直线下降。这里要学的是缓存穿透、缓存击穿、缓存雪崩三个经典问题的解决方案。第三引入消息队列做放号异步通知。预约成功后给患者发短信通知正常的同步调用也能实现但高峰期系统响应会变慢。引入RabbitMQ或者RocketMQ挂号成功后发一条消息消费者异步处理通知逻辑接口响应时间能减少不少。第四整个系统的前后端分离模式做微服务化改造。把用户服务、挂号服务、排班服务拆成独立服务服务间通过Feign调用配合Nacos做注册发现。这是从单体架构走向微服务的必经之路但工作量会翻倍适合用于学习和面试加分。这套系统说到底是一块很好的“练手砖”麻雀虽小五脏俱全。从数据库设计到接口开发到前端联调再到部署上线全栈里该遇到的问题它基本都有但每个问题又都控制在初学者能够解决的范围内。整套做下来你对SpringBoot自动装配、MyBatis动态SQL、Vue组件通信、前后端联调的理解都会比看视频强很多。项目源码我可以直接把核心部分的代码贴出来讲有些接口逻辑一两句话说不清的直接在源码里看就行。有问题的话评论区帖子下面留言我看到会回。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价