医院挂号系统前后端分离实战:SpringBoot+Vue+MyBatis
前后端分离、SpringBoot、Vue、MyBatis、MySQL这几个词放一起就是现在做管理系统类项目最标准的一套组合拳。这套医院挂号就诊系统我在本地跑通过很多次也给不少卡在环境或者联调环节的同学排查过问题。归纳下来你会发现真正让你痛苦的往往不是业务逻辑本身而是环境配置、数据字段映射、跨域请求、打包路径这些看起来不起眼的细节。这篇文章我会把系统架构、核心表结构、接口定义、数据库初始化、前后端启动、生产部署以及我实际踩过的那些坑全部梳理出来。不管你是准备把它当毕业设计、课程设计参考还是刚接触前后端分离想找一份能完整跑通的源码这份实操笔记应该都能帮你省下不少折腾时间。1. 项目整体设计与技术选型思路1.1 为什么选前后端分离而不是传统 JSP医院挂号系统这种项目早些年最常见的做法是 SpringMVC JSP页面模板放在服务端前端人员写页面还要懂 Java 数据封装Java 开发改一个按钮样式也得跟着重启。这套模式在系统规模小的时候没什么问题一旦牵扯到用户端、管理端两套界面并行迭代大家全挤在一个项目里改文件效率和体验都很糟糕。前后端分离的核心是把数据怎么存、接口怎么给和界面长什么样、交互怎么触发彻底拆成两个工程。前端只关心调用接口、渲染数据后端只关心业务逻辑和数据库读写。开发阶段前端跑 Vue 的 dev server后端跑独立的 SpringBoot 服务两边通过 HTTP JSON 通信改前端不需要重启后端改后端也不需要动页面。这个项目采用这种架构还有一个很现实的原因医院挂号涉及患者注册、科室查询、医生排班、在线挂号、后台管理多条业务线前后端并行开发能明显压缩开发周期而且后期前端打包产物是纯静态文件可以交给 Nginx 或直接放进 SpringBoot 静态目录部署方式非常灵活。1.2 技术栈组合的选型逻辑很多人拿到项目后会问为什么是 SpringBoot Vue MyBatis MySQL而不是其他组合这套选型本质上考虑了学习成本、社区资料和就业市场三个维度。后端用 SpringBoot 是因为它把 Spring 那一套繁琐的 XML 配置几乎全部自动化了。嵌入式 Tomcat 让项目一个命令就能启动starter 依赖开箱即用对于没有太多企业部署经验的开发者来说这就是最低门槛的标准后端框架。前端用 Vue 是因为它的核心是数据驱动视图相比直接操作 DOM 的 jQuery 时代写页面效率高很多配合 Element UI 组件库后台管理界面基本是拼积木式的开发。MyBatis 在这套组合里的角色是数据库访问层它没有完全屏蔽 SQL反而把 SQL 写在 XML 里让你能精准控制每一条查询对挂号系统这种涉及多表关联、条件筛选、事务扣减的场景非常合适。MySQL 则是关系型数据库里的首选稳定、免费、资料多订单、用户、排班这类强关系数据天然适合用表来组织。需要提醒的是这套项目网上版本很多有的用 MyBatis-Plus有的用 Spring Data JPA。选原生 MyBatis 的版本通常更裸你反而能看清一条 SQL 是怎么从 Mapper 接口走到 XML 再返回结果的这对学习阶段来说比什么都重要。1.3 功能模块划分从业务角色出发整个系统可以分成两个端端角色核心功能患者端普通用户注册登录、浏览科室、查看医生、选择排班与时间段挂号、查看我的挂号记录、取消挂号管理端管理员登录、科室信息维护、医生信息管理、排班管理、挂号订单查询与状态更新这看起来功能不多但每一个模块背后都有一整套接口、表、前端页面在支撑。比如在线挂号这个功能前端有选医生、选日期的预约页面后端有排班查询接口、余号判断逻辑、订单创建接口数据库里对应排班表和订单表还要在事务里完成余号扣减否则多人同时挂号会出现超卖。真正把这种小功能大链路走通才是这个项目最大的价值。2. 核心细节解析与实操要点2.1 数据库设计核心表的关联关系一个相对完整的挂号系统至少需要这几张核心表用户表、科室表、医生表、排班表、挂号订单表。它们的关系是这样的科室一对多医生医生一对多排班记录排班记录决定某一天某个时段有多少个号挂号订单记录用户实际购买了哪个排班的哪个号。用户表除了存放用户名、密码、手机号之外建议加一个角色字段比如role用 0 表示患者、1 表示管理员这样后端接口就可以通过角色做权限控制不额外建复杂的三张关联权限表课程项目完全够用。挂号订单表是整个系统设计的关键CREATE TABLE registration_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id int(11) NOT NULL COMMENT 挂号用户ID, doctor_id int(11) NOT NULL COMMENT 医生ID, schedule_id int(11) NOT NULL COMMENT 排班ID, visit_date date NOT NULL COMMENT 就诊日期, time_slot varchar(20) DEFAULT NULL COMMENT 时间段, order_status int(1) DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT挂号订单表;订单编号要保证唯一常用做法是日期字符串加随机数或者直接用 UUID 去掉横线。就诊日期和排班 ID 同时冗余在订单表里是为了查询我的挂号记录时少联两张表属于典型的空间换性能做法。2.2 后端分层与接口设计规范后端代码建议严格按 Controller、Service、Mapper 三层来组织。Controller 只负责接参数、调 Service、返回结果不写业务逻辑Service 层承载核心业务比如挂号时的余号校验、事务控制Mapper 层只有接口和 XML 或注解 SQL不出现业务判断。这样拆的好处是排错时能快速定位无论是应聘面试还是答辩被问到项目结构时也能讲得清楚。所有接口统一返回一个 Result 对象结构固定为{ code: 200, msg: 成功, data: {...} }。前端不用为每个接口单独处理返回格式只需要在 axios 响应拦截器里判断 code 即可。接口路径按模块设计模块接口示例说明用户POST /api/user/login登录返回 token 和用户信息用户POST /api/user/register注册校验用户名唯一科室GET /api/department/list科室列表医生GET /api/doctor/list?departmentId按科室查医生排班GET /api/schedule/list?doctorIddate按医生和日期查排班挂号POST /api/order/create创建挂号订单事务扣减余号挂号GET /api/order/my查询当前用户挂号记录挂号PUT /api/order/cancel/{id}取消挂号回补余号登录认证建议用 JWT。前后端分离之后Session 依赖浏览器的 Cookie 和 Tomcat 内存非常不好跨端使用JWT 把用户信息加密放在 token 里后端只需要校验签名天然适合这种分布式部署。后端加一个拦截器从请求头Authorization里取 token校验通过后把用户信息放进 ThreadLocalController 直接取当前登录用户即可。2.3 前端工程与路由设计前端部分虽然看起来只是几个页面但工程结构如果不规范后期改起来会很痛苦。推荐的路由规划是把患者端和管理端分成两个布局患者端是普通页面包含首页、科室列表、医生列表、挂号、我的订单管理端是带侧边栏的布局路由嵌套在 AdminLayout 下统一处理登录状态。Vue Router 要配置全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })axios 封装一定要做两件事请求拦截器把 token 加到 headers响应拦截器统一处理 code 非 200 的情况。比如 token 过期时后端返回 401前端拦截器直接清掉 localStorage 并跳到登录页这样不用在每个页面里重复写错误处理逻辑。2.4 前后端联调的两种方式开发阶段最常见的联调方式是配置 Vue 的 devServer 代理。前端开发服务器跑在 8080后端跑在 8081前端请求/api/login时通过代理转发到http://localhost:8081/api/login。这样做的好处是浏览器里请求路径和接口路径一致不会出现跨域报错也不需要在后端单独配置 CORS。配置在vue.config.js里module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }如果后端不做处理直接跨域调用需要在 SpringBoot 里加一个 CorsFilter允许指定来源携带凭证访问。我的建议是开发期用代理生产期如果前后端分离部署用 Nginx 反代尽量少在两套环境下都依赖 CORS排查问题会少很多干扰因素。3. 实操过程与部署教程3.1 环境准备与版本选择这是整个项目最容易出问题的一步。我的建议版本组合是JDK 1.8、Maven 3.6.3、Node.js 16、MySQL 8.0、IDEA后端 VSCode前端。为什么强调版本SpringBoot 2.x 基于 JDK 8 就够SpringBoot 3.x 强制要求 JDK 17很多课程项目的源码又是旧写法升级之后启动直接报错。Node 18 以上的版本跑旧版 Vue 项目时node-sass编译经常失败Node 16 兼容性好很多。MySQL 这里需要多说一句5.7 和 8.0 在连接驱动上有差异。8.0 的驱动类名是com.mysql.cj.jdbc.Driver而且要求 URL 里必须带时区参数否则报Server returns invalid timezone。建议统一用 MySQL 8.0连接参数直接照下面配置写。3.2 数据库初始化拿到源码后第一步不是启动项目而是先把数据库建好。用 Navicat 或命令行执行项目中提供的hospital.sqlmysql -u root -p hospital.sql执行完成后确认数据库hospital_db下面有 5 张以上的表并且有初始数据比如管理员账号、科室数据、部分医生数据。如果没有初始数据后面前端页面打开是空白的你根本分不清是接口问题还是数据库问题。然后修改后端application.yml里的数据库连接server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置特别关键。它把数据库下划线字段order_no自动映射成实体类的orderNo驼峰属性没有这行配置你会发现查询出来一堆字段是 null根本跑不起来。3.3 启动后端服务用 IDEA 打开后端工程等 Maven 把依赖下载完直接运行启动类里的 main 方法。如果命令行方式在工程根目录执行mvn spring-boot:run启动成功的标志是控制台出现 Spring Boot 启动日志并且没有报数据源连接错误。可以用浏览器访问http://localhost:8081/api/department/list测试接口如果中间件拦截了未授权请求可以先放行该接口或者用 Postman 加 token 测试。3.4 启动前端工程用 VSCode 或 IDEA 打开前端目录先装依赖npm install如果网络不好可以使用国内镜像源npm config set registry https://registry.npmmirror.com再执行npm run serve等待编译完成后浏览器打开http://localhost:8080正常会显示登录页面。用项目初始化的管理员账号登录管理端或者注册一个患者账号走一遍挂号流程能看到完整数据流转即可。3.5 生产环境打包与两种部署方案开发跑通之后部署方式有两种选择。第一种是单应用合并部署前端执行npm run build把生成的dist目录里的静态资源全部复制到 SpringBoot 的src/main/resources/static下然后重新打包后端mvn clean package -DskipTests java -jar target/hospital.jar这样访问http://localhost:8081就能同时看到页面和接口省去单独部署前端的成本。但要注意前端打包时接口地址不能再写http://localhost:8081要用相对路径/api否则部署后因为端口不同而跨域。第二种是分离部署前端dist目录放到 Nginx 的 html 目录Nginx 监听 80 端口后端 jar 包跑在 8081Nginx 把/api开头的请求反代到后端server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html这一行一定要写否则 Vue 使用 history 路由模式时刷新子页面会报 404。这是很多人在部署阶段卡住最久的问题。4. 常见问题与排查技巧实录4.1 后端接口通了前端页面一片空白这类问题九成出在 Vue 打包后的资源路径上。默认publicPath是/如果你把 dist 文件放在某个子目录或者访问端口不对js 和 css 资源就加载不出来。解决办法是在vue.config.js里按部署环境调整module.exports { publicPath: process.env.NODE_ENV production ? ./ : /, // 其他配置 }改成相对路径./之后可以解决大部分子目录部署时的静态资源加载问题。4.2 请求接口报跨域错误跨域分两种情况。本地开发联调时优先检查 vue.config.js 的 proxy 配置是否生效改了文件要重启 npm run serve。生产环境如果前后端分离部署检查 Nginx 的 location 是否有代理到后端真实端口别让前端直接请求 8081否则浏览器会以http://你的域名为来源向http://你的ip:8081发跨域请求。如果后端确实需要开启 CORS注意别用CrossOrigin一个个加注释而是写一个全局配置类统一放行。同时在网关或 Nginx 层面还要处理 OPTIONS 预检请求否则部分浏览器会拦截。4.3 查询结果某些字段为 null这个是 MyBatis 项目最经典的坑。第一种原因是没有开启驼峰映射order_no映射不到orderNo。第二种原因是实体类字段名和数据库列名不一致尤其是 Java 类用了isDelete这种类型和数据库is_delete映射时容易出问题。第三种原因是 XML 里用了if testxxx ! null但参数名写错导致条件被绕过去SQL 没有完全执行。排查思路很简单打开 MyBatis 的 SQL 日志看看实际执行的 SQL 是什么再跟预期的对比。mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl加上这个配置后控制台会打印每条 SQL 和参数排查效率会提高很多。遇到条件不生效尤其是if teststatus ! 这种写法时要注意如果 status 是 Integer 类型空字符串判断会出现类型误判建议严格用! null配合默认值处理。4.4 MySQL 连接启动报错最常遇到的两个错一个是Public Key Retrieval is not allowed因为 MySQL 8.0 默认用 caching_sha2_password 认证连接时要在 URL 后面加allowPublicKeyRetrievaltrue。另一个是The server time zone value Öйú±ê׼ʱ¼ä这是时区乱码在 URL 后加serverTimezoneAsia/Shanghai即可。还有一类问题是本机之前装过 MySQL 5.7卸载不干净导致服务启动失败。Windows 上建议用管理员权限彻底卸载服务清理注册表和残留数据目录再重新初始化 8.0。注意数据目录初始化命令要指定--initialize-insecure不然 root 密码随机生成会让人找半天。4.5 SpringBoot 版本过高带来的连锁问题网上很多教程写的代码基于 SpringBoot 2.x但 Maven 仓库默认下载最新版 3.x启动时报错说找不到javax包或者需要 JDK 17。如果源码不是按 Jakarta 命名空间写的可以在pom.xml里把版本锁定为2.7.18同时确保 JDK 用 1.8。这个坑尤其容易出现在新手贪新、默认升级到最新依赖的时候。锁版本之后mvn clean再重新 compile基本能恢复。另外SpringBoot 版本太高还会影响内置 Tomcat 的版本不同版本对 HTTP 请求格式的容忍度不一样有时候本地旧接口在升级后突然返回 400多半也是版本差异造成的。5. 一些我觉得值得记住的实践经验挂号和退号的并发处理是这个项目里最值得深挖的地方。我在实测时用两个浏览器同时登录两个账号怼同一个排班如果创建订单的 Service 方法没有加事务控制会出现余号变负数的情况。正确做法是在事务里先SELECT ... FOR UPDATE锁住排班记录或者用乐观锁版本号再判断remain 0才生成订单。这个细节面试官经常问也是系统真正从能跑走向可靠的关键一步。排查问题时我的习惯是先看后端日志再看前端 Network 面板最后才动代码。很多突发问题并没有想象中复杂找到第一次出现的时间点往回追溯往往就是一条 SQL 或一个字段的问题。调试前后端分离项目时浏览器开发者工具的 Network 面板加上后端 SQL 日志这两样组合起来能覆盖九成以上的 Bug 场景。这套项目后续还可以扩展的方向很多比如加入医生二维码签到、短信通知、预约时间段的号源池、数据统计报表。把一套前后端分离项目完整跑通意义不在于学会了几个框架的语法而是理解了从数据库表设计、接口约定到前端页面渲染这条完整链路是如何一环扣一环工作的。后面你再去做任何管理系统类的项目骨架都是相通的无非是换了一套业务字段和几条查询 SQL 而已。