资讯动态

HIS系统源码拆解:从挂号到发药的核心业务链路与二次开发

发布时间:2026/10/18 5:30:17 来源:尧图企业网站定制
简介一套完整的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起步依赖齐全内嵌容器部署省事ORMMyBatis 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_cardregister_source号源表doctor_id, register_date, remain_countoutpatient_register门诊挂号记录register_id, patient_id, statusoutpatient_prescription处方主表register_id, doctor_id, total_amountprescription_item处方明细表prescription_id, drug_id, quantitydrug_info药品字典drug_id, drug_name, specdrug_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 LambdaQueryWrapperRegisterSource() .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 0Transactional注解保证挂号记录和号源扣减同步成功或同步失败。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(处方状态异常无法发药); } ListPrescriptionItem items itemMapper.selectList( new LambdaQueryWrapperPrescriptionItem() .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.xJDK 用 1.8 最稳妥Maven 需要 3.5 以上。前端是 Vue 2 项目Node.js 建议用 14 或 16npm 版本跟着 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: truecharacterEncodingutf8这个参数非常重要漏掉它就会出现中文乱码。serverTimezoneAsia/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 脚本后前端页面上患者姓名和药品名称显示成问号。原因数据库连接串没带characterEncodingutf8或者导入脚本时客户端的字符集和服务端不一致。解决修改application.yml的数据源 URL加characterEncodingutf8参数若数据已经导入重新执行一次ALTER DATABASE his_db CHARACTER SET utf8mb4并重新导入数据。还有一次我遇到的是 SQL 脚本里有中文注释用mysql -uroot -p --default-character-setutf8 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找到 PIDtaskkill /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 系统的价值在于把门诊流程的完整链路做通了二次开发时尽量在链路中间插节点而不是改链路上的固有逻辑。我后来已经养成一个习惯拿到任何一套医疗信息化源码先把手动走一遍挂号到发药的全流程当成第一优先级链路都走不通的代码改哪里都是填坑。这个习惯帮我避开了好几次交付前夜的重大返工。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑