前后端分离、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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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?doctorId=&date= | 按医生和日期查排班 |
| 挂号 | POST /api/order/create | 创建挂号订单,事务扣减余号 |
| 挂号 | GET /api/order/my | 查询当前用户挂号记录 |
| 挂号 | PUT /api/order/cancel/{id} | 取消挂号,回补余号 |
登录认证建议用 JWT。前后端分离之后,Session 依赖浏览器的 Cookie 和 Tomcat 内存,非常不好跨端使用,JWT 把用户信息加密放在 token 里,后端只需要校验签名,天然适合这种分布式部署。后端加一个拦截器,从请求头Authorization里取 token,校验通过后把用户信息放进 ThreadLocal,Controller 直接取当前登录用户即可。
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.sql:
mysql -u root -p < hospital.sql执行完成后确认数据库hospital_db下面有 5 张以上的表,并且有初始数据,比如管理员账号、科室数据、部分医生数据。如果没有初始数据,后面前端页面打开是空白的,你根本分不清是接口问题还是数据库问题。
然后修改后端application.yml里的数据库连接:
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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 包跑在 8081,Nginx 把/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 test="xxx != null">但参数名写错,导致条件被绕过去,SQL 没有完全执行。排查思路很简单:打开 MyBatis 的 SQL 日志,看看实际执行的 SQL 是什么,再跟预期的对比。
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl加上这个配置后,控制台会打印每条 SQL 和参数,排查效率会提高很多。遇到条件不生效,尤其是<if test="status != ''">这种写法时要注意,如果 status 是 Integer 类型,空字符串判断会出现类型误判,建议严格用!= null配合默认值处理。
4.4 MySQL 连接启动报错
最常遇到的两个错:一个是Public Key Retrieval is not allowed,因为 MySQL 8.0 默认用 caching_sha2_password 认证,连接时要在 URL 后面加allowPublicKeyRetrieval=true。另一个是The server time zone value 'Öйú±ê׼ʱ¼ä',这是时区乱码,在 URL 后加serverTimezone=Asia/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 而已。