资讯动态

医院挂号系统全栈开发实战:SpringBoot+Vue+MySQL从设计到部署

发布时间:2026/10/16 1:43:12 来源:尧图企业网站定制
我今年早些时候接手了一套医院挂号就诊系统的二次开发技术栈正好是 SpringBoot Vue MyBatis MySQL 这套前后端分离的组合。拿到手的第一反应是项目结构还算清爽但真要把文档之外的设计意图摸透还是花了不少功夫。借着这次整理我把这套系统从需求拆解、表设计、核心接口逻辑到最终部署的完整过程梳理了一遍希望对还在纠结“挂号系统到底怎么做”的朋友有点实际帮助。这套系统覆盖了患者端和后台管理端的常见业务场景用户注册登录、科室医生查询、号源排班、在线挂号、退号、就诊状态流转以及管理端的排班设置、号源管控、患者管理等功能。适合正在学习 SpringBootVue 全栈开发的学生也适合需要快速交付类似业务系统的中小项目团队。下面我按自己的理解把整个项目的关键环节逐个拆开讲。1. 为什么把医院挂号系统做成前后端分离先说一个很多人容易忽略的点挂号系统这类业务系统用户端和管理端的使用场景差异很大前者要求页面轻、操作快、移动端友好后者更看重信息密度和批量操作效率。前后端分离正好能让两端独立演进把接口层做成稳定的契约前端想怎么改交互都不影响后端逻辑。1.1 挂号场景的痛点决定了技术选型医院挂号这个场景有很强的时效性。早高峰号源更新频繁患者反复刷新页面后端接口要承受不小的查询压力。而号源一旦被抢数据必须立刻扣减否则就会超卖。这类业务对接口响应速度和数据一致性都有要求但并发量又没到需要上消息队列、分库分表的程度所以传统单体加关系型数据库的组合其实最合适。我在选型时参考了这套系统的做法后端采用 SpringBoot 的经典分层架构Controller - Service - Mapper用 MyBatis 作为数据库映射层前端用 Vue 配合 Vue Router 和 Axios 做单页应用。数据库使用 MySQL存储过程、视图这类高级功能基本没用靠合理的表结构和事务就能满足需求。整体成本低部署简单团队上手也快。1.2 一套系统拆解出的业务模块与用户角色在动手看代码之前先把业务角色理清楚是关键。这套系统里主要有三种角色患者普通用户注册登录后可以浏览科室、查询医生排班、预约挂号、取消挂号、查看就诊记录。医生业务用户可以查看自己某天的排班看到挂号患者列表并更新就诊状态比如“待就诊”变成“已完成”。系统管理员运维用户负责维护科室、医生、排班和号源数量也可以处理异常号单。从功能模块看可以简单拆成“基础数据管理”“排班管理”“挂号业务”“用户中心”四大块。其中排班和挂号是核心链路管理员先设置某医生某天的号源总数患者才能在这一天的号源池里占一个位置。这里给初学 SpringBoot 的朋友一个建议拿到这种项目源码后先别急着跑起来而是花半天时间把 Controller 里的接口路径全部列出来再对着前端路由表捋一遍业务边界很快就清楚了。这个习惯能让你少走很多弯路。2. 技术栈选型的真实考量不是越火越好很多同学做毕业设计或者项目练手时容易陷入“技术越新越好”的误区恨不得把微服务、分布式事务、Redis 集群全塞进去。但挂号系统这类业务真正考验的是代码规范和边界把控选型必须围绕可维护性、部署成本和学习曲线来做权衡。2.1 Spring Boot 在服务端的定位以及为什么不选微服务Spring Boot 在这套系统里承担的职责很简单提供 RESTful API处理业务逻辑管理数据库事务做参数校验和异常拦截。它对新手很友好内置 Tomcat不用配置一堆 XMLMaven 打包后直接一个 jar 就能跑。为什么不拆微服务因为微服务的核心价值是独立伸缩和故障隔离需要配合服务注册中心、配置中心、网关等一系列基础设施。一套日挂号量几百单到几千单的医院系统拆成十几个微服务只会让部署和排障成本翻倍不会有明显收益。用单体加缓存配合好索引和事务隔离级别性能完全够用。当年把架构做复杂最后坑的都是自己。2.2 Vue MyBatis MySQL 组合的取舍选 Vue 是考虑到生态成熟、中文资料多、组件库丰富。用 Element UI 这类组件库做后台管理端表格和表单处理效率很高。做患者端页面时Vue 的单文件组件也让代码更整洁。MyBatis 在这套系统里用来写 SQL 映射。相比 Spring Data JPAMyBatis 的优点是 SQL 可控性强遇到复杂查询比如排班列表按医生科室多条件动态拼接时用 XML 里的动态 SQL 非常直观。而 MySQL 的选择就没什么好争论的——存储引擎在事务支持上足够可靠普遍使用遇到索引查询、性能调优的问题有大量现成经验可以参考。2.3 目录结构前后端分离的项目长什么样项目拿到手是前后端分离的两个工程后端是一个标准的 Maven 项目包结构大概是这样hospital-server ├── src/main/java │ ├── controller // 接口层 │ ├── service // 业务逻辑层 │ ├── mapper // MyBatis 数据访问接口 │ ├── entity // 数据库实体类 │ ├── dto // 前后端交互对象 │ ├── config // 安全拦截、跨域、全局异常配置 │ └── common // 通用工具类、统一返回码 ├── src/main/resources │ ├── mapper // MyBatis XML 文件 │ ├── application.yml // 数据源、Redis、自定义配置 │ └── sql // 初始建表脚本前端是标准的 Vue CLI 工程hospital-web ├── src │ ├── api // 按模块封装 Axios 请求 │ ├── router // 前端路由 │ ├── store // 登录态与全局状态 │ ├── views // 页面组件 │ └── utils // 请求封装、日期格式化等熟悉这套结构之后新增一个功能模块的路子就很清晰后端在 mapper 加接口和 XMLservice 写业务逻辑controller 暴露 REST 接口前端在 api 里加方法在 views 里加页面在 router 里注册路由。一前一后两头补互相不干扰。3. 数据库表设计挂号系统的地基怎么打表设计直接决定后续业务逻辑的复杂程度。这套项目的建表脚本写得中规中矩但我看了之后也发现一些可以优化的地方。我按实际开发经验把核心表结构和设计要点重新整理一下。3.1 核心表结构与字段说明挂号系统的基础表大概有这六张表名作用关键字段user用户表id, username, password, real_name, id_card, roledepartment科室表id, dept_name, dept_desc, statusdoctor医生表id, doctor_name, dept_id, professional, avatar, introschedule排班表id, doctor_id, schedule_date, total_amount, remain_amount, statusappointment挂号单表id, user_id, schedule_id, visit_date, status, create_timepatient_record就诊记录表id, appointment_id, doctor_id, user_id, diagnosis, prescription排班表承担了“号源池”的角色。我在理解它的时候喜欢把它比作一张电影票存量表每个医生在某一天有几张票total_amount卖出去一张就减掉一张余票remain_amount。挂号单表则是“卖出去的票”记录了哪个患者在哪个排班下占了一个号。状态字段在表里到处都是这是业务系统的常态。挂号单状态可以包括待就诊、已完成、已取消。排班状态包括正常、停诊。这些状态要在后端统一用常量管理千万别散落在 SQL 里硬编码数字。3.2 号源扣减的并发方案用乐观锁而不是分布式锁我刚开始看这个系统的时候重点关注的是“号源扣减”这个并发敏感操作。很多教学项目在扣减余票时直接用update schedule set remain_amount remain_amount - 1 where id ?这样写在小流量下没问题但在高并发场景下两个请求同时读到余票为 1两个都执行减一余票就变成 -1也就是超卖了。这套系统里采用了乐观锁的经典做法在排班表里不依赖额外版本字段而是用remain_amount本身做条件约束。扣减号源时的 SQL 大致长这样update schedule set remain_amount remain_amount - 1 where id #{scheduleId} and remain_amount 0执行完 update 之后通过判断受影响行数来确认是否扣减成功int rows scheduleMapper.decreaseRemainAmount(scheduleId); if (rows 0) { // 说明余票不足或已无号抛出业务异常 throw new BusinessException(该时段号源已挂完); }这种做法比先 select 再 update 安全得多因为 update 本身是原子操作配合remain_amount 0的条件就能防止扣成负数。业务量再大一点的场景可以给 schedule 表加一个乐观锁版本号字段或者引入 Redis 的 Lua 脚本做原子扣减但当前这套系统用数据库条件更新已经足够稳健。如果您自己动手改这个项目我的建议是先保留条件更新方案理解它的作用后再考虑分布式缓存避免在一开始就引入多余的基础组件。3.3 时间与状态的字段设计踩坑在表设计上最容易出问题的是日期时间字段建议统一用 datetime 或 date 类型。比如排班日期用 date挂号单创建时间用 datetime。同时有一个很常见的经验字段默认值要设计合理比如挂号单表创建时间可以设置DEFAULT CURRENT_TIMESTAMP这样就减少一条后端的赋值代码。另外保存密码的时候需要注意系统保存的是 BCrypt 加密后的密码串而不是明文。在用户表里password的长度需要设置为 60 或以上否则 BCrypt 生成的字符串会截断届时登录永远报密码错误这类问题排查起来非常费时间。4. 开发中最容易翻车的几个地方代码看下来系统的整体逻辑不算复杂但有几个点是实际开发中经常踩坑的。我把这些细节挑出来一个一个说清楚。4.1 跨域问题与 Axios 拦截器前后端分离之后最直观的问题就是跨域。前端跑在 8080 端口后端跑在 8081 端口如果不做处理浏览器会拦截响应页面上看着请求失败但服务端日志却能正常接收请求排查起来很头疼。这套项目的做法是在后端加一个跨域配置类实现WebMvcConfigurer接口重写addCorsMappings方法指定允许的来源、请求头和请求方法Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }另外前端在封装 Axios 时配置了axios.interceptors.request.use在请求头里自动带上 token同时用response拦截器统一处理后端返回的 401 状态码。这样做的好处是每个页面调用 API 时不需要手动处理 token 携带和错误提示代码重复度大大降低。4.2 MyBatis 动态 SQL 和病案模糊查询在服务端的 Mapper 层最核心的技术点是动态 SQL。比如在管理端查询挂号单列表的时候需要支持按日期、科室、医生姓名、患者姓名等多条件组合过滤。如果每写一个条件都去改 SQL那维护成本非常高。MyBatis 的where和if标签在这里非常合适select idselectAppointmentList resultTypecom.example.vo.AppointmentVO select a.*, d.dept_name, doc.doctor_name, u.real_name from appointment a left join schedule s on a.schedule_id s.id left join doctor doc on s.doctor_id doc.id left join department d on doc.dept_id d.id left join user u on a.user_id u.id where if testdeptId ! null and deptId ! and d.id #{deptId} /if if testdoctorName ! null and doctorName ! and doc.doctor_name like concat(%, #{doctorName}, %) /if if testvisitDate ! null and a.visit_date #{visitDate} /if if teststatus ! null and a.status #{status} /if /where order by a.create_time desc /select这个动态拼接的思路很重要where标签会自动去掉第一个条件的 and避免“where 后面直接跟 and”这种低级 SQL 错误。我还建议在关联查询时统一用 left join 而不是在 where 里隐式内连接这样能让关联关系更明确避免漏查无排班的医生数据。4.3 JWT 登录态与权限控制系统的登录态用的是 JWTJSON Web Token。用户在登录接口校验账号密码成功后后端会签发一个 token里面携带用户 id、用户名、角色这些非敏感信息前端把它存到 localStorage 中每次请求带上。在服务端这套系统用拦截器拦截非白名单接口通过解析 token 校验用户是否登录。权限控制上稍微简陋一些我看到的版本里主要是靠拦截器判断角色是否匹配。比如后台管理接口要求管理员角色如果普通用户去访问管理端接口拦截器就返回 403。我在重构这类逻辑的时候习惯用自定义注解加 HandlerInterceptor 的方式做细粒度权限控制也算给这套系统预留了扩展点。比如定义RequireRole(ADMIN)注解在 interceptor 里解析比在代码里写死角色判断更优雅。4.4 退号与状态流转的边界处理在门诊挂号场景中用户有退号需求。退号这个操作最大的坑点是用户已经退号排班表的余票必须恢复但又要防止重复退号导致余票被重复加一。这套系统处理得比较稳妥退号时先更新挂号单状态再更新排班余票Transactional(rollbackFor Exception.class) public void cancelAppointment(Long appointmentId, Long userId) { // 1. 校验挂号单存在且归属当前用户 Appointment appointment appointmentMapper.selectById(appointmentId); if (appointment null || !appointment.getUserId().equals(userId)) { throw new BusinessException(挂号单不存在或无权操作); } // 2. 校验状态必须为待就诊 if (!StatusEnum.WAITING.getValue().equals(appointment.getStatus())) { throw new BusinessException(当前状态不允许退号); } // 3. 更新挂号单状态 int updateRows appointmentMapper.updateStatusById(appointmentId, StatusEnum.WAITING.getValue(), StatusEnum.CANCELED.getValue()); if (updateRows 0) { throw new BusinessException(退号失败请刷新后重试); } // 4. 恢复排班余票 scheduleMapper.increaseRemainAmount(appointment.getScheduleId()); }这里有两个关键细节第一更新挂号单状态时 SQL 里要带上旧状态条件where status WAITING这是防止重复退号的另一种乐观锁思路第二整个方法加上Transactional保证状态更新和余票恢复在同一个事务里要么都成功要么都回滚。我用一个粗浅的比喻来解释就是退票时你先在系统里把票作废同时现场门口又放出一个座位这两个动作必须同时发生不能只做一半。5. 从源码到线上部署的完整过程最后说说部署。很多学生在本地跑通项目很开心到了服务器上就懵了。这套系统的部署分成三个环节后端打包、前端打包、反向代理配置。把这三个环节理清楚部署就成功了一大半。5.1 打包前后的准备工作后端打包前先检查application.yml里的数据库配置是否指向生产环境。建议把配置拆成多环境模式比如application-dev.yml和application-prod.yml用启动参数区分环境。打包命令很简单mvn clean package -DskipTests构建完成后target 目录下会生成一个 jar 包用以下命令启动java -jar hospital-server-1.0.0.jar --spring.profiles.activeprod服务启动后先用 Postman 或 curl 测试基础接口是否返回正常。再检查日志中是否报数据库连接错误、端口占用等问题。把后端先跑通再处理前端这个顺序不要反。前端打包前需要确认src/utils/request.js里 axios 的 baseURL 是指向服务器地址而不是本地 localhost。不要直接把本地代码打包后再手动改 dist 文件思路是改完配置再打包npm install npm run build执行完成后项目根目录下会生成dist文件夹里面就是需要部署到 Nginx 的静态资源。5.2 Nginx 反向代理与前端 history 路由支持前后端分离项目部署到生产环境时通常不会直接暴露后端端口而是用 Nginx 做反向代理。系统有两个地址需要处理前端页面监听 80 端口访问路径直接指向 root 目录下的 dist 文件。后端接口接口路径统一以/api开头客户端请求到达 Nginx 后转发到后端服务的 8081 端口。Nginx 配置案例如下server { listen 80; server_name your-domain.com; # 前端静态资源 root /usr/share/nginx/html/dist; index index.html; # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 解决前端 history 路由刷新 404 问题 location / { try_files $uri $uri/ /index.html; } }其中try_files $uri $uri/ /index.html;是对付 Vue Router history 模式的关键配置。前端路由在浏览器里看起来是真实的路径比如/schedule但刷新时会向 Nginx 请求这个路径Nginx 发现没有这个静态文件就会 404。加了 try_files 后只要找不到文件就回退到 index.html由前端路由接管。5.3 部署后常见的故障排查部署完之后我习惯做一轮冒烟测试把用户注册、登录、查询医生、预约挂号全流程走一遍。这里把比较容易出问题的几个点列出来遇到类似情况可以少走弯路现象根因解决办法前端白屏打包时 baseURL 指向错误检查 axios baseURL 与 Nginx/api转发路径登录后刷新页面丢失状态token 存储方式与路由守卫逻辑冲突检查路由守卫是否从 localStorage 读取 token接口能通但页面报 CORS 错误开发环境跨域配置覆盖生产配置生产环境优先走 Nginx 同源代理不要依赖后端 CORS数据库连接数打满连接池配置过小或连接泄漏增加maximum-pool-size排查代码中未关闭的 Connection上传图片无法显示静态资源未映射到物理路径Nginx 配置/upload/路径别名到服务器目录这个排查表是从实际部署中总结出来的。前几天一个朋友在自己的服务器上部署类似的系统折腾了一天发现是 Nginxlocation /api/的 proxy_pass 少了尾部斜杠导致接口路径多了一层前缀后端一直接收不到正确请求。这类问题看错误日志比盲猜有效得多。部署这块我再多写一点自己的习惯每次发布前先用nohup配合重定向日志文件把后端进程放到后台避免终端窗口关闭导致进程被终止。等到系统稳定以后再考虑用 systemd 管理服务这样开机自启和统一日志管理都会方便很多。nohup java -jar hospital-server-1.0.0.jar \ --spring.profiles.activeprod \ /data/logs/hospital.log 21 检查进程状态就用ps -ef | grep java确认 jar 包进程存在后再去看日志确认没有异常堆栈。整套系统走到这里核心链路已经全部打通。回想起来最让我觉得有价值的地方倒不是用了多前沿的技术而是把业务边界、并发安全和部署流程真正打通了。医院挂号系统这个业务模型本身非常适合做全栈练手麻雀虽小五脏俱全——有用户体系、有权限控制、有并发敏感操作还有前端交互和后端管理的完整闭环。如果你正在拿这套系统做课程设计或者项目积累我的建议是不要停留在把代码跑起来这一步。试着改动一个业务逻辑比如加一个“黑名单患者一天内最多挂三科”的规则再把流程从数据库到前端页面完整走一遍你会发现从“能跑”到“能改”之间隔着很多值得亲手踩一遍的东西。

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

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

免费获取报价 →
↑