拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue前后端分离医院网站系统实战解析

SpringBoot+Vue前后端分离医院网站系统实战解析

1. 为什么医院网站要选这套前后端分离技术栈

先说说我是怎么接下这个项目的。有一家二级规模的民营医院要做官网改版,最初的诉求特别简单:"把科室介绍和医生排班放到网上,患者能查得到就行"。结果需求评审时,陆续加进来预约挂号、检查报告查询、在线留言、后台排班管理,还要求手机和电脑都能用。事到临头,技术选型就成了第一道门槛。

说实话,市面上做医疗信息化的方案特别多,从轻量级的WordPress到医院级别的HIS系统都有。但中小型医院网站有个非常典型的特点:预算不高、业务量不大、迭代需求却不断。这种场景下,SpringBoot + Vue + MyBatis + MySQL这套组合几乎是最稳妥的选择,原因有几个层面。

第一是团队上手成本低。后端Java生态的招聘行情一直稳定,懂SpringBoot的人远比懂Go、懂Python的人好找;Vue在国内前端圈普及率极高,就算外包接盘也不至于无人能维护。第二是部署环境友好,医院自有的服务器经常是老旧Windows Server或低配CentOS,Java的跨平台特性和Nginx的轻量级转发,能很好地兼容这类环境。第三是生态完善,从短信通知到支付接口,从Excel导入导出到打印模板,Java和Vue都有大量成熟的第三方库,踩坑资料也多,中小公司不需要太多底层研究就能搞定。

但我也要泼一盆冷水:如果你只是想搭个静态展示官网,没必要上前后端分离。纯展示页面用Nuxt或Next直接服务端渲染,或者干脆WordPress,成本低得多。前后端分离真正适合的是有交互、有状态、有权限的业务系统,比如预约挂号、报告查询、后台管理。这个项目的核心恰恰就是这些。

1.1 核心业务流程梳理:挂号、排班、报告是三大主干

我在动手写代码前,先做了一件很多人会跳过的事:把医院网站的完整用户路径画出来。我们服务的对象有两类,一类是患者,一类是医院内部运营人员(导诊台、药房、门诊医生)。患者端的主要路径是:

  • 浏览科室和医生介绍,确认科室是否开诊;
  • 查看医生排班,选择上午/下午的号源;
  • 填写就诊人信息并提交预约;
  • 到院后医生开处方、检查单;
  • 患者查看检查报告和处方记录。

后台端的主要路径是:

  • 维护科室、医生、排班信息;
  • 查看预约记录,处理停诊改签;
  • 药品库存和处方发药管理;
  • 配置公告、首页轮播图等展示内容。

这两条路径同时跑在一个系统里,后端就必须拆成患者端接口和后台管理接口两套,权限上也要严格区分。这里有个容易掉坑的点:医院项目里患者信息属于敏感数据,前后端接口交互时不能把身份证号、手机号这类字段直接明文返回给前端。我当时的做法是后端统一做脱敏处理,查询列表时手机号只显示前3位和后4位,只有点击详情且具备权限时才返回完整数据。这个细节在需求文档里往往不会写,但上线前如果被安全测试提出来,返工代价会很大。

1.2 表结构设计里的导诊台逻辑

数据库设计是这类系统的地基。中小型医院预约量不会特别大,但业务关联关系却不少。我拆出来的核心表包括:用户表、患者表、科室表、医生表、排班表、预约挂号表、处方表、处方明细表、药品表、检查报告表、公告表。其中最容易设计失误的是排班表和预约表之间的关系。

先看排班表。医生排班的要素是:医生、日期、时段(上午/下午)、号源总数、剩余号源、状态(正常/停诊)。有些系统会把上下午合并成一条记录,加一个时间段字段,我强烈不建议这么做。因为上午号和下午号的停诊、放号规则是完全独立的,合并成一条记录后,所有针对时段的操作都要加个条件判断,代码里全是这种分支,维护起来非常痛苦。

CREATE TABLE doctor_schedule ( id BIGINT AUTO_INCREMENT PRIMARY KEY, doctor_id BIGINT NOT NULL, schedule_date DATE NOT NULL, period TINYINT NOT NULL COMMENT '1上午 2下午', total_count INT NOT NULL, remain_count INT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停诊', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_date_period (doctor_id, schedule_date, period) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

UNIQUE KEY uk_doctor_date_period这行很重要,它从数据库层面保证了同一个医生同一天同一个时段只有一条排班记录,防止前端重复提交造成两条排班。这类并发问题在开发环境很难暴露,但上线后一旦有人手抖点了两次发布,数据就会出问题。

再说预约表。预约的核心字段是:患者、排班、就诊日期、时段、状态、就诊人姓名、手机号、身份证号、创建时间。这里有一个业务上的细节:就诊人并不一定等于注册用户。很多患者是子女帮忙挂号、老人来看病,所以预约表里必须冗余一份就诊人的姓名和证件信息,不能只存一个用户ID就完事,否则实际到院核验时会出现"挂号和就诊人不是同一人"的情况。

1.3 为什么说 MyBatis 在这类系统里比 JPA 更顺手

SpringBoot 的持久层方案,主流无非两种:Spring Data JPA 和 MyBatis。说实话,JPA 写简单的 CRUD 非常爽,几乎不用写 SQL。但在医院这种业务系统里,报表统计、动态条件查询、分页联动这些场景非常多,用 JPA 做复杂查询时生成的 SQL 往往不可控,性能调优时让人挠头。MyBatis 在这类场景下反而更合适:SQL 掌握在自己手里,动态 SQL 用<if>、<where>、<foreach>就能解决 90% 的需求,排查问题时直接看 XML 里的语句,思路非常清晰。

我自己写 MyBatis 有一个习惯:所有 UPDATE 操作都在 XML 里写明乐观锁或状态条件,比如UPDATE ... WHERE id = #{id} AND status = 1,这样即使两个请求同时到达,数据库层面也能保证状态一致。还有一个点,MyBatis 的二级缓存默认是打开的,但在医院系统里我不建议轻易启用,因为查询关联多、并发写多,缓存失效策略稍有不慎就会查到脏数据。干脆关闭,靠 MySQL 的 InnoDB 缓冲池来处理重复查询,对几百人同时在线的中小医院来说性能完全够用。

2. 后端接口设计:从预约挂号到处方发药,关键接口怎么拆

2.1 预约挂号的并发控制

预约挂号是整个系统的核心接口,也是最容易出并发问题的地方。一个号源总数 30 的上午排班,如果 100 个患者同时请求,怎么保证只有 30 个人成功?这里我采用了数据库悲观锁方案,使用SELECT ... FOR UPDATE锁定排班记录,再检查剩余号源并执行扣减操作。

很多教程会推荐乐观锁,也就是在排班表里加一个 version 字段,更新时SET version = version + 1 WHERE version = #{version}。但实际用下来,乐观锁更适合读多写少、冲突概率低的场景。医院预约的号源是稀缺资源,冲突概率极高,乐观锁会让大量请求做无效重试,用户体验差。悲观锁虽然会阻塞,但医院预约的高峰窗口就那么一两个小时,数据库连接池配 10~20 个连接完全扛得住。

真正容易被忽略的是事务边界。预约操作必须包含三步:锁排班记录、检查号源并扣减、插入预约记录。这三步必须在同一个事务里,任何一个失败都要整体回滚。我见过有人把扣减号源和插入预约分成两个接口,前端先调第一个再调第二个,结果第二步网络超时,号源扣了但预约没生成。这种设计上的错误,上线后会成为灾难。

2.2 科室与排班查询的接口拆分

科室和排班查询是患者端访问量最大的接口。如果只做一个大接口返回所有数据,前端页面加载会变慢,后端压力也大。我拆成GET /api/departments和GET /api/schedules?doctorId=&date=,前者返回科室树,后者按医生和日期筛选排班。这样前端的"科室列表页"和"医生排班页"可以独立拉取数据,首屏加载速度提升明显。

排班接口返回的数据里我特意加了一个字段:remain_count。这个字段直接决定前端页面展示的是"有号"还是"约满"。但要注意,这个数字是实时变化的,前端不能把它缓存起来太久。我当时在 Vue 端做了一个定时刷新,每 30 秒重新拉取一次排班数据,保证患者看到的号源情况尽量接近真实值。虽然不是完全实时,但对用户体验的提升非常明显——至少不会出现患者明明看到有号、提交时就提示约满的情况。

2.3 报告查询和处方模块的权限设计

报告查询涉及患者隐私,权限控制必须严格。我的设计是:患者只能查询自己名下的报告,后台医生可以查看所有关联自己看诊记录的报告。前者通过 Token 中的用户ID过滤,后者通过医生ID和排班记录关联过滤。本质上就是所有查询 SQL 都强制带上一个归属条件,不能出现"传一个报告ID就返回报告内容"的裸接口。

处方模块的权限分得更细。开方是医生的权限,发药是药房人员的权限,查看是患者和医生共有的权限。我用了 Spring Security 配合自定义注解来做细粒度权限控制,比如在 Controller 方法上加@PreAuthorize("hasRole('DOCTOR')"),只允许医生角色访问开方接口。这里提醒一下,千万不能只用前端路由来隐藏入口,后端每个接口都必须做权限校验。否则别人直接构造请求就能绕过前端页面,这是安全底线问题。

3. 前端 Vue 项目落地:路由权限、组件复用和状态管理

3.1 动态路由与权限控制

前端用的是 Vue 3 + Vite + Vue Router 4 + Pinia,这套组合现在已经是 Vue 项目的标准配置。医院系统涉及多种角色:普通患者、医生、药房人员、管理员。不同角色登录后看到的菜单和能访问的页面完全不同。

我采用的方式是动态路由。登录成功后,后端根据用户角色返回一个权限码数组,前端拿这个数组去匹配路由表,把有权限的路由动态添加到 Router 里。这需要在路由配置里预定义好所有页面组件,然后用router.addRoute()逐个注册,同时把页面标题和菜单项绑定,统一渲染左侧导航栏。这里有个坑:如果刷新页面,动态加的路由会丢失,需要在router.beforeEach()全局守卫里重新拉取用户信息和权限码,再重新注册路由。这个逻辑不写的话,刷新后必现"页面白屏"或"找不到路由"的问题。

3.2 页面组件与通用封装

医院网站虽然页面多,但很多模块有相似性。比如科室列表、医生列表、报告列表,都是"列表 + 搜索 + 分页"的形态。我没有每个页面重写一套,而是封装了一个通用的ProTable组件,把请求、分页、Loading、空状态都内置进去,页面只需提供列配置和数据接口地址。这个封装的收获超出预期,光在预约记录、科室查询、报告查询这三块上就省了大量重复代码。

另外,预约流程的交互设计也花了心思。患者从选科室到选医生再到选时段确认预约,一共四步,比一般电商下单多了一步选择就诊时间。我对这一步做了特别的交互优化:当日和次日的号源用时间线展示,颜色区分上午、下午,点击后立即显示剩余号数,避免患者点了医生还要再等页面跳转才能看到有没有号。

3.3 axios 封装与错误处理

所有接口请求统一走封装后的 axios 实例。基础配置包括超时时间(30秒)、请求头里自动附带 Token、响应拦截器里统一处理错误码。医院系统对错误提示的要求比一般网站更高:预约失败时要明确告诉用户是号源不足、网络异常还是身份信息不完整,不能只弹一个"系统错误"。

我在响应拦截器里做了错误码映射:401 跳转登录页,403 提示无权限,500 提示服务器繁忙。业务级错误码(比如 20001 表示号源不足,20002 表示排班已停诊)则从响应体里读取,并在页面里展示对应的友好提示。前端所有时间字段统一用 dayjs 格式化,医院项目里时间格式必须精确到分钟,比如"2024-06-15 08:30",这个细节在核对就诊时间时特别重要。

4. 部署上线全流程:从本地到服务器,从 Nginx 到 MySQL

4.1 本地打包与构建

后端用 Maven 打包,执行mvn clean package -DskipTests生成 jar 包。这里有个细节:-DskipTests是跳过测试,但很多人会把测试类的编译也跳过(-Dmaven.test.skip=true),我建议保留测试编译而只跳过测试运行,这样至少能发现代码有没有编译错误。

前端用npm run build构建,生成dist目录。Vue 项目默认的路由模式有两种:hash 模式和 history 模式。医院项目我推荐 history 模式,URL 更干净,但必须配合 Nginx 做配置。如果你用 hash 模式,URL 里会多个#,不美观也不利于分享。更重要的是,history 模式遇到部分老旧浏览器时可能不支持history.pushState,不过现在国内主流浏览器基本都没问题。

4.2 Nginx 反向代理与静态资源托管

部署时我选择了 Nginx 作为 Web 服务器。Nginx 承担两个职责:托管前端静态资源和反向代理后端 API。为了让前端代码里不硬编码后端地址,axios 的 baseURL 直接写成/api,通过 Nginx 把/api开头的请求转发到 SpringBoot 服务的 8080 端口。

server { listen 80; server_name your-hospital-domain.com; # 前端静态资源 root /var/www/hospital/frontend/dist; index index.html; # 后端 API 代理 location /api/ { proxy_pass http://127.0.0.1:8080; 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 路由回退 location / { try_files $uri $uri/ /index.html; } }

这段配置里最关键的是最后一个location /块里的try_files $uri $uri/ /index.html;。它的作用是:当请求的 URL 不是实际存在的文件时,Nginx 统一返回index.html,让 Vue Router 使用 history 模式也能在刷新子路由时正常加载。如果不加这一行,你访问/schedule/detail/1这种地址刷新后,Nginx 会去找这个路径下的静态文件,找到就返回内容,找不到就 404,页面直接白屏。

proxy_pass这里也有一个细节:http://127.0.0.1:8080;后面不跟路径,因为后端接口本身带有/api前缀,后端 Controller 的 RequestMapping 也包含api,所以保持原样转发。如果你的后端接口前缀不一致,比如 Nginx 上用/api/命中、后端又没有/api前缀,那就需要在proxy_pass里做路径改写,这种情况配置会更复杂一点,需要小心处理。

4.3 SpringBoot 打包后的启动参数与配置外置

后端打包成 jar 后,启动方式可以很简单:java -jar hospital-system.jar。但生产环境不会这么粗暴。我用的是java -jar hospital-system.jar --spring.profiles.active=prod,也就是通过启动参数指定加载application-prod.yml配置。

配置外置是必须的,否则每次改数据库密码、改短信密钥都要重新打包。我的做法是:把需要经常变的配置放在application-prod.yml里,路径放在 jar 同级目录的config文件夹下,SpringBoot 会自动加载外部配置并覆盖 jar 内的默认配置。这样运维时只需要改一个 yaml 文件,重启服务即可,完全不用动 jar 包。

数据库密码这类敏感信息,我没有明文写在 yaml 里,而是用环境变量占位符:password: ${DB_PASSWORD},然后在启动脚本里 export 出来。虽然中小医院的内网环境威胁不大,但养成这个习惯没有坏处。启动脚本用nohup java -jar ... > log.out 2>&1 &,把日志输出到文件,方便后续排查问题。

4.4 MySQL 初始化与常见部署坑

MySQL 侧有两个高频问题值得说。第一个是字符集,MySQL 8.0 默认字符集已经是 utf8mb4,但如果你用的还是 5.7,建库时必须手动指定:CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。如果不加,存个"患者姓名"带生僻字就会乱码或者插入失败。

第二个是时区问题。数据库连接串里一定要带serverTimezone=Asia/Shanghai,否则 SpringBoot 连接 MySQL 8.0 时会报时区错误。Spring Boot 2.x 之后默认使用 HikariCP 连接池,如果连接 MySQL 8.x 还需要把驱动版本升到mysql-connector-java8.0 以上,否则会提示 SSL 连接错误。

初始数据也很重要。我写了一个data.sql文件,里面预置了科室、医生、管理员账号的初始化数据,首次部署时自动执行。这样做的好处是:部署完成后立刻就能登录后台,不需要手动敲 SQL 维护基础数据。前端页面的医院介绍、科室介绍等内容,我把它们做成了后台可维护的富文本数据,存表内容,页面动态渲染。这样改医院简介、更新医生出诊时间时,都不需要重新部署代码。

4.5 上线验证与回归测试

部署完成后,不能只看首页能打开就算完事。我的上线验证清单是:

  • 用普通患者账号走一遍完整预约流程,确认能收到预约成功的返回信息;
  • 用管理员账号登录后台,确认能修改排班并保存;
  • 用医生账号登录,确认能查询到分配给自己的患者列表;
  • 检查报告上传后,患者端能正常查看;
  • 停掉后端再访问前端,确认 Nginx 的 502 页面不会裸奔给用户。

这套流程每次都手跑一遍,虽然繁琐,但能保证发布后不会出现"某个角色完全没法用"的最恶劣状况。做完这些验证,我才会让护士长或门诊主任在实际环境里走一遍真人事流程。

5. 最后想对做类似项目的人说的话

前后端分离的医院网站系统,技术上并没有太多高深的地方,真正的挑战在于两件事:一是业务流程必须想清楚,排班、停诊、改签、退号这些边缘场景都要提前设计,否则上线后天天接需求补丁;二是权限和数据安全不能偷懒,医疗数据的敏感性决定了系统的容错率很低,一次信息泄露或一次号源错乱,代价都远超普通网站。

踩过的坑列一下,给大家做个参考:

  • 数据库连接字符串必须显式指定characterEncoding=utf8和serverTimezone=Asia/Shanghai,否则大概率出现中文乱码和日期偏差;
  • 前端 history 路由配 Nginx 时,try_files这行配置必加,否则刷新子页面白屏;
  • 动态路由在刷新后会失效,必须在路由守卫里重新注册;
  • 预约扣库存不能拆成两个接口,必须一个事务里同步完成;
  • MySQL 8.0 需要更新 JDBC 驱动,连接池参数建议把maximum-pool-size设在 10~20 之间,避免空闲连接被数据库断开。

我在交付这套系统时,额外把启动脚本和数据备份脚本都写好了,用 crontab 每天凌晨备份一次数据库。医院系统里数据就是命根子,程序可以随时重启,库不能丢。这套系统上线后运行了半年,基本稳定,除了偶尔的服务器重启之外,没有出过业务故障。

如果大家手头也在做类似项目,我建议不要把太多精力花在追求花哨的前端特效上,把预约流程的稳定性、数据权限的严谨性、备份机制的可靠性打磨好,这个系统的价值就已经很高了。

返回列表