☰
SpringBoot+Vue3+MyBatis实现医院门诊挂号系统
2026/10/14 7:01:01 网站建设 项目流程

医院门诊挂号这个场景,我第一次接触也是帮朋友处理一个很头疼的需求:患者排队时间长、窗口反复问诊信息、医生排班全凭手工表格。当时最直接的解法就是做一个线上挂号系统,把线下排队挪到手机和网页上,患者按科室查医生、看号源、下单预约,医院后台管理排班和订单。这套基于 Java SpringBoot + Vue3 + MyBatis 的门诊在线挂号系统,数据库用的 MySQL,接口和前端页面完全分离,源码结构清楚、拿来就能改能跑。特别适合正在做医疗信息化项目、课程设计,或者想系统过一遍全栈开发流程的朋友。它解决的不仅是“挂上号”这个动作,而是把科室、医生、排班、号源、订单、登录鉴权这一整条链路,用工程化的方式串起来。

1. 项目整体设计与技术选型

1.1 前后端分离不是炫技,是分工的自然结果

我见过很多老系统,JSP 页面里直接写 SQL,现在看维护成本确实太高。这套挂号系统从设计第一天就定死了前后端分离:后端只出 JSON 接口,前端只负责渲染和交互,两者通过 HTTP 通信。好处你实际开发过就会很明显——前端和后端可以同步开工,后端把接口文档先定义好,前端拿 mock 数据先跑页面,不用等对方;上线部署也能分开扩,哪个环节卡了单独处理。

另一个实际好处是权限控制更干净。后端所有接口都走统一的鉴权逻辑,前端根本不需要关心“这个按钮谁能点”,反正没权限的请求会被后端直接拦掉。医院这种场景特别在乎数据安全,把逻辑收敛在后端比前端藏着掖着要稳妥得多。

1.2 SpringBoot + MyBatis + MySQL 这套组合到底合不合理

选 SpringBoot 这件事没什么争议,现在做 Java 后端项目基本都是起步配置。它把 Spring 那一堆繁琐的 XML 配置都收进去了,内嵌 Tomcat,打包成 jar 就能跑。尤其是帮医院做信息化这类中小型项目,不需要微服务那套复杂体系,一个单体 SpringBoot 应用配上够用的模块划分,维护起来最轻松。

MyBatis 常有人问为什么不直接用 JPA。说实话 JPA 对于纯 CRUD 确实省事,但挂号系统里有大量带业务逻辑的 SQL——查剩余号源、锁号、统计科室放号量、查询医生排班列表,这些场景用 MyBatis 写原生 SQL 反而更直白,优化也好做。你可以精确控制每条 SQL 的执行计划,慢查询出现时直接对着 SQL 调整索引就行。MySQL 不用多说,中小型医院门诊量完全扛得住,部署也简单。

1.3 Vue3 对比 Vue2 的实际体验差异

前端我之前用 Vue2 写过类似项目,换到 Vue3 后最大的感受是 Composition API 确实更适合这种业务逻辑集中的场景。挂号流程里有不少跨页面共享的状态——当前选中的医生、排班日期、号源编号、患者信息,用 ref 和 reactive 组织起来,比 Options API 里散落一堆 data、methods、computed 要直观得多。

Vue3 的响应式系统基于 Proxy,性能上也有提升。这个项目还用了 Vite 做构建工具,冷启动速度比我之前用 Webpack 舒服太多,改代码热更新基本是秒级,调试体验明显不一样。

2. 数据库设计与数据模型

2.1 核心表结构拆解

这套系统的表不算多,但每张表之间关联关系比较关键。我把核心的表列出来,先有个整体印象:

表名作用关键字段
user患者账号信息id、username、password、real_name、phone、id_card
doctor医生信息id、name、department_id、title、introduction、avatar
department科室信息id、name、location、description
schedule医生排班id、doctor_id、work_date、time_type、total_slots
order_info挂号订单id、user_id、schedule_id、order_no、status、create_time

user 表没什么特别,注意 password 存的是 BCrypt 加密后的密文,千万别明文存储。doctor 表里的 department_id 关联科室,title 字段存医生的职称,像主任医师、副主任医师这种,前端可以拿这个字段做筛选排序。schedule 表是关键,它记录的是“哪个医生在哪个日期哪个时段出诊”,total_slots 表示这个排班总共放出多少个号。

2.2 号源排班模型的设计心得

一开始我脑子里也闪过一个简单的方案:直接在 schedule 表里存一个 remain_slots 剩余号数字段,患者挂号时把这个数字减 1。但这会有一个问题——同一个排班被多人同时点击时,并发扣减很容易超卖。后来我换了个思路:排班表只存总号数和出诊信息,真正被下单扣减的是另一套号源机制。

实际操作时,我建议在生成排班的同时,往号源明细表里插入对应数量的记录,每条记录就是一个独立的号。患者下单时锁定的是某一条具体的号源记录,而不是把一个数字做减法。这样每个号都有独立状态:0 未占用、1 已锁定、2 已支付。好处是后续如果要做退号、换号、统计放号量,都有迹可循,不会出现“减多了”或者“减错了”查不到原因的情况。

不过为了减少表数量、降低实现成本,也可以把号源做成区间判断,在 order_info 表通过排班 ID 统计已支付订单数,再和 total_slots 做比较。这个方案并发控制要求高一些,但表结构更精简。这个项目我用的是独立订单加条件更新,既保留简单性也避免了超卖。

2.3 字段设计与索引优化的几个细节

数据库字段命名我用的是下划线风格,Java 实体类里配好驼峰映射,MyBatis 写 SQL 时不用做太多转换。时间字段统一用 datetime,不要用 varchar 存日期,否则排序和区间查询会很难受。status 字段用 tinyint 表示,0、1、2 这种含义在代码里定义成枚举常量,比字符串判断快也不容易写错。

索引这块踩过一次坑:订单表查用户的挂号记录时,一开始只给 user_id 加了索引,但用户查询时还可能按状态过滤,结果状态条件筛选的时候走了全表扫描。后来改成联合索引 (user_id, status),查询速度立刻上来了。还有 schedule 表的查询高频场景是“某科室某天有哪些医生出诊”,这种检索条件可以加 (doctor_id, work_date) 联合索引。

3. 后端核心模块实现

3.1 登录鉴权与 JWT 的落地细节

用户登录之后,后端签一个 JWT 返回给前端,前端存在本地存储里,每次请求在请求头带上 token。后端用拦截器统一解析 token,解析失败直接返回 401。这里有个容易忽略的点:JWT 的密钥不要写在代码里硬编码,放到配置文件中,部署时通过环境变量注入,不然源码泄露后任何人都能伪造 token。

实际项目里我还加了一个简单的双端处理逻辑:如果用户之前已经登录过,再次登录签发新 token 后,旧 token 不会立即失效。如果医院要求严格一点,可以引入 token 黑名单机制,把退出或重登后的旧 token 标记失效。基础的挂号系统可以不做这么重,但如果做支付环节,建议还是处理一下。

3.2 排班管理与号源发布流程

排班是医院运营侧的核心操作。管理员选择科室、医生、出诊日期、上午下午时段、放号总数,提交后后端先生成 schedule 记录,然后根据 total_slots 循环生成号源明细。如果某一天医生临时停诊,管理员需要把状态改成停诊,同时对已经预约的患者做退号或者改签。这块逻辑虽然不复杂,但容易遗漏提醒操作,所以代码里我会在停诊时加一个消息表记录,提示前端管理员确认通知患者。

排班发布接口我用了一个事务保护:先插入 schedule,再批量插入号源明细,任何一步失败都回滚,避免出现排班存在但没号源的脏数据。

3.3 挂号下单与并发锁号机制

患者选择号源后点击挂号,后端要做三件事:创建订单、锁定号源、等待支付。这里最核心的就是避免两个人同时抢同一个号。我最开始用的是先查号源状态,如果是未占用就 update 成已锁定,但两个请求同时查到未占用,后更新的那个就会覆盖前一个,出问题。

后来改成条件更新,SQL 大概是这样的:

update source_detail set status = 1, lock_user_id = #{userId}, lock_time = now() where id = #{sourceId} and status = 0

这条语句受影响行数为 1 才表示锁定成功,为 0 说明被别人抢了,直接返回“号源已被占用”。通过数据库的行级锁天然规避了并发覆盖问题,不需要引入 Redis 分布式锁也能保证正确性。如果团队里已经有 Redis,用分布式锁当然更灵活,但从系统简洁性考虑,这种条件更新方式我是推荐优先尝试的。

3.4 超时未支付自动取消

挂号订单和电商订单很像,锁定号源后患者如果一直不支付,号就一直被占着,其他人只能干等。我用了两种方式配合处理:一是前端倒计时,到达时间提示订单超时并引导取消;二是后端定时任务兜底,每 30 秒扫描一次超过 15 分钟未支付的锁定单,把状态改回取消并把号源恢复成未占用。

写定时任务时要注意数据库时间尽量用服务器时间统一,避免应用服务器和数据库服务器时间不一致导致误判。这里也用了一个条件更新:只有订单状态还是“已锁定”时才允许改成“已取消”,防止患者刚好在支付成功的那一刻被任务误取消。

4. 前端 Vue3 模块落地过程

4.1 工程初始化与目录结构设计

前端我用 Vite 创建 Vue3 项目,目录结构按业务模块划分,不是按文件类型划分。简单说就是挂号相关的页面放一起,医生管理放一起,个人中心放一起。这样新同事接手时找一个功能,直接进对应目录就能看到页面、组件和接口调用,不用满项目找文件。

npm create vite@latest hospital-web -- --template vue

创建完之后我习惯把 axios 封装成一个模块,统一定义 baseURL,加请求拦截器和响应拦截器。请求拦截器负责从本地存储取 token 并设置到请求头,响应拦截器统一处理后端返回的业务码和异常信息,页面里不需要每个请求都重复写错误处理。

4.2 状态管理与路由守卫

前端项目里我用 Pinia 管理用户信息和一些全局状态。登录成功后把用户信息存进 store,页面刷新时再从本地存储恢复。路由守卫也很有必要:访问需要登录的页面时,如果检测不到有效 token,直接跳转到登录页。这个逻辑不复杂,但能省去每个页面手动判断登录状态的重复代码。

还有一个细节是患者挂号过程中选择的临时状态,比如当前科室、选中的医生、排班日期,这些我用了一个独立的 store 去暂存,下单完成之后再清空。这样用户从科室列表点进医生详情,再跳转到确认下单页面,数据不会丢失。

4.3 核心页面实现要点

挂号首页主要展示科室列表,点击科室后进入医生排班列表页。排班列表页需要展示医生姓名、职称、出诊时间、剩余号数,通过调后端接口获取数据。剩余号数小于某个阈值时前端做视觉提醒,比如显示“仅剩 3 个号”,但这个提醒只是辅助,真正判断还是以后端为准。

确认下单页面比较关键。要展示当前选择的排班信息、患者姓名、就诊人手机号,并让用户确认。我在这里加了 15 分钟倒计时提醒,倒计时归零后自动禁用提交按钮。提交成功后跳到结果页,提示用户等待支付或者直接去支付。整个流程跳转逻辑要理清楚,不能出现用户订单创建了但前端不知道去哪的情况。

4.4 前后端接口对接的常见问题

开发环境前端访问后端接口最大的坑就是跨域。我在 Vite 配置里加了代理,把/api开头的请求转发到后端服务地址,这样前端代码里请求的 URL 就是相对路径,上线后通过 nginx 同样配置转发规则,代码不用改。这个方案比后端用 @CrossOrigin 配置跨域更干净,因为生产环境后端接口本来就不应该对所有人开放跨域访问。

后端返回的数据结构我也统一了一下:code、message、data 三个字段。code 是 0 表示成功,非 0 表示失败。这样前端响应拦截器里只需要判断一次 code,不需要为每个接口单独写 if else。

5. 从源码到可运行的完整部署流程

5.1 前置环境准备

这套系统依赖的东西不多:JDK 17、Node.js 18、MySQL 8.0,再加一个 Maven。MySQL 里先把建库建表的 SQL 执行一遍,创建数据库比如hospital,然后导入项目里带的数据初始化脚本,里面会有一些测试用的科室、医生、排班数据,省得自己手动插数据调试。

5.2 后端配置文件关键项

后端核心配置在application.yml里,我一般重点关注这几块:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.entity

注意 MySQL 连接串里的 serverTimezone 一定要配置,不然经常出现时区差 8 小时的诡异问题。MyBatis 的 mapper 文件路径要和实际目录匹配,很多启动报错都是这里对不上。

5.3 前端构建与 nginx 配置

前端开发调试时用npm run dev,部署时用npm run build,构建产物在 dist 目录。上线我用 nginx 托管前端静态文件,接口反向代理到后端。一个简单的配置示例:

server { listen 80; server_name your-domain; location / { root /opt/hospital-web/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files 那行一定不能丢,否则 Vue Router 使用 history 模式时刷新页面会 404。改成 hash 模式也可以避免这个问题,但我习惯用 history,配合 nginx 这段配置刷新就正常了。

5.4 部署后验证清单

启动完后端、构建完前端后,至少跑一遍核心流程验证:注册或登录、查看科室列表、进入医生排班、选择号源、下单、观察订单状态变化。再打开两个浏览器,用两个不同账号同时点击同一个号源,看会不会出现一个订单覆盖另一个的情况。如果两个都能下单成功说明条件是没加对,回去检查更新 SQL 的 status 条件。

6. 常见问题排查与避坑清单

6.1 启动阶段的典型报错

现象可能原因处理方式
启动报 mapper 绑定错误mapper XML 路径配置不对检查 application.yml 中 mapper-locations 和 XML 实际目录
登录接口返回 401token 过期或未携带检查前端请求拦截器是否正常设置请求头
查询中文乱码数据库连接串没配 utf8补上 characterEncoding=utf8 并重建表
明明有排班但查不到号源排班发布时事务回滚或号源未生成检查后端日志中排班发布接口是否完整执行

6.2 高并发抢号场景的隐藏坑

条件更新锁号这个方案本身没问题,但要注意事务隔离级别和持续时间。锁定号源后要尽快提交事务,不要在事务里做耗时的外部调用,比如发短信、调支付接口,这些放到锁号成功之后再异步执行。否则事务长时间持有行锁,数据库连接池会被打满,别人连查询都变慢。

6.3 前端联调时的几个老问题

开发环境接口能通,但打包上线后请求地址不对,大多数情况是前端代码里写成固定的开发地址,没有使用相对路径。上线后请求路径必须以 /api 开头,由 nginx 转发。另外就是检查浏览器 Network 面板,看请求是否带上了正确的 token,以及返回的状态码是什么。如果 401 跳转登录页死循环,看看路由守卫和登录页面里是否也发请求带了无效 token。

6.4 数据库层面的实践经验

建议把数据库表的初始数据脚本单独放在一个 sql 文件里,方便重新部署时初始化,不要手动一条条插入。排班和号源这类高频写入表,定期巡检索引使用情况。上线前用 explain 查看几条核心 SQL 的执行计划,凡是出现 type 为 ALL 的慢查询,优先补索引。这段时间运行下来,数据量在几万条订单级别时,这套结构配合索引单表查询都能稳定在几十毫秒内返回。

我自己的体会是,这类项目最耗时间的部分不是单一功能怎么写,而是把整个链路串起来之后反复联调的过程。只有实际跑一遍完整的挂号流程,才会发现排班发布、号源锁定、订单取消这些环节之间有多少边界情况需要处理。如果你准备基于这套源码做二次开发,建议先从数据库模型入手看懂表关系,再顺着“排班发布 -> 患者下单 -> 订单支付”这条主线去看代码,会比一上来就追每个接口的实现清晰很多。最后分享一个小技巧:后端接口的入参校验不要偷懒,尤其是下单时涉及 scheduleId、sourceId 这些传递过来的业务 ID,一定要校验是否存在且匹配,避免被人为构造请求绕过正常流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询