简介:一套完整的HIS系统源码包,面向医疗信息化开发与实施人员,也适合高校相关专业学生学习使用。内容包括药房管理、护士站、药库管理、住院结算等核心模块,覆盖药品库存、电子处方、医嘱执行、护理记录、费用结算和医保报销等实际业务环节,能帮助读者建立医院信息系统的整体结构认知。资源共4290个文件,压缩包约6.51MB,以pas源码、dfm窗体、dpr工程、sql数据库脚本为主,另有dpk包、说明文档和少量资源文件,便于加载工程、分析界面与数据表结构。已有2599人学习浏览,适合需要参考传统C/S架构HIS项目或进行二次开发的读者。整体来看,药房与药库采用独立管理,住院结算模块强调查费明细与医保处理,护士站模块注重医嘱执行与护理记录;文档与维护目录还提供部署、更新和排错资料,便于快速上手与持续维护。
1. 一套完整 HIS 系统的落地价值:把挂号、医嘱、发药串成一条数据链
很多第一次拿 his 系统源码做二次开发的工程师,以为这就是一堆增删改查页面拼起来的后台管理系统,真正把代码跑起来才发现,事情没那么简单。这套完整的 HIS 系统以门诊流程为主线,覆盖号表管理、门诊收费、医生工作站、药房发药、住院入出转、电子病历等核心业务,代码里最值钱的地方不是某个页面写得有多花哨,而是挂号、收费、医嘱、发药这几件事被串在同一条数据链路上。如果你要给医院信息科做定制交付,或者想拿一套结构干净的医疗信息化项目当脚手架,这套源码是值得花一个周末拆开看的。
2. 技术选型与项目结构:前后端分离下 HIS 系统的模块边界
2.1 技术栈全景:为什么是 Spring Boot + Vue + MySQL
这套系统采用前后端分离架构,后端以 Spring Boot 2.x 为主框架,ORM 层使用 MyBatis Plus,数据库用 MySQL,缓存层接了 Redis,登录鉴权用的是 JWT 令牌方案。前端部分基于 Vue 2 生态,UI 组件库是 Element UI,接口请求统一走 Axios。这个组合在医疗信息化项目里非常常见,医院内网环境对部署要求简单直接,后端打成 war 或 jar 包,前端构建成静态文件交给 Nginx 托管,一条链路就能跑通。
我拆这套源码时最直观的感受是:选型上没有任何炫技成分。HIS 系统业务表多、查询条件复杂、权限模型细,MyBatis Plus 的条件构造器能省掉大量 XML 手写 SQL;而 JWT 方案适合前后端分离下的无状态接口,不需要在多个服务之间同步 Session。技术栈清单如下:
| 技术层 | 选型 | 关键理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.x | 起步依赖齐全,内嵌容器部署省事 |
| ORM | MyBatis Plus | 单表查询和分页效率高,代码生成方便 |
| 数据库 | MySQL 5.7 / 8.0 | 医院信息系统最普遍的存储底座 |
| 缓存 | Redis | 验证码、号源计数、Token 黑名单场景 |
| 前端 | Vue 2 + Element UI | 组件成熟,表格表单密集场景开发快 |
| 鉴权 | JWT | 无状态,适合前后端分离架构 |
2.2 目录结构梳理:后端模块与前端页面的对应关系
项目后端采用多模块 Maven 结构,业务边界划分得很清楚。公共模块放统一返回结果、异常处理、工具类;系统模块管用户、角色、菜单权限;业务模块承接门诊、药房、住院等核心业务;还有一个定时任务模块,用来处理挂号号源每日初始化、报表统计之类的任务。
his-admin ├── his-common # 公共模块:统一返回、异常、工具类 ├── his-system # 系统模块:用户、角色、菜单、字典 ├── his-business # 业务模块:门诊、收费、药房、住院 │ ├── controller # 接口层,接收前端请求 │ ├── service # 业务层,事务与业务规则 │ ├── mapper # 数据访问层,MyBatis Plus Mapper │ └── entity # 数据库实体类 └── his-quartz # 定时任务:号源初始化、数据统计前端目录和业务模块一一对应,views/outpatient放挂号收费页面,views/pharmacy放药房发药退药页面,views/system放用户和菜单管理页面。接口封装统一放在api目录下,每个业务模块一个 JS 文件。这种前后端按模块对齐的组织方式,最大好处是改需求时定位代码路径非常快,比如要调整处方审核流程,只需要同时打开后端his-business里的处方 service 和前端views/pharmacy里的发药页面,两处都在同一个业务边界内。
2.3 数据库设计要点:核心表结构与表间关系
HIS 系统的核心数据模型围绕「一次就诊」展开。患者先建档案,挂号生成一条就诊记录,医生在就诊记录下开医嘱和处方,药房凭处方发药并扣减库存。表与表之间靠register_id这个逻辑主键关联起来。重点表包括:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| patient_info | 患者档案 | patient_id, name, id_card |
| register_source | 号源表 | doctor_id, register_date, remain_count |
| outpatient_register | 门诊挂号记录 | register_id, patient_id, status |
| outpatient_prescription | 处方主表 | register_id, doctor_id, total_amount |
| prescription_item | 处方明细表 | prescription_id, drug_id, quantity |
| drug_info | 药品字典 | drug_id, drug_name, spec |
| drug_stock | 药品库存 | drug_id, stock, warehouse_id |
这套表设计里最容易忽略的是register_source号源表。它记录每个医生某一天的剩余号数,挂号接口扣减号源和生成挂号记录必须放在同一个事务里,否则会出现号源扣了但挂号记录没生成的情况。库存表也做了同样的事务控制,发药扣库存时要在 SQL 层面加上库存充足判断,这部分后面第三章节讲代码时会具体拆。
3. 核心模块拆解:一个门诊患者从挂号到取药的完整链路
3.1 挂号收费模块:号源锁定与费用结算的事务边界
挂号是整条链路的入口。我在his-business模块里找到了RegisterService.createRegister方法,它做了三件事:校验号源是否充足,插入挂号记录,扣减号源剩余量。这三步全部包在事务里,任何一个环节失败都会整体回滚。
@Transactional(rollbackFor = Exception.class) public RegisterResult createRegister(RegisterRequest request) { // 1. 查询当天指定医生的有效号源 RegisterSource source = sourceMapper.selectOne( new LambdaQueryWrapper<RegisterSource>() .eq(RegisterSource::getDoctorId, request.getDoctorId()) .eq(RegisterSource::getRegisterDate, request.getRegisterDate()) .eq(RegisterSource::getStatus, 0) .last("limit 1")); if (source == null || source.getRemainCount() <= 0) { throw new BusinessException("当前医生号源已挂满"); } // 2. 创建挂号记录,status 0-待就诊 OutpatientRegister register = new OutpatientRegister(); register.setPatientId(request.getPatientId()); register.setDoctorId(request.getDoctorId()); register.setRegisterType(request.getRegisterType()); register.setStatus("0"); registerMapper.insert(register); // 3. 号源剩余量减一,更新前校验剩余数 int rows = sourceMapper.reduceRemainCount(source.getSourceId()); if (rows == 0) { throw new BusinessException("号源已被其他患者挂完,请重新选择"); } return new RegisterResult(register.getRegisterId()); }这里最关键的是reduceRemainCount方法对应的 SQL,它用了条件更新来防并发超挂:
update register_source set remain_count = remain_count - 1, version = version + 1 where source_id = #{sourceId} and remain_count > 0@Transactional注解保证挂号记录和号源扣减同步成功或同步失败。LambdaQueryWrapper是 MyBatis Plus 的条件构造器,用 Lambda 表达式引用实体字段,避免硬编码字段字符串。remain_count > 0这个条件是防超挂的关键,两条并发请求同时进来时,数据库行锁会保证只有一个更新成功。如果不用这个条件,而是在 Java 代码里先查询再判断再更新,高并发下就会出现两个患者挂到同一个号源的问题。
3.2 医生工作站:医嘱录入与处方保存的联动
医生工作站是业务规则最密集的模块。医生开方时,前端把处方主表和处方明细表一次性提交到后端,后端需要校验药品是否存在、剂量是否合理、总价是否计算正确。这套系统把处方主表和明细表分别落库,用prescription_id关联。
@Transactional(rollbackFor = Exception.class) public Long createPrescription(PrescriptionDTO dto) { // 1. 校验就诊记录状态,必须是待就诊或就诊中 OutpatientRegister register = registerMapper.selectById(dto.getRegisterId()); if (register == null || !"0".equals(register.getStatus())) { throw new BusinessException("就诊记录状态异常,无法开立处方"); } // 2. 保存处方主表 OutpatientPrescription prescription = new OutpatientPrescription(); prescription.setRegisterId(dto.getRegisterId()); prescription.setDoctorId(dto.getDoctorId()); prescription.setTotalAmount(dto.getTotalAmount()); prescription.setStatus("0"); // 0-未收费 prescriptionMapper.insert(prescription); // 3. 批量保存处方明细 for (PrescriptionItemDTO item : dto.getItems()) { PrescriptionItem detail = new PrescriptionItem(); detail.setPrescriptionId(prescription.getPrescriptionId()); detail.setDrugId(item.getDrugId()); detail.setQuantity(item.getQuantity()); detail.setAmount(item.getAmount()); itemMapper.insert(detail); } return prescription.getPrescriptionId(); }这段代码展示了主从表保存的常规处理:先插主表拿到主键,再循环插入明细表,整体放在一个事务里。明细表数据量不大,循环插入性能可以接受,如果处方明细超过几十条,可以改成批量插入。这里的status字段是流程状态机的一部分,0-未收费、1-已收费、2-已发药,后续收费和发药模块都会根据这个状态做流转校验,避免出现处方还没收费就直接发药的情况。
3.3 药房发药:处方状态更新与库存扣减的防超卖处理
药房发药这步我重点关注了库存扣减的并发处理。发药接口的入参是处方号,后端先查出处方和明细,校验处方状态必须是已收费,然后逐条扣减药品库存。
@Transactional(rollbackFor = Exception.class) public void dispenseDrug(Long prescriptionId) { OutpatientPrescription prescription = prescriptionMapper.selectById(prescriptionId); if (prescription == null || !"1".equals(prescription.getStatus())) { throw new BusinessException("处方状态异常,无法发药"); } List<PrescriptionItem> items = itemMapper.selectList( new LambdaQueryWrapper<PrescriptionItem>() .eq(PrescriptionItem::getPrescriptionId, prescriptionId)); for (PrescriptionItem item : items) { int rows = drugStockMapper.deductStock(item.getDrugId(), item.getQuantity()); if (rows == 0) { throw new BusinessException("药品库存不足:" + item.getDrugId()); } } // 全部扣减成功后再更新处方状态 prescription.setStatus("2"); prescriptionMapper.updateById(prescription); }库存扣减的 SQL 同样使用了条件更新的思路,这也是一段可以直接抄走的写法:
update drug_stock set stock = stock - #{quantity} where drug_id = #{drugId} and stock >= #{quantity}stock >= #{quantity}这个条件保证了扣减不会把库存扣成负数。返回影响行数为 0 时说明库存不够或者药品不存在,需要回滚整个发药事务。这里有一个细节值得注意:发药时没有用悲观锁for update,而是用条件更新加受影响行数判断,因为悲观锁在并发高的时候会阻塞其他事务,条件更新更轻量。药品库存字段本身是整数型,配合这个 SQL 足够应对医院门诊正常的并发量。
4. 本地搭建运行指南:从拉取代码到跑通全流程
4.1 环境准备:JDK、Node.js、MySQL 的版本选择
在动手之前把环境配好,能省掉后面一半的排查时间。这套 HIS 系统后端基于 Spring Boot 2.x,JDK 用 1.8 最稳妥,Maven 需要 3.5 以上。前端是 Vue 2 项目,Node.js 建议用 14 或 16,npm 版本跟着 Node 走就行。数据库用 MySQL 5.7 及以上,字符集统一用utf8mb4,不然患者姓名里的生僻字容易乱码。
我一般会先检查这三个命令的输出,确认环境变量没问题再往下走:
java -version mvn -version node -v如果mvn -version提示找不到命令,说明 Maven 没有配到PATH里,需要手动设置MAVEN_HOME,并把 Maven 的bin目录追加到 Path。前端部分如果 npm 安装依赖时卡住,可以换成国内镜像源,这是 Vue 项目最常见的安装加速方式。Redis 也别忘了启动,登录验证码和 Token 黑名单依赖它。
4.2 初始化数据库与修改配置
项目里通常会带一个sql目录,里面是初始化脚本,包含建库、建表、初始化管理员账号和字典数据。导入时注意执行顺序,先建库再导入表结构和数据。我习惯用命令行导入,避免图形客户端工具在编码上做文章。
mysql -uroot -p < his_db.sql导入后要修改后端配置文件application.yml,重点是数据源和 Redis 地址。下面这段配置是核心部分:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/his_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: truecharacterEncoding=utf8这个参数非常重要,漏掉它就会出现中文乱码。serverTimezone=Asia/Shanghai是 MySQL 8.0 的必填项,不填会报时区错误。map-underscore-to-camel-case开启后,数据库字段remain_count能自动映射到实体属性remainCount,省掉大量 XML 映射配置。如果连接的是 MySQL 5.7,驱动类可以改回com.mysql.jdbc.Driver。
4.3 启动后端与前端:常见启动方式
后端是标准 Spring Boot 工程,在根目录执行 Maven 启动命令即可。第一次启动会下载依赖,时间长短取决于网络状况,看到Started Application的日志说明启动成功。
mvn spring-boot:run前端需要先安装依赖,然后启动开发模式。开发模式下 Vite 或 webpack-dev-server 会监听文件变化,改前端代码保存后浏览器自动刷新。
cd his-ui npm install npm run dev前端启动后默认访问http://localhost:8080,如果端口冲突,在vue.config.js里改devServer.port。后端接口地址如果不在同域,需要在vue.config.js里配置代理转发,把/api开头的请求代理到后端 8080 端口。初始登录账号一般是admin,密码存在数据库初始化脚本里,通常是admin123或123456,登录后第一件事改成自己知道的密码。
注意:如果前后端部署在同一个 Nginx 下,前端静态文件和反向代理配置要分开写。前端
dist目录作为站点根目录,/api路径反向代理到后端服务,这是最容易配置出错的地方。
5. 常见问题排查与避坑记录:这份源码里最容易被绊倒的地方
5.1 数据库连接乱码与数据导入报错
现象:导入 SQL 脚本后,前端页面上患者姓名和药品名称显示成问号。原因:数据库连接串没带characterEncoding=utf8,或者导入脚本时客户端的字符集和服务端不一致。解决:修改application.yml的数据源 URL,加characterEncoding=utf8参数;若数据已经导入,重新执行一次ALTER DATABASE his_db CHARACTER SET utf8mb4,并重新导入数据。
还有一次我遇到的是 SQL 脚本里有中文注释,用mysql -uroot -p --default-character-set=utf8 < his_db.sql导入时指定字符集,问题直接消失。
5.2 前端登录后接口 401 或 Token 失效
现象:登录页面能正常访问,账号密码也对,但登录成功跳转主页后,所有接口都返回 401。原因:这类系统用 JWT 做鉴权,前端 Axios 拦截器在请求头里带Authorization字段,而后端接口路径过滤配置没放行登录接口,或者 Token 刷新机制没生效。解决:先打开浏览器开发者工具,看登录接口的响应头里是否返回了 Token;再看请求列表里接口是否带了Authorization头;最后检查后端 Shiro 或 JWT 过滤器的白名单配置,确认/login、/captcha路径放行。
这条血泪经验来自我一次前端能登录但所有业务请求都失败的排查:问题出在前端请求封装时把 Token 字段名写成了token,后端拦的是Authorization,字段对不上,改一行代码就通了。
5.3 药品库存扣成负数
现象:同一个药品同时被两个窗口发药,后台日志没报错,但库存字段变成负数。原因:库存扣减 SQL 没有加stock >= #{quantity}条件,Java 代码里先用selectById查出库存,判断足够后再执行update,并发场景下两步之间有间隙。解决:把扣减 SQL 改成update drug_stock set stock = stock - #{quantity} where drug_id = #{drugId} and stock >= #{quantity},用受影响行数判断是否扣减成功,这也是 3.3 节里那段代码的正确版本。从那以后我写库存类更新都会先看 SQL 有没有条件保护,这是血泪换来的习惯。
5.4 占用的端口没有关干净
现象:后端启动时提示Port 8080 was already in use。原因:之前启动的应用没有完全退出,或者 IDE 里热部署残留了进程。解决:用netstat -ano | findstr 8080找到 PID,taskkill /pid <pid> /f强制关闭。如果是 Linux 环境,用lsof -i:8080和kill -9处理。端口冲突本身是小问题,但在集成调试阶段特别容易让人误判成代码问题,白白浪费半小时排查时间。
6. 二次开发建议:把通用 HIS 拆成适合科室的专属流程
最常见的改造需求是「按科室定制流程」。比如某医院健康管理中心要在门诊流程里加一个体检套餐业务,这时候不需要动挂号核心链路,利用系统现有的字典表就能扩展。在科室字典里新增一个体检科,套餐类型挂到收费项目字典下,然后在前端views/outpatient下新增一个physical_exam页面,接口走已有的处方保存逻辑,把套餐项当作特殊处方明细插入。这种改法对核心事务代码零侵入,风险最小。
如果要做更深度的改造,我建议从两个扩展点入手。第一个是医嘱类型的枚举扩展,新增检验、检查、处置等类型,后端校验逻辑里加一个类型分支判断;第二个是收费模块的费用明细拆分,把收费记录拆成药品费和检查费两类,为后续对接 LIS、PACS 系统留好数据位。这类对接通常没有标准协议,常见做法是后端提供一个IntegrationService,定时从 HIS 业务表里捞待检查申请数据,转成标准消息推送给外部系统。
权限菜单的改造也有一套固定打法。后端sys_menu表加一条记录,前端在路由配置文件里注册对应页面路径,角色授权后新菜单才会出现在侧边栏。注意菜单路由的component路径一定要和views目录下的文件实际位置对得上,否则页面会白屏报错,这是新手最容易翻车的地方。改动完记得重启后端清空一次 Redis 缓存,菜单权限有缓存,频繁变更有时候会不生效。
这套 HIS 系统的价值在于把门诊流程的完整链路做通了,二次开发时尽量在链路中间插节点,而不是改链路上的固有逻辑。我后来已经养成一个习惯:拿到任何一套医疗信息化源码,先把手动走一遍挂号到发药的全流程当成第一优先级,链路都走不通的代码,改哪里都是填坑。这个习惯帮我避开了好几次交付前夜的重大返工。希望帮到你。
本文还有配套的精品资源,点击获取