☰
医院后台管理系统源码实战:SpringBoot+Vue+MyBatis+MySQL核心业务闭环设计
2026/10/3 3:56:28 网站建设 项目流程

开发医院后台管理系统,技术栈上来就是 SpringBoot + Vue + MyBatis + MySQL 四个词,这组合看起来没什么稀奇,但真正把它做成一套能交付的“企业级完整源码”,坑比想象中多得多。这套源码我整体梳理过一遍,核心是围绕门诊业务流程:挂号、就诊、开方、收费、发药、住院管理、药库药房、权限日志,最后落到统计报表。这篇文章就把这套系统的设计思路和实现要点拆开讲,适合正在做医疗相关项目的 Java 开发者,也适合毕业设计选了医院管理方向、想少走弯路的同学。

1. 医院后台管理系统源码为什么大多从这个思路入手

1.1 先看清楚这套系统的用户和业务闭环

我做这套源码之前,最先做的就是梳理角色和流程。医院后台跟普通后台系统有一个非常大的区别:它不是一个简单的“用户管理 + 增删改查”,而是多个角色一起协作完成医疗业务闭环。

大致角色有这几类:

  • 挂号收费员:负责建档、挂号、收费、退号退费。
  • 门诊医生:看排队患者、写病历、开处方、开检查检验单。
  • 药房药师:接收处方、发药、退药、库存管理。
  • 护士站:住院登记、床位分配、医嘱执行。
  • 信息科/管理员:维护科室、用户、角色权限、查看系统日志。

核心业务流长这样:

  1. 患者建档后,在门诊挂号,选择科室、排班医生、号别。
  2. 医生在门诊医生站看到排队患者,接诊并书写病历。
  3. 医生开立处方或者检查单。
  4. 患者去收费窗口缴费,系统更新处方状态。
  5. 药房看到“已收费”的处方,进行发药并扣减库存。
  6. 需要住院的患者办理入院、分配床位、录入医嘱、执行医嘱,最后结算出院。

源码的完整性,很大程度上取决于这些步骤是不是真正闭环。现在网上能下载的很多“医院管理系统源码”只是做了一个患者表、挂号表、医生表的 CRUD,点几个页面也能跑,但处方没有状态流转,收费和库存没有联动,医生站也只是一个空壳。这种源码做完只能应付答辩,离“企业级”还差着十万八千里。

1.2 “完整版”源码的判断标准不是页面数量

判断一套医院后台管理系统源码是否完整,不要只看扫出来的静态页面数。我一般直接问几个问题:挂号时号源会不会超卖?处方收费后药房能不能立刻看到?发药会不会把库存扣成负数?作废处方后费用和库存能不能还原?这些场景能跑通,说明业务闭环是设计过的,否则再多的页面也只是一堆摆设。

这套源码在设计时,把业务状态流转放在第一优先级。比如挂号记录,状态可能是“已挂号”“已就诊”“已退号”;处方有“未收费”“已收费”“已发药”“已退药”;住院记录有“在院”“预出院”“已出院”。状态不是用中文硬编码写在代码里到处判断,而是用状态值配合枚举或者字典表去管理,后面做统计报表、查历史记录都会轻松很多。

还有一点,企业级系统里权限和日志是刚需,不是锦上添花。医院项目交付后,信息科最关心的是“谁能退费”“谁能作废处方”“谁在什么时间改了什么数据”。所以用户、角色、菜单按钮权限要单独建模,敏感接口必须做强校验,操作日志要详细记录参数、操作人、时间。这些不写,后期维护的时候很容易被甲方翻旧账。

2. SpringBoot+Vue+MyBatis+MySQL的组合选择与架构落地

2.1 为什么是这四个技术组合

这套技术栈乍一看很“古典”,但放在医疗后台项目里其实是很务实的选择。

SpringBoot 的好处不用多说,它把配置简化到了极致,内置 Tomcat,打一个 jar 包就能跑起来。医院项目往往要部署在内网服务器,没有复杂的容器编排环境,单体应用反而是最省事的选择。如果一上来就上微服务、Spring Cloud、Nacos、网关那一套,光是运维成本就能把一个小团队拖垮,对常规门诊量的医院完全是过度设计。

Vue 选它的核心原因是前后端分离后,开发效率确实高。医院后台的页面交互其实不简单:门诊医生站要同时展示待诊列表、患者信息、病历编辑、药品检索,药房窗口要刷新未发药处方列表。Vue 组件化之后,这些模块都能拆开写,页面状态管理也更清晰,后面接电子病历、检验检查报告这类第三方嵌入也方便。

MyBatis 在这套架构里的定位特别明确:SQL 交给开发者控制,不被 ORM 绑架。医院业务里复杂统计特别多,比如按科室、医生、时间段统计门诊量,按月汇总药品消耗,这类报表用 MyBatis 自定义 SQL 最顺手。还有一个很重要的场景,就是并发更新库存时,MyBatis 里直接写条件更新语句,比全自动 ORM 提供的乐观锁插件更直观,可控性强。

MySQL 面对医院这个量级的数据完全够用。常规二级医院一天门诊量撑死几千人次,核心表的数据量增长并不夸张,MySQL 加上合理的索引和配置,查询性能没有问题。相比 Oracle 和 SQL Server,MySQL 在开源生态、部署便利度、运维成本上优势明显,这也是大量医疗信息化项目选择它的原因。

这四个技术组合,本质上是“单体应用 + 前后端分离 + SQL 可控 + 轻量级数据库”的务实方案。它不一定是最炫的,但一定是最容易招聘到人、最容易上手、也最容易长期维护的。

2.2 前后端分离落地:统一接口协议和认证方式

前后端分离看起来简单,但落地时最容易乱在接口协议上。这套源码从一开始就约定了一套统一返回格式,所有接口都遵循同一个结构:

{ "code": 200, "message": "success", "data": { "id": 1, ... } }

后端对应一个统一的响应包装类,代码大致是这样:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }

前端无论是 Axios 还是其他 HTTP 库,只需要在响应拦截器里统一处理 code,等于把异常处理收口到一个地方,不用每个页面去判断乱七八糟的返回状态。

认证方式用的是 JWT Token。登录接口验证用户名密码后,签发一个包含用户ID和角色信息的 Token,前端存到本地,之后每次请求在请求头里带Authorization: Bearer <token>,后端拦截器统一校验。这样做的最大好处是:网关层面不用维护 session,内网部署多台机器做负载均衡时,也不用考虑会话复制的问题。

为了防止前后端联调时反复折腾跨域配置,后端做了一个统一的 CORS 过滤器,允许指定来源,生产环境只放开前端域名或者干脆同域部署。这一点看着不起眼,实际开发中能省下很多无效沟通时间。

2.3 工程结构和分层设计

后端工程不是所有代码塞一起,而是按基础功能分包,这套源码的包结构大致是:

  • controller:接口入口层,负责参数校验、调用 service。
  • service:业务逻辑层,事务控制基本都放在这一层。
  • mapper:MyBatis 数据访问接口。
  • entity:数据库实体。
  • dto/vo:请求参数和返回视图对象,避免实体直接暴露给前端。
  • config:安全配置、CORS 配置。
  • common:统一返回、异常处理、常量定义。

一个经验:不要把返回视图对象直接拿数据库实体顶替。比如患者实体里面可能有身份证号、联系方式这些敏感字段,如果用同一个实体直接返回前端,很容易出现不想暴露的字段被带出去的情况。所以patient实体对数据库,PatientVO对前端,中间做一次转换,安全性和灵活性都更好。

前端 Vue 工程目录也做了拆分:

  • views:页面级组件,对应路由。
  • router:路由配置。
  • api:接口调用层,按模块拆文件。
  • store:全局状态,比如用户信息、菜单权限。
  • components:通用组件,比如分页组件、日期选择器。

为什么要单独抽一层api?因为所有请求都封装在这里,接口地址变动只需要改一处,而且可以在这一层统一挂载 Token,统一做错误码判断,页面里就不用每个请求都写一遍请求头处理逻辑。

3. MySQL核心表结构设计——医院业务的建模重点

3.1 基础数据表:科室、医生、床位

医院后台表的数量不会少,但核心主数据就那几张。科室表是阵地,所有挂号和排班都挂在科室下面,科室本身还有层级关系,比如内科下面分心血管内科、呼吸内科,外线分骨科、普外科,所以科室表里要有一个parent_id字段做树形结构。

医生表不要把排班逻辑混进去。医生表只存医生基本资料、工号、职称、所属科室、执业范围;排班单独建doctor_schedule表,记录某医生在某个时间段的号源总量、剩余数量。为什么分开?因为医生资料是相对静态的数据,排班是高频变化的业务数据,如果塞在同一张表,每次调整排班都要去 update 医生表,数据会越改越乱。

床位管理也是一样。病区可以理解成科室下面的一个护理单元,表结构里ward_id和bed_id分开,病床状态分成“空闲”“占用”“消毒中”等,这样护士站分配床位时才能一眼看出哪张床能用。

3.2 挂号、就诊、处方之间的关键关系

患者表是核心主索引,所有业务都围绕patient_id展开。患者表的唯一性设计要注意,通常用身份证号做逻辑唯一键,但也要考虑到极少数没有身份证的儿童或特殊患者,所以主键用自增或雪花 ID,身份证号加唯一索引但允许空值。

挂号表registration是门诊流的第一个节点,它关联了患者、科室、医生、号别,还有挂号费用和状态。这里有一个设计细节:挂号费用不要直接用 decimal 在代码里运算后塞进去,而是在创建挂号记录时从号别字典表里带出费用,存成快照。因为收费项目价格可能调整,但历史缴费记录必须保持当时的价格,这一点在医疗收费场景里特别重要。

处方表建议拆成主表和明细表,prescription和prescription_item。主表记录处方单号、关联的就诊记录、总费用、状态;明细表记录每一条药品,包括药品ID、药品名称、规格、数量、单价、用法用量。药品名称、规格、单价都做成冗余快照,药品目录可以后续维护调整,但已开具处方的历史数据不能跟着变,这是医院系统跟一般电商系统明显不同的地方。

3.3 药库药房和结算,两个容易写乱的地方

药库和药房的库存不建议直接在一张表上解决,因为业务阶段不同。药库管采购入库、供货商退货、入库批次;药房侧重于发药扣减、退药回补。做得简单一点,至少要有两张核心表:

  • 药品库存表:记录药品当前总库存、可用库存。
  • 药品出入库流水表:记录每一笔入库、出库、退库的来源和去向。

扣库存时不要直接写死一个负数,而是在发药时做“扣减前检查”,保证可用库存充足才允许发药,否则直接抛异常。药品销售出库和药库采购入库不要用一个接口,逻辑要分开,否则后期盘点会对不上账。

结算表是整个收费环节的落点。无论是挂号费、门诊处方费还是住院预交金,最后都应该有一条结算流水。结算表建议设计成多业务来源的模式,不强行和某一张业务表建物理外键,而是通过biz_type和biz_no关联不同类型业务。比如biz_type=1表示挂号费,biz_no存挂号单号;biz_type=2表示处方费,biz_no存处方单号。这样做的好处是以后加检查费、检验费,不需要大改结算表结构。

4. 核心业务闭环的后端实现——从号源到发药的完整链路

4.1 挂号:用条件更新解决并发超卖

挂号是后台系统里第一个需要严肃处理并发的地方。挂过医院号的朋友都知道,专家号放出来可能几秒就没了。如果用“先查询剩余号数,判断大于0,再执行扣减更新”这种套路,高并发下一定会超卖,因为多个请求同时查到的剩余号数可能是同一个值,然后都进入更新逻辑。

正确做法是用条件更新直接把判断和扣减合并成一条 SQL:

<update id="decreaseRemain"> update doctor_schedule set remain = remain - 1 where id = #{scheduleId} and remain > 0 </update>

这条 SQL 的妙处在于,remain > 0是原子性判断条件,受影响行数为 1 才代表扣号成功,为 0 就代表号源已经被抢完。Service 层代码写成这样:

@Transactional(rollbackFor = Exception.class) public RegisterResult register(RegisterDTO dto) { int updated = doctorScheduleMapper.decreaseRemain(dto.getScheduleId()); if (updated == 0) { throw new BizException("该号源已挂满"); } // 保存挂号记录 registrationMapper.insert(registration); // 需要预交费则同步生成结算流水 return buildResult(registration); }

事务为什么加在 Service 层?因为“扣号源”和“保存挂号记录”必须作为一个整体,要么都成功要么都失败。如果先扣了号源,后面插入挂号记录失败,事务回滚会把扣号操作也撤销,不会出现号扣了但挂号记录没有的情况。

4.2 处方状态流转和药房发药联动

门诊医生开完处方,并不代表药品库存要立刻扣减。如果处方还没收费就被作废,提前扣库存会把库存数量搞乱。所以处方状态机按这样的规则设计:

  • 初始开立:状态为“未收费”,此时不触碰库存。
  • 门诊收费成功:状态变为“已收费”,药房工作台出现可发药的处方。
  • 药房确认发药:状态变为“已发药”,同时扣减药品库存、写库存流水。
  • 退费退药:状态回到“已作废”或者单独的“已退药”,库存回补。

这里有一个很容易写错的地方:把“收费”和“发药”做成同一个接口。虽然看起来省事,但窗口业务上不是一回事。收费是在收费窗口完成的,发药可能在药房窗口,两个动作有明确的时间差,所以对应的后端接口也应该分开。发药接口里再执行库存扣减逻辑,库存表更新和处方状态更新放在同一个事务里,保证数据一致。

退药场景也要考虑好。药房退药后,库存应该加回去,费用要做退费流水。如果退药时已经过了收费窗口时间,还要支持退费申请和审批流转。虽然底层逻辑不复杂,但状态一定要走完整,不能直接删除原来的处方记录,必须用状态标识历史轨迹,不然后面查账和审计会出大问题。

4.3 权限控制和操作审计的埋点方式

权限设计采用经典的 RBAC 模型,用户表、角色表、权限表、用户角色关联表、角色权限关联表。前端根据当前用户的权限集合控制菜单和按钮的显示,后端在接口上做二次拦截。按钮级别的权限用自定义注解@RequiresPermission,比如:

@RequiresPermission("registration:refund") @PostMapping("/refund") public Result<Void> refund(@RequestBody RefundDTO dto) { registrationService.refund(dto); return Result.success(null); }

后端的权限校验不能依赖前端隐藏按钮,必须接口层面独立校验。因为绕过前端直接请求接口的成本很低,按钮隐藏只能防君子,不能防小人。

操作审计埋点有几个关键动作必须记录:退号、退费、作废除方、修改费用、修改库存、删除患者信息。实现上可以写一个OperationLog注解配合 AOP 切面,统一记录操作人、操作时间、接口方法、请求参数、操作结果。为了避免切面里用 JSON 序列化把参数打得太笨重,还可以在注解上指定哪些参数不入日志,防敏感信息泄漏。

5. 这套源码部署自测时最容易踩的坑

5.1 MyBatis 多表查询结果映射的别名问题

用 MyBatis 的时候,最简单的字段映射可以靠开启驼峰转换解决:

mybatis.configuration.map-underscore-to-camel-case=true

这样数据库的department_name能自动映射到实体的departmentName。但多表 JOIN 查询时,很容易出现两个字段同名的情况,比如医生表有name,科室表也有name,直接查出来映射就会乱。所以复杂查询必须在 SQL 里起别名,并且用resultMap明确映射关系。这是一个非常基础的坑,但大多数初学者都会在这里卡很久。

另外还有一个常见问题:resultMap 里个别字段不写映射,也不开自动映射,导致明明查询结果有值,实体里却是 null。我的习惯是:能开驼峰转换就开,resultMap 里只写特殊映射字段,其余交给自动映射,避免把 resultMap 写成一长串,维护起来很痛苦。

5.2 并发抢号场景下“先查后改”的经验教训

前文已经讲了挂号怎么用条件更新解决并发超卖,这里补充一个我在开发过程中实际遇到的变体:有人为了显示剩余号数,在查询剩余号之后再执行更新,代码写成“先查剩余再判断”,导致并发测试时偶尔还是会出现超卖。原因就是两个请求同时读取到remain=1,接着都判断“大于0”,然后都执行了更新。所以必须非常明确地跟团队成员强调:所有库存类数据的扣减,必须在 UPDATE 语句的 WHERE 条件里带上数量约束,而不是在代码里先查后比。

顺带建议,开发阶段可以直接写一个简单的 JMeter 测试脚本,模拟 200 个并发请求抢同一个号源,验证数据库剩余数量不会变成负数。这一步跑通了,这套源码里最核心的并发问题才算真正解决。

5.3 MySQL 连接参数、时区和 SQL 模式

SpringBoot 连 MySQL 8 的时候,JDBC 连接字符串里几个参数比较关键:

spring.datasource.url=jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=yourpassword

serverTimezone=Asia/Shanghai必须配,否则会报时区错误。allowPublicKeyRetrieval=true在 MySQL 8 的某些认证插件下必须配,否则连接时可能报 Public Key Retrieval 错误。还有一个常见的坑是 MySQL 的sql_mode设置了ONLY_FULL_GROUP_BY,这时如果写了一个没有把所有非聚合字段都放进GROUP BY的统计 SQL,就会直接报错。解决办法不是全局关掉这个模式,而是把报表 SQL 改规范,每个非聚合字段都明确分组条件。

5.4 Vue 前端部署后的刷新 404 问题

前端用的是 Vue Router 的 history 模式时,直接打包部署到 Nginx,用户访问到子路径比如/patient/list,刷新一次就会 404,因为 Nginx 没有对应的物理文件。解决方案是在 Nginx 配置里加一个 fallback,把不存在的路径都重写到index.html:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

如果后端接口和前端不在同一个域,还要在 Nginx 里加一层反向代理,把/api转发到后端服务地址,避免前端直接请求跨域接口。很多同学本地开发是好的,部署到服务器就白屏或者接口不通,大部分都是这个原因。

另外提一点,开发环境里跨域可以直接用 Vue CLI 的 proxy 配置,但生产环境一定要走 Nginx 反向代理或者后端 CORS 配置,不要为了省事在前端代码里把后端地址写死成localhost。我自己见过不少项目,代码提交到了生产环境还在请求 localhost,查了半天才发现是环境变量没替换。

落地说一句,这套医院后台管理系统源码的难点,其实不在于用到了多高深的技术,而在于有没有把业务闭环想清楚,把并发、库存、状态流转、权限审计这些容易出错的地方处理干净。如果你也是自己动手写这类项目,我的建议是先从挂号到发药这条主链路跑通,再补统计报表和权限细粒度控制,最后才去考虑美化页面。主链路通了,这套源码才算真正立住了。

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

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

立即咨询