简介:这是一套基于SpringBoot、Jpa与Thymeleaf开发的Java医院信息管理系统源码,面向中小型医疗机构信息化建设需求,也适合希望深入理解主流Java Web技术栈实战应用的开发者与学习者。系统整合患者管理、医生排班、科室协作、药品库存、财务收支、预约挂号、住院管理、检查报告、权限控制与系统设置等核心业务模块,覆盖医院日常运营的主要流程。压缩包共3973个文件,约15.37MB,以svg图标、scss与css样式、class编译文件、js脚本、java源码及html模板为主,另含yml配置、sql脚本与少量字体、图片资源,目录结构完整,便于按模块检索与二次开发。目前已有2525人学习下载。通过这套源码,读者可参考实体与业务分层设计、数据库映射方式及页面渲染实现,对照自身项目进行功能扩展或定制改造,也可作为课程设计、毕业设计的技术选型参考。
1. 医院信息管理系统源码:从一堆表结构到能跑起来的门诊流程
很多开发者第一次拿到“java医院信息管理系统源码”时,会以为这是一套开箱即用的成品,结果解压后看到几百张表、上千个 Java 文件,直接懵了。医院HIS源码的核心价值不在于代码量,而在于它把挂号、就诊、收费、发药、住院这条业务链用数据流串了起来。你真正要搞清楚的,是这套系统怎么把“一个患者从进医院到离开”拆成可落地的模块。适合谁看?想接医疗信息化私活的后端、需要做课程设计的学生、以及准备把 HIS 作为中台底座的技术负责人。下面我按实际落地顺序,把选型、跑通、踩坑、进阶一次讲透。
2. 先看懂医院HIS源码的模块拆分与数据流
2.1 门诊、住院、药房、收费四条主线的边界
一套能用的 HIS 源码,模块划分基本逃不出这四块:门诊管理(挂号、分诊、医生站)、住院管理(入院登记、医嘱、床位)、药房药库(库存、发药、退药)、收费结算(划价、支付、发票)。很多新手一上来就扎进代码里找 Controller,结果被各种 Service 绕晕。我的习惯是先看数据库表名前缀,比如outp_开头的基本是门诊,inp_是住院,drug_是药品,fee_是费用。表结构清楚了,业务边界自然就出来了。
这四块之间靠什么串?靠患者主索引和就诊流水号。一个患者一次就诊生成一个visit_id,挂号、开单、收费、发药全挂在这个 ID 下。你如果发现源码里到处用patient_id直接关联,而没有中间的就诊表,那这套代码的扩展性基本到头了,后面加个复诊逻辑就会翻车。
2.2 表结构里藏着的业务约束
看源码别急着跑,先导出建表语句。重点看三类字段:状态位、金额、时间戳。状态位决定流程能不能往下走,比如挂号表的status从 0(待就诊)到 1(已就诊)到 2(已退号),你改错一个值,整个队列就乱了。金额字段必须用decimal,用double的源码直接放弃,收费对不上账是迟早的事。时间戳要看是datetime还是timestamp,涉及跨天结算的逻辑,时区处理不好就是一笔糊涂账。
-- 典型的挂号表核心字段,注意状态位和金额类型 CREATE TABLE outp_register ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL COMMENT '患者主索引', visit_id VARCHAR(32) NOT NULL COMMENT '就诊流水号', dept_id INT NOT NULL COMMENT '科室', doctor_id INT COMMENT '医生', reg_level TINYINT COMMENT '挂号级别', reg_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '挂号费', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待就诊 1已就诊 2已退号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_visit (visit_id) ) COMMENT='门诊挂号';上面这段建表语句里,visit_id加了唯一索引,这是防止重复挂号的第一道防线。reg_fee用DECIMAL(10,2)而不是FLOAT,因为金额计算不能有精度丢失。status字段没有用枚举类型,是为了方便后续扩展状态而不用改表结构。你拿到任何一套 HIS 源码,先检查这三处,基本能判断代码质量的下限。
2.3 从挂号到发药的一次完整调用链
以最简门诊流程为例:患者挂号 → 医生站开处方 → 收费处划价收费 → 药房发药。对应到代码里,通常是RegisterController调RegisterService写挂号表,然后DoctorController调PrescriptionService写处方主表和明细表,接着FeeController调ChargeService更新处方状态并写收费记录,最后PharmacyController调DispenseService扣库存、写发药记录。每一步都依赖上一步的状态位,不能跳步。
你调试的时候,可以在每个 Service 方法入口打日志,把visit_id和status打出来,这样一眼就能看出卡在哪。常见做法是用 AOP 切面统一记录,但源码里如果没带,手动加几行log.info也够用。
3. 把源码跑起来:环境、依赖与最小启动命令
3.1 JDK、数据库、中间件的版本对齐
医院HIS源码常见的技术栈是 Spring Boot + MyBatis + MySQL + Redis。JDK 版本看pom.xml里的java.version,1.8 和 11 居多,17 的也有但少。MySQL 建议 5.7 或 8.0,注意驱动包版本要匹配,mysql-connector-java8.0 以上用com.mysql.cj.jdbc.Driver。Redis 一般用来存会话和挂号队列,没有的话先把相关配置注释掉,能启动再说。
# 先确认本地环境 java -version mysql --version redis-server --version # 导入数据库,注意字符集用 utf8mb4 mysql -uroot -p -e "CREATE DATABASE his_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p his_db < doc/sql/his_init.sql导入 SQL 的时候,如果报Unknown collation: utf8mb4_0900_ai_ci,说明你的 MySQL 是 5.7 而脚本是 8.0 导出的,把脚本里的utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci即可。这个坑我踩过不止一次,血泪经验就是先看 SQL 文件头部的版本注释。
3.2 配置文件里必须改的四个参数
源码里的application.yml或application.properties,拿到手先改这四处:数据库连接、Redis 地址、文件上传路径、日志级别。数据库连接里url要加useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,不然中文乱码和时间差 8 小时会同时出现。文件上传路径如果写的是 Linux 绝对路径,在 Windows 上跑必然报错,改成相对路径或临时目录。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/his_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 servlet: multipart: location: /tmp/his_upload logging: level: com.his: debugserverTimezone=Asia/Shanghai这个参数不加,挂号时间可能变成前一天,收费对账直接对不上。logging.level.com.his改成 debug 是为了看 MyBatis 打印的 SQL,排查数据问题时比任何工具都快。上传路径用/tmp在 Linux 下没问题,Windows 下改成D:/his_upload并确保目录存在。
3.3 启动类与前端资源的挂载方式
后端启动类一般叫HisApplication,直接mvn spring-boot:run或打 jar 包java -jar his.jar。前端如果是 Vue 或 React,看package.json里的scripts,npm run dev起开发服务器,npm run build出静态文件。有些源码把前端打包后放在src/main/resources/static下,这种直接访问http://localhost:8080就能看到页面。如果前后端分离,注意跨域配置,源码里通常有个CorsConfig类,没有的话自己加一个@CrossOrigin注解在 Controller 上。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }这段跨域配置用了allowedOriginPatterns而不是allowedOrigins,因为后者在allowCredentials=true时不能写*。很多源码在这里翻车,前端一直报 CORS 错误,其实就是这个细节。加上之后重启,前端就能正常调接口了。
4. 二次开发前必须搞清的权限与业务校验
4.1 基于角色的菜单与按钮级控制
HIS 源码的权限模型通常是 RBAC,用户关联角色,角色关联菜单和按钮。菜单控制页面能不能进,按钮控制操作能不能点。你看sys_user、sys_role、sys_menu、sys_role_menu这几张表就够了。关键点在拦截器或过滤器里,判断当前用户有没有某个权限编码。常见做法是用 Shiro 或 Spring Security,源码里如果用了自定义注解加 AOP,那就看注解的value值跟菜单表里的perms字段是否对应。
// 自定义权限注解,配合 AOP 使用 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission { String value(); } // AOP 切面里校验 @Around("@annotation(requiresPermission)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { String perm = requiresPermission.value(); if (!SecurityUtils.hasPermission(perm)) { throw new BusinessException("无权限操作"); } return joinPoint.proceed(); }这段代码里SecurityUtils.hasPermission需要你自己实现,从当前会话里拿用户 ID,查角色权限列表,判断是否包含传入的perm。注意权限编码要跟菜单表里的perms完全一致,大小写敏感。我见过有人把outp:register:add写成outp:register:save,结果按钮一直灰着,排查半天。
4.2 挂号、收费、发药的状态机校验
状态机是 HIS 里最容易出 bug 的地方。挂号状态从待就诊到已就诊,收费状态从待收费到已收费,发药状态从待发药到已发药。每个操作前必须校验当前状态是否允许。比如退号只能对“待就诊”的挂号操作,已经就诊的不能退。收费只能对“已开单未收费”的处方操作。发药只能对“已收费”的处方操作。这些校验如果源码里没写全,你二次开发时一定要补上,否则数据一乱,整个科室的队列都受影响。
public void cancelRegister(Long registerId) { OutpRegister reg = registerMapper.selectById(registerId); if (reg == null) { throw new BusinessException("挂号记录不存在"); } if (reg.getStatus() != 0) { throw new BusinessException("只有待就诊状态才能退号"); } reg.setStatus(2); registerMapper.updateById(reg); // 退号后释放号源,更新排班剩余数量 scheduleMapper.increaseRemain(reg.getDoctorId(), reg.getRegLevel()); }这段退号逻辑里,先查记录,再判状态,最后更新并释放号源。increaseRemain是自定义的更新语句,把医生排班的剩余号加回去。如果你只改状态不释放号源,后面的人就挂不上这个医生的号了,这是典型的业务漏洞。
4.3 药品库存扣减的并发处理
药房发药时要扣库存,多个窗口同时发药,库存扣减必须加锁。源码里常见两种做法:数据库乐观锁(版本号)或 Redis 分布式锁。乐观锁简单,在drug_stock表加version字段,更新时where id=? and version=?,影响行数为 0 就重试。Redis 锁适合并发量大的场景,但要注意锁过期时间和误删问题。
// 乐观锁扣库存 public boolean reduceStock(Long drugId, Integer quantity) { int retry = 3; while (retry-- > 0) { DrugStock stock = stockMapper.selectById(drugId); if (stock.getQuantity() < quantity) { throw new BusinessException("库存不足"); } int rows = stockMapper.reduceStock(drugId, quantity, stock.getVersion()); if (rows > 0) { return true; } } throw new BusinessException("库存扣减失败,请重试"); }对应的 SQL 是update drug_stock set quantity=quantity-#{quantity}, version=version+1 where id=#{drugId} and version=#{version} and quantity>=#{quantity}。注意quantity>=#{quantity}这个条件不能少,否则并发下可能扣成负数。重试次数设 3 次够用,再多说明并发冲突太频繁,该考虑换方案了。
5. 避坑与排查:源码跑不通时先看这五处
5.1 启动报错“找不到符号”或“程序包不存在”
现象:mvn clean install时报cannot find symbol,或者 IDEA 里一片红。原因通常是依赖没下全,或者 JDK 版本不匹配。解决:先看pom.xml里的java.version,改成你本地的 JDK 版本。然后mvn clean install -U强制更新依赖。如果还不行,删掉本地仓库里对应的com/his目录再重新拉。IDEA 里记得 File → Invalidate Caches → Restart。
5.2 登录后菜单空白或接口 403
现象:能登录,但左侧菜单不显示,或者点任何按钮都提示无权限。原因一般是权限数据没初始化,或者角色菜单关联表是空的。解决:查sys_role_menu表有没有数据,没有的话手动插入几条,把管理员角色关联到所有菜单。另外检查登录接口返回的用户信息里有没有permissions字段,前端是根据这个字段渲染按钮的。
5.3 挂号时间比实际时间少 8 小时
现象:挂号记录里的create_time比当前时间早 8 小时。原因:数据库连接串没加serverTimezone=Asia/Shanghai,或者 JVM 时区不对。解决:改连接串,同时在启动参数加-Duser.timezone=Asia/Shanghai。如果用的是 Docker,检查容器时区是不是 UTC,是的话挂载/etc/localtime进去。
5.4 收费金额出现 0.01 的误差
现象:收费明细加起来和总金额差一分钱。原因:用了double或float存金额,或者计算时先除后乘。解决:把所有金额字段改成DECIMAL,Java 里用BigDecimal,并且setScale(2, RoundingMode.HALF_UP)。除法运算一定要先乘后除,比如(a * 100) / b而不是a / b * 100。
5.5 发药后库存没变或变成负数
现象:发药成功但库存没扣,或者扣成了负数。原因:没加乐观锁或锁失效,或者扣减 SQL 的条件写错了。解决:按 4.3 节的方案加版本号,SQL 里必须带quantity>=#{quantity}。如果用了 Redis 锁,检查锁的 key 是不是按药品 ID 维度加的,锁过期时间是不是太短导致业务没执行完就释放了。
6. 从能跑到好用:HIS 源码的进阶改造与验证
6.1 用接口幂等保证挂号不重复
挂号接口如果被重复提交,会生成两条挂号记录。进阶做法是加幂等控制:前端传一个requestId,后端用 Redis 的setnx判断是否已处理。setnx成功才执行业务,执行完设置过期时间。这样即使网络抖动导致重试,也只会有一条记录。
public RegisterResult register(RegisterRequest req) { String key = "reg:idempotent:" + req.getRequestId(); Boolean ok = redisTemplate.opsForValue().setIfAbsent(key, "1", 30, TimeUnit.SECONDS); if (Boolean.FALSE.equals(ok)) { throw new BusinessException("请勿重复提交"); } try { // 原有挂号逻辑 return doRegister(req); } catch (Exception e) { redisTemplate.delete(key); // 失败时删除,允许重试 throw e; } }注意失败时要删 key,否则用户重试会被拦住。过期时间设 30 秒,够覆盖一次正常请求。这个方案对挂号、收费、发药都适用,是投入产出比最高的改造之一。
6.2 用对账脚本验证收费数据一致性
HIS 最怕收费对不上。我一般会写一个对账脚本,每天跑一次,比对处方明细金额之和与收费记录金额是否一致。不一致的记录下来人工核查。脚本用 SQL 就能写,不用写 Java。
-- 找出处方明细总额与收费金额不一致的记录 SELECT p.visit_id, p.total_amount AS pres_amount, c.charge_amount, (p.total_amount - c.charge_amount) AS diff FROM prescription p JOIN charge_record c ON p.visit_id = c.visit_id WHERE ABS(p.total_amount - c.charge_amount) > 0.00;这个查询跑出来如果有数据,说明要么有退费没同步,要么有折扣没算对。每天定时跑,比出了事再查强得多。我习惯把这个脚本放在运维目录,用 cron 每天凌晨执行,结果发到工作群。
6.3 一个具体技巧:用日志追踪一次就诊的全链路
最后分享一个我常用的排查技巧。在visit_id生成的地方打一条日志,格式固定为[VISIT_TRACE] visitId=xxx, step=register,然后在每个关键节点(开单、收费、发药)都打同样格式的日志。出问题时用grep VISIT_TRACE | grep visitId就能把一次就诊的所有操作按时间顺序拉出来,比看数据库快十倍。这个习惯我保持了五年,每次线上问题都是靠它定位。希望帮到你。
本文还有配套的精品资源,点击获取