医疗挂号管理系统源码详解:SpringBoot+Vue3+MyBatis实现

发布时间:2026/9/15 9:16:11
医疗挂号管理系统源码详解:SpringBoot+Vue3+MyBatis实现 做医疗挂号管理系统光听名字就知道是个典型的管理系统科室、医生、号源、挂号、退号、统计全是最常见的CRUD但真正落地时最烧脑的不是写增删改查而是怎么把业务状态机理清楚、怎么在并发挂号场景下不超号。这套基于 SpringBoot Vue3 MyBatis MySQL 的医疗挂号管理系统源码采用了前后端分离架构后端提供 RESTful 接口前端用 Vue3 单页应用承载业务页面数据库用 MySQL 存储业务数据。它解决的问题很具体让患者可以在线查看科室、医生排班、选择号源并完成挂号让医院管理人员可以维护科室、医生、排班、号源和基础用户数据还能从挂号记录里拉出统计报表。适合在校学生拿来当毕设项目、Java 开发人员做全栈项目练习也适合中小型门诊做一次真实业务系统的开发样本。我拿到一套源码时一般不会急着往上加功能而是先把它拆开理解每个模块存在的理由。这样后面改源码、二次开发、甚至重构才不会改一处崩三处。下面我就按自己的理解把这套系统的技术选型、架构设计、核心业务实现、本地部署步骤和常见问题完整过一遍。1. 项目定位与技术选型为什么是这套组合1.1 系统功能清单与目标用户医疗挂号管理系统面向三种角色管理员、医生、患者。管理员负责基础数据维护比如科室管理、医生管理、排班管理、号源设置、用户管理还有每日挂号量和科室就诊量的统计。医生登录后可以看自己的排班查看自己某个时间段内挂了多少号也可以根据业务需要调整个人简介或部分排班信息。患者登录后可以浏览科室列表、医生列表查看医生排班和剩余号源然后在线挂号也可以在我的挂号单里取消尚未就诊的号。这三类角色对应的功能并不是互相独立的它们围绕同一条业务主线展开科室维护 - 医生维护 - 排班生成 - 号源扣减 - 挂号记录生成。源码里的菜单、接口、表结构基本都是沿着这条主线设计出来的。理解了这个主线后面看任何代码都不会迷路。从适用范围来说这套系统不只适用于医院社区卫生服务中心、体检中心、口腔诊所、宠物医院这类需要“按医生/按时间段预约”的场景也基本能直接复用。把科室改成“体检套餐”、医生改成“体检医师”挂号改成“预约登记”核心流程就平移过去了。这也是我希望你在看源码时重点研究的点不要把它当成一个只能跑在医院的程序而要把它当成一套“资源预约系统”去理解。1.2 技术栈分工与选型理由后端用 Java SpringBoot这是目前国内中小型管理系统最主流的技术栈。SpringBoot 解决了传统 Spring 项目里繁重的 XML 配置问题内嵌 Tomcat打成 jar 包就能跑接口开发效率很高。MyBatis 作为持久层框架核心优势是把 SQL 控制权完全交给你尤其适合这种业务规则多、查询条件动态变化的系统。比如“按科室、按日期、按医生名称、按号源状态”组合查询MyBatis 的动态 SQL 写起来很直观SQL 调优也方便不像 JPA 那样会在复杂查询上绕弯子。前端选择 Vue3 而不是 Vue2一方面是 Vue3 的组合式 API 让组件逻辑复用更清晰另一方面是现在 Element Plus、Vite 等周边生态已经非常成熟。Vue3 Vite 的开发体验比 Vue2 Webpack 快很多热更新基本秒级对需要频繁调整页面和接口联调的开发场景非常友好。前端状态管理一般用 Pinia路由用 Vue RouterUI 组件库用 Element Plus这三样几乎成了 Vue3 管理系统的标准组合。MySQL 负责持久化理由简单免费、稳定、资料多。这类挂号系统的并发量远没到需要上分布式数据库的程度MySQL 单库单表做好索引完全够用。源码里一般把数据库名命名为hospital_registration或者medical_registration字符集建议统一utf8mb4这样才能稳妥地支持中文和生僻字。1.3 前后端分离的落地形态前后端分离不是简单地“前端一套代码、后端一套代码”它意味着两端通过 JSON 接口通信状态通过 Token 维护。后端只做业务逻辑和数据持久化前端负责页面渲染、表单校验、路由跳转。这套系统的典型请求链路是浏览器访问 Vue3 页面 - 前端调用 Axios 发起请求 - 请求经过 Vite 代理或 Nginx - 到达 SpringBoot Controller - Service 处理业务 - MyBatis 操作 MySQL - 返回 JSON - 前端渲染。分离的好处是前后端可以并行开发、独立部署后端接口可以用 Postman 直接测试前端页面可以用 Mock 数据先跑起来。对学习的人来说分离也让职责边界更清晰你不会在一个 JSP 页面里塞满 Java 代码也不会在 Controller 里拼接 HTML。源码中如果已经做好了跨域配置和登录拦截就说明项目在架构上不只是“能用”而是考虑了真正部署时的可维护性。2. 整体架构与核心模块拆解2.1 后端工程结构与分层一套规范的 SpringBoot 工程源码目录一般长这样com.example.hospital ├── config │ ├── CorsConfig.java │ ├── WebMvcConfig.java │ └── MybatisPlusConfig.java如果集成了 MyBatis-Plus ├── controller │ ├── DeptController.java │ ├── DoctorController.java │ ├── ScheduleController.java │ ├── AppointmentController.java │ └── AuthController.java ├── service │ ├── DeptService.java │ ├── DoctorService.java │ ├── ScheduleService.java │ ├── AppointmentService.java │ └── impl ├── mapper │ ├── DeptMapper.java │ ├── DoctorMapper.java │ ├── ScheduleMapper.java │ └── AppointmentMapper.java ├── entity │ ├── SysUser.java │ ├── Department.java │ ├── Doctor.java │ ├── Schedule.java │ └── Appointment.java ├── common │ ├── Result.java │ ├── ResultCode.java │ └── BusinessException.java ├── interceptor │ └── JwtInterceptor.java ├── utils │ ├── JwtUtils.java │ └── PasswordUtils.java └── HospitalApplication.java分层上没有强求“Service 必须拆接口和实现”但在稍微复杂的业务里接口 impl 是惯例。Controller 只负责参数接收和结果封装Service 处理业务规则Mapper 只做数据访问。实体类与数据库表字段基本一一对应常见字段比如create_time、update_time会统一处理。Result是统一返回体一般包含 code、message、data 三个字段前端 Axios 拦截器也是根据这个结构判断请求是否成功。2.2 核心业务链路从挂哪个科到号源落库这套系统的核心链路我建议你反复看科室 - 医生 - 排班 - 号源 - 挂号单。患者的操作路径是登录系统 - 进入科室列表找到对应科室 - 查看该科室下的医生列表 - 选择某位医生 - 查看该医生的排班日期和时段 - 点挂号 - 系统扣减号源 - 生成挂号记录。管理员的操作路径是维护科室信息 - 维护医生并归属到科室 - 为医生创建排班设置总号数和剩余号数 - 查看每日号源消耗。两条路径最后汇聚到appointment表也就是挂号记录表。我在看源码时最关心的是“号源扣减”这一步。很多入门项目会把挂号写成“先 select 剩余号判断大于 0再 update 号源再 insert 挂号”这种写法在单用户测试时没有问题但在真正多用户并发下两个请求同时读到剩余号数为 1就会都通过判断最后把号源减成 -1超挂。这个系统如果做得好会在 update 语句里直接带remaining_number 0条件用数据库的行锁来保证不会超挂。这一点也是面试官最喜欢问的高频考点。2.3 数据库设计要点表结构、字段与索引数据库设计是整个系统的地基表结构设计不合理后面写接口和调排查都会很痛苦。这套系统的核心表一般包括表名核心字段作用sys_userid, username, password, role, real_name, phone登录用户角色区分管理员/医生/患者departmentid, dept_name, dept_desc, status科室信息doctorid, dept_id, doctor_name, title, avatar, intro医生信息关联科室scheduleid, doctor_id, dept_id, schedule_date, start_time, end_time, total_number, remaining_number, status医生排班与号源appointmentid, patient_id, doctor_id, schedule_id, appt_date, time_slot, status, create_time挂号单关联排班和患者字段命名一般用下划线风格比如dept_id、create_time这样配合 MyBatis 的map-underscore-to-camel-case配置可以自动把下划线映射成实体的驼峰属性省去大量手动映射。索引方面我建议至少补上这几条ALTER TABLE doctor ADD INDEX idx_dept_id (dept_id); ALTER TABLE schedule ADD INDEX idx_dept_doctor_date (dept_id, doctor_id, schedule_date); ALTER TABLE appointment ADD INDEX idx_patient_id (patient_id); ALTER TABLE appointment ADD INDEX idx_schedule_id (schedule_id); ALTER TABLE appointment ADD INDEX idx_status (status);原因很简单患者的查询场景一般是“先按科室查医生再按医生查排班日期再按排班查号源”挂号记录查询场景一般是“查某个患者的所有挂号单”或“管理后台按状态筛出所有记录”。没有索引数据量上来之后联表查询和条件筛选会越来越慢。MySQL 在数据量小的时候不明显但索引是必须提前埋好的伏笔。3. 关键功能实现从登录鉴权到挂号扣号3.1 JWT 登录鉴权与角色权限控制挂号系统涉及患者隐私和医院数据不可能让所有人都直接访问接口。这套系统的登录鉴权一般用 JWT 方案用户登录成功后后端生成一个带用户 ID 和角色的 Token 返回给前端前端把它存到 localStorage 或 Pinia 里后续每个请求在请求头带上Authorization: Bearer token后端通过拦截器校验 Token解析出当前用户信息。JWT 工具类的大致写法如下public class JwtUtils { private static final String SECRET 你的签名密钥; private static final long EXPIRE_TIME 2 * 60 * 60 * 1000; public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器要做两件事一是放行登录接口和静态资源二是校验非放行接口的 Token。角色权限可以放在 Controller 的方法上也可以直接在拦截器里判断 URL 前缀。比如/api/admin/**只允许管理员访问/api/doctor/**只允许医生访问。一般不建议在这套系统里直接引入 Spring Security因为配置复杂度会明显上升用 JWT 拦截器 角色判断最贴合中小型管理系统的实际需求。前端部分Vue3 里需要在 Axios 请求拦截器里统一加上 Tokenaxios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })同时用 Vue Router 的全局前置守卫做登录控制router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这套组合写起来简单但已经能覆盖绝大多数业务系统的登录状态管理需求。3.2 号源扣减与防超挂的设计挂号是并发敏感操作最常见的问题是同一个号源被多个患者同时抢到。源码里如果做得规范会用一个带条件的 update 来解决而不是在代码里先查询再更新。Service 层核心逻辑Transactional(rollbackFor Exception.class) public boolean createAppointment(AppointmentDTO dto) { int rows scheduleMapper.decreaseRemaining(dto.getScheduleId()); if (rows 0) { throw new BusinessException(当前号源已挂完请选择其他时段); } Appointment appointment new Appointment(); appointment.setPatientId(dto.getPatientId()); appointment.setDoctorId(dto.getDoctorId()); appointment.setScheduleId(dto.getScheduleId()); appointment.setStatus(已挂号); appointmentMapper.insert(appointment); return true; }对应的 MyBatis XMLupdate iddecreaseRemaining UPDATE schedule SET remaining_number remaining_number - 1 WHERE id #{scheduleId} AND remaining_number 0 /update这里的关键是 MySQL 的UPDATE会对匹配的行加锁两条并发请求同时执行时第二条会等第一条提交后再执行此时remaining_number已经被减掉条件remaining_number 0就可能不成立影响行数为 0于是抛业务异常。这个方案不需要分布式锁也不需要 Redis单库场景下完全是够用的。同时还要在appointment表上加一个唯一约束防止同一个患者对同一个排班重复挂号ALTER TABLE appointment ADD UNIQUE KEY uk_patient_schedule (patient_id, schedule_id);有了这个约束就算前端双击提交、用户刷新后重复提交数据库层也能兜住最后一层防线。取消挂号时也要做相反操作在同一个事务里把挂号单状态改成“已取消”并把对应排班的remaining_number加回去。3.3 Vue3 前端核心页面与接口对接前端我用 Vue3 Vite Element Plus 重构过类似页面体验确实比 Vue2 时代舒服很多。创建项目可以用npm create vitelatest hospital-frontend -- --template vue cd hospital-frontend npm install npm install element-plus axios vue-router pinia页面结构一般包含登录页、首页布局、科室管理、医生管理、排班管理、挂号页面、我的挂号单、统计页面。核心的挂号页面通过接口动态加载数据流程大概是进入页面后onMounted里请求科室列表选择科室后请求该科室下的医生列表选择医生后请求该医生的排班列表排班列表中显示日期、时段、剩余号数点击“挂号”按钮时调用后端接口排班列表的关键展示字段是“剩余号源”这个值来自后端schedule.remaining_number。源码里如果对已满的排班做了禁用按钮处理细节就到位了。更完整的做法是后端在返回列表时额外返回isFull字段前端根据这个字段控制按钮状态而不是在前端拿剩余号数自己判断因为判断逻辑应该以服务端为准。Axios 请求和响应拦截器也是前端联调的关键。响应拦截器一般这样写axios.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常请检查后端服务) return Promise.reject(error) } )统一错误提示最大的好处是后端只要把错误信息放进message前端不用在每个页面都重复写try/catch。4. 实操部署与运行从源码到本地跑起来4.1 环境与版本匹配建议我把这套系统在本地跑起来的经验拆成几步来说先从环境版本说起。不同源码用的版本可能不一样最容易踩坑的是 SpringBoot 版本和 JDK 版本不匹配。组件建议版本说明JDK8 或 11SpringBoot 2.x 用 JDK 8/11 都行Maven3.6后端依赖管理Node.js16.20 或 18Vue3 Vite 对 Node 版本有要求MySQL5.7 或 8.0建议 8.0字符集 utf8mb4IDEIntelliJ IDEA前后端都建议用 IDEA如果你拿到的源码是 SpringBoot 3.x那 JDK 必须用 17 或更高因为 SpringBoot 3 基于 Spring Framework 6底层要求 Jakarta EE 9不再兼容 JDK 8。反过来如果你是 JDK 17 却拿了一套 SpringBoot 2.3 的源码也可能由于旧版依赖和 JDK 版本兼容问题导致启动异常。看源码第一件事先打开pom.xml确认 SpringBoot 版本然后配对应 JDK这个动作能省下很多折腾时间。4.2 MySQL 初始化与数据导入先确保 MySQL 服务已经安装并启动。然后创建一个数据库并导入源码附带的 SQL 文件CREATE DATABASE hospital_registration DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hospital_registration; SOURCE /你的目录/hospital_registration.sql;如果是在 Navicat 或 DataGrip 里操作也可以直接新建数据库后右键运行 SQL 文件。导入完成后重点检查三类表用户表、科室医生表、排班挂号表。源码里一般会预置几个演示账号比如管理员、医生、患者各一个方便启动后直接登录测试。如果 SQL 文件里没有那就需要自己在sys_user表手工插入一条管理员记录密码字段通常存的是 BCrypt 加密后的字符串不要直接写明文。4.3 后端启动配置与 MyBatis 常见设置后端启动前需要修改application.yml里的数据库连接信息server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_registration?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你自己的密码 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.hospital.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里我建议开发阶段一定打开map-underscore-to-camel-case并开启 SQL 日志。开启日志后控制台会打印出每条 SQL 和参数调试业务逻辑时能看到实际执行的语句排查问题效率会高很多这也是很多人容易忽略的一点。数据库连接 URL 里有几个参数值得留意serverTimezoneAsia/Shanghai解决时区问题useSSLfalse避免 MySQL 8 连接时 SSL 警告allowPublicKeyRetrievaltrue解决连接 MySQL 8 时偶尔出现的 public key retrieval 报错。这三个参数我建议直接保留。启动后端时可以在 IDEA 里直接运行主类也可以打包后运行mvn clean package -DskipTests java -jar target/hospital-system-1.0.jar后端启动成功的标志是控制台出现 Tomcat started on port(s): 8080。启动失败时优先看日志里的红色报错大部分情况不是端口被占用就是数据库连接不上。4.4 前端启动与接口代理配置前端进入项目目录先装依赖npm install如果安装速度很慢可以临时切换镜像源或者在项目根目录创建.npmrc文件配置国内镜像。启动开发服务器npm run dev默认访问地址一般是http://localhost:5173/。前后端联调时前端要解决跨域问题。最方便的思路是使用 Vite 的 dev server 代理把/api开头的请求转发到后端8080端口。在vite.config.js中配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/login时Vite 会自动转发到http://localhost:8080/api/login浏览器端看起来是同一个域名跨域问题就绕过去了。如果后端接口路径没有统一/api前缀就需要和后端约定好或者调整代理规则。很多刚接触前后端分离的人在这里反复踩坑明明后端接口能通但前端页面一调就报跨域原因往往就是代理路径和后端实际路径没对齐。5. 常见问题与排查技巧实录5.1 后端接口跨域与 401 登录失效跨域问题最常见的表现是浏览器控制台提示Access-Control-Allow-Origin或者请求状态是CORS error。如果你不走 Vite 代理而是直接用http://localhost:8080请求后端那后端必须配置跨域。SpringBoot 里可以写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }这里有个细节如果开启allowCredentials(true)就不能把allowedOrigins写成*必须写具体域名。写*时浏览器会认为请求不安全依旧报跨域错误。所以我建议直接明确填前端地址开发阶段填http://localhost:5173部署阶段填正式域名。登录失效的问题则通常和 Token 过期时间或者拦截器放行配置有关。如果前端发现接口每隔一段时间就 401先检查JwtUtils里的过期时间是否设置得太短如果某些接口不需要登录却被拦截了则检查拦截器是否只拦截/api/**而不是拦截所有路径。5.2 MyBatis 查询结果字段为 null 或动态 SQL 失效做这套系统时MyBatis 里最容易出的问题有两个。第一个是实体属性有值但数据库字段查出来是 null大多数情况是数据库字段是下划线命名比如doctor_name实体属性是doctorName而 MyBatis 没有开启驼峰映射。加了map-underscore-to-camel-case: true之后大部分问题可以解决。如果遇到多表联查的返回值不是实体类而是自定义 DTO那 XML 里就不能只写resultType通常需要配resultMap做字段映射或者给 SQL 查询字段起与 DTO 属性一致的别名。第二个常见坑是 MyBatis 比较单个数字字符时行为不符合预期。比如 XML 里写if teststatus 1如果status是字符串类型MyBatis/OGNL 可能把1当成字符常量a之类的值去比较导致条件一直不生效。遇到这种情况我的习惯是改成if teststatus ! null and status 1或者直接在 Java 代码里把参数类型转成 Integer再到 XML 里比较。这个细节很偏但一旦遇到就会卡很久搜“mybatis 单个数字字符比较”能搜出一堆同款问题。5.3 Vue3 页面刷新 404 与打包部署本地开发时Vue Router 如果用createWebHistory()在浏览器地址栏直接刷新某个子路由页面可能会出现 404。原因是开发服务器没有把未知路径回退到index.html。Vite 开发环境一般会自动处理但打包后部署到 Nginx 时一定要注意配置location / { try_files $uri $uri/ /index.html; }这样做是让 Nginx 在找不到对应文件时把所有路径交给 Vue Router 处理。如果你不想处理这个配置也可以把 Vue Router 改成createWebHashHistory()地址栏会变成/#/appointment的形式刷新不会 404不过地址看起来不够干净。生产环境我一般建议用 history 模式 Nginx 回退配置这样更规范。打包前端时执行npm run build生成dist目录后把里面的文件放到 Nginx 的html目录下后端 jar 包单独运行或者用 Nginx 反向代理把/api请求转发到后端端口。整个部署链路就是前端静态文件由 Nginx 托管后端接口跑在8080Nginx 负责把/api请求代理过去。这也是前后端分离最常见的生产部署形态。5.4 源码改造与功能扩展建议跑通源码之后如果你还想继续往上加功能我建议优先做这几个方向第一把验证码从普通的图片验证码升级为 Redis 存储的短信验证码这需要引入 Redis但能明显提升预约场景的真实感第二增加挂号费的在线支付模拟在挂号单表里加一个支付状态字段下单后走模拟支付回调第三把管理后台的统计页面做成图表可视化前端用 ECharts后端新增聚合查询接口统计每日挂号量、科室挂号占比、医生工作量等数据。这几个扩展方向都不会破坏原有表结构都是在现有业务链路上加新字段、新接口、新页面。源码最值得学习的地方恰恰是它给你搭好了一套完整的业务骨架你在上面添砖加瓦时能清楚看到每一块砖应该放在哪里。如果你现在正在跑这套系统最后分享一个我调试挂号模块时的土办法打开 MyBatis 的 SQL 日志然后在挂号按钮上操作一次复制控制台打印出来的 update 和 insert 语句直接拿到 Navicat 里手动执行一遍。尤其是update schedule set remaining_number remaining_number - 1 where id ... and remaining_number 0这条手动执行能看到影响行数是 1 还是 0 一目了然。很多时候业务逻辑看起来没问题实际在数据库层执行失败都是靠这种方式发现问题的。用这个办法排查比我盯着代码干想要快得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询