HIS系统部署与二次开发实战:从数据库初始化到挂号收费主链路
发布时间:2026/9/26 16:56:34来源:尧图网络
简介一套面向小型诊所和医疗机构的轻量级HIS医院信息系统源码包基于ASP.NET Web技术构建覆盖病患管理、挂号、药品、收费、统计报表、医生排班和患者追踪等核心模块。压缩包共451个文件约7.05MB以C#后端代码136个cs为主配以aspx页面、JavaScript脚本、CSS样式、图片素材和少量数据库脚本同时包含合同参考文件、示例数据与说明文档目录结构较完整便于阅读和二次开发。目前已有283人学习或浏览适合医疗信息化初学者、中小型机构IT人员或想快速搭建HIS演示系统的开发者参考。资源可直接部署试用也可拆分各业务模块用于课程设计、毕业设计或产品原型验证对理解HIS业务流程、Web表单开发和三层架构实践有较高参考价值。1. 这个“超级简单商业HIS”到底装了什么先别急着解压拿到“超级简单版本商业HIS系统.rar”这份资源很多人的第一反应是直接右键解压然后双击 exe 或者敲个java -jar。我之前拆过不下十套类似的医疗系统源码包可以负责任地说真正卡住 80% 接手者的地方从来不是代码高深而是你根本不知道这个压缩包里哪些文件有用、哪些是给实施工程师的交付物、哪些纯粹是开发环境残留。这份资源之所以叫“超级简单”是因为它的业务范围只覆盖中小诊所或医院信息科做演示用的最小闭环——挂号、收费、发药、退费、基础字典管理没有医保接口、没有电子病历、没有复杂的排班系统。换句话说它适合三类人想快速跑通 HIS 业务流程的在校生、准备做医疗信息化二次开发的初级工程师、以及刚转行做 HIS 实施、想搞明白“医院到底怎么用系统”的新人。但“简单”不等于“解压就能跑”接下来的内容会带你从头理清楚环境、数据、代码、踩坑一步一步把它变成一套真正能点开用的系统。2. 把压缩包变成能跑的系统环境准备与数据库初始化2.1 目录结构先看懂bin、sql、config、webapp 各管什么这套 HIS 采用的是前后端分离的经典结构后端基于 Spring Boot前端是 Vue 打包后的静态文件。rar 解压后你会看到四个核心文件夹和一个README.txt。bin目录下是启动脚本和 Tomcat 内嵌配置sql目录存放的是初始化数据库脚本里面通常有一个init.sql和update.sqlconfig目录是配置中心存放application.yml和日志配置webapp目录是前端构建产物也就是浏览器访问时加载的页面资源。千万不要上来就改代码。第一步是确认你本机的环境版本JDK 1.8 以上MySQL 5.7 或 8.0Node 的版本其实不需要因为前端已经构建好你只需要一个能访问的浏览器。这套系统对内存要求不高2G 内存跑起来绰绰有余。真正需要注意的是 MySQL 的sql_mode如果开启ONLY_FULL_GROUP_BY后面很多带 GROUP BY 的统计查询会直接报错所以先执行一条关闭命令SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION;这条命令的作用是去掉ONLY_FULL_GROUP_BY严格模式原理是 HIS 系统里大量报表查询会按科室、日期、收费类型分组如果分组字段没有全选查询列老版本 MySQL 会直接拒绝执行。执行完最好重启 MySQL 服务让全局参数永久生效。2.2 数据库脚本执行顺序为什么先建库再导数据打开sql目录你会看到init.sql和update.sql。init.sql负责建库建表文件开头通常有CREATE DATABASE his_db这样的语句update.sql是后续迭代的变更脚本包含新增字段和索引。我见过很多人直接把两个文件一起导入结果update.sql里引用的字段在旧表上不存在直接报错。正确的执行顺序是先执行init.sql再执行update.sql。具体的导入命令以 Windows 环境为例mysql -u root -p -h 127.0.0.1 init.sql mysql -u root -p -h 127.0.0.1 his_db update.sql这里第二条命令指定了数据库名his_db因为update.sql里的语句默认不带USE如果不指定MySQL 会报“No database selected”。参数上-u root是用户名-p会提示输密码-h指定主机地址。如果你本机没有mysql命令行工具也可以用 Navicat 之类的客户端直接运行 SQL 文件但注意导入顺序不能颠倒。导入完成后验证一下核心表是否存在USE his_db; SHOW TABLES;正常情况下你会看到patient、doctor、department、drug、prescription、charge_record等二十多张表。如果表数量差很多或者缺少某张关键表大概率是脚本中途报错被中断回到第 2.2 节重新导入不要接着往下改代码。2.3 配置文件里的三个必改项数据库连接、端口、上传路径启动系统的前一步必须打开config/application.yml找到spring.datasource这一行。这里默认配置是127.0.0.1:3306/his_db用户名root密码123456。如果你本机的 MySQL 密码不是这个不修改的话启动日志会一直报Access denied for user rootlocalhost。把密码改成你自己的连接池建议保持默认因为这套系统并发量不大maximum-pool-size: 10足够。spring: datasource: url: jdbc:mysql://127.0.0.1:3306/his_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword hikari: maximum-pool-size: 10第二个必改项是端口。默认是8080如果被其他程序占用启动会直接崩溃。把server.port改成8081或9090。第三个是文件上传路径配置里的his.upload-dir默认指向D:/his_upload如果你在 Linux 上跑或者不想用 D 盘改成/home/his/upload并确认目录存在否则后台上传患者照片时会报“目录不存在”。server: port: 8081 his: upload-dir: /home/his/upload修改完这三个地方就可以启动后端了。在项目根目录执行java -jar his-server.jar --spring.config.locationconfig/application.yml启动成功的标志是日志出现Started HisApplication in x.x seconds。如果卡在某个地方不动优先看数据库连接是不是超时其次是端口有没有被占用。前端不需要单独启动直接访问http://127.0.0.1:8081如果看到登录页说明资源已经活了。3. 挂号-收费-发药这条主链路核心表结构与业务流转3.1 从 patient 到 prescription五张核心表的关系别被二十多张表吓倒真正每天都在用的核心链路只有五张表patient患者档案、department科室、doctor医生、prescription处方、charge_record收费记录。它们的关系是患者先建档然后去科室找医生医生开处方处方关联药品和收费项收费时写收费记录。一张处方可以包含多个药品明细所以处方一般拆成主表和子表prescription是主表prescription_item是子表。一个典型的查询把患者、医生、处方串起来SELECT p.patient_name, d.doctor_name, pr.prescription_no, pr.total_amount FROM prescription pr JOIN patient p ON pr.patient_id p.id JOIN doctor d ON pr.doctor_id d.id WHERE p.patient_name LIKE 张% ORDER BY pr.create_time DESC;这条 SQL 的关键是JOIN的连接顺序先连患者再连医生因为prescription表里同时有patient_id和doctor_id两个外键。如果你把连接条件写反比如ON pr.patient_id d.id查询出来的数据会完全错乱——这就是外键命名不规范的坑这个系统统一使用xxx_id而不是xxxId所以写 SQL 时不要想当然用驼峰。3.2 收费单据状态流转未收费、已收费、已退费如何设计charge_record表里有一个status字段取值范围是0未收费、1已收费、2已退费。很多新手不理解为什么要有状态直接把收费记录 DELETE 掉不就行了但医院要求每一笔操作都有痕迹退费不是删除而是把状态改成2同时原收费金额在报表里会被统计为“退费金额”。这个设计在代码里对应的处理逻辑是public void refund(Long chargeId) { ChargeRecord record chargeRecordMapper.selectById(chargeId); if (record.getStatus() ! 1) { throw new BusinessException(只有已收费的单据才能退费); } record.setStatus(2); record.setRefundTime(new Date()); chargeRecordMapper.update(record); }这里的判断逻辑很重要如果单据已经是“未收费”再执行退费就会把状态改成“已退费”造成逻辑上说不通。所以入口先校验status ! 1直接抛异常。实际开发中退费还伴随库存回补和统计数据回滚这就是为什么状态字段比直接删除更安全——你随时能查出“今天退了几笔、退了多少金额”。3.3 一个典型的挂号收费操作SQL 与接口调用顺序模拟一个患者从挂号到取药的完整操作你至少需要四个接口POST /api/patient建档、POST /api/registration挂号、POST /api/prescription提交处方、POST /api/charge收费。顺序不能乱因为挂号需要患者 ID处方需要医生 ID收费需要处方 ID。接口调用顺序在 Postman 里可以这样模拟# 1. 建档 curl -X POST http://127.0.0.1:8081/api/patient \ -H Content-Type: application/json \ -d {patientName:测试患者,gender:1,age:30,phone:13800000000} # 2. 挂号 curl -X POST http://127.0.0.1:8081/api/registration \ -H Content-Type: application/json \ -d {patientId:1,departmentId:3,doctorId:5,registrationFee:15}curl里的-X POST指定请求方法-H传 JSON 头-d是请求体数据。注意挂号参数里departmentId和doctorId必须真实存在否则会报外键错误。挂号接口返回的registrationId要记下来后面收费时要用。收费接口接收的是一笔完整单据包含处方明细和收费金额curl -X POST http://127.0.0.1:8081/api/charge \ -H Content-Type: application/json \ -d {prescriptionId:10,chargeItems:[{drugId:88,quantity:2,price:3.5}],totalAmount:7.0}调用完收费接口再去查询charge_record表你会看到一条status1的记录。如果状态还是0八成是收费接口里的事务没有提交检查一下你的方法上有没有加Transactional注解。这个注解的作用是让“插入收费记录”和“更新处方状态”在同一个数据库事务里任何一个失败都会整体回滚不会出现“钱收了但处方还是未收费”的尴尬情况。4. 让新患者、新药品快速入门字典与基础数据维护4.1 药品字典和收费项目的分类设计这套系统的drug表不只存药品还存所有可收费项目比如检查费、治疗费。区分方式是item_type字段1代表药品2代表检查项目3代表治疗项目。为什么要这样设计因为收费窗口在结算时不管你是开药还是开检查最终都要走进charge_record统一编码能让报表统计更简单。新增一条药品记录的典型 SQLINSERT INTO drug (drug_code, drug_name, spec, unit, price, stock, item_type, pinyin_code) VALUES (YP002233, 阿莫西林胶囊, 0.25g*24粒, 盒, 12.50, 500, 1, AMXL);这里的pinyin_code很有用收费员输入拼音首字母就能带出药品不用记编码。如果你导入一批药品price建议统一保留两位小数否则收费时可能出现 0.1 元的分差。stock是库存数量初始值不要写负数否则发药功能会被判定为缺药。4.2 科室与医生的关联逻辑department和doctor是一对多关系doctor表里有个department_id字段。挂号时前端先加载科室列表选中科室后再加载该科室下的医生。这个联动逻辑在前端是写死的如果你改了科室表的 ID别忘了检查医生的关联。修改医生所属科室时直接用一条更新语句UPDATE doctor SET department_id 7 WHERE id 12;但需要注意如果医生已经存在历史挂号记录修改关联后历史记录里的department_id也会跟着变因为挂号单存的是医生 ID查询时临时关联科室。如果医院要求“历史数据保持原科室”你就需要在挂号记录表冗余一个department_name字段这个系统暂时没做所以二次开发时要想清楚。4.3 修改基础数据后为什么需要重启或清缓存我遇到过很多人在后台增加了一个药品前台收费窗口一直刷不出来。原因不是没保存而是这个系统的药品字典做了本地缓存。缓存逻辑在DrugCacheService里启动时加载全量药品到 JVM 内存查询接口直接读缓存这样收费窗口响应快。修改代码或手动改了数据库后需要强制刷新缓存。这个服务提供了一个 APIcurl -X POST http://127.0.0.1:8081/api/cache/refresh执行完这个接口会重新从数据库加载药品和科室信息。如果接口不存在那就只能重启服务。我的习惯是每次导入新药品后先访问这个刷新接口再用前台搜索验证。否则你守着一个旧缓存的数据查来查去都查不到浪费时间还以为代码写错了。5. 避坑这套 HIS 最常翻车的五个地方5.1 现象数据库脚本导入报错导入init.sql时报Unknown collation: utf8mb4_0900_ai_ci。原因是你本机的 MySQL 版本低于 8.0而脚本里写的是 MySQL 8.0 默认的排序规则。解决方法是把脚本里的utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci或者用编辑器批量替换后再导入。我一般直接用 NotePad 打开CtrlH 全局替换。5.2 现象登录后白屏前端页面加载出来了输入账号密码点登录页面一片白控制台报Failed to load resource: 401。原因是系统默认 Token 过期时间只有 30 分钟你从前台打开页面到登录输入完密码如果超过 30 分钟登录请求携带的临时验证码已经失效。解决方法是重新刷新登录页或者在后端把 Token 过期时间调长。在application.yml里找到token.expire-hours改成8保存重启。5.3 现象挂号成功但收费窗口看不到患者挂号接口返回成功但收费窗口的待收费列表里没有这位患者。查数据发现registration表有记录但charge_record没有对应数据。原因是收费列表的查询条件是WHERE status 0而挂号接口只写了挂号单并没有自动生成一条未收费的charge_record。解决方法是检查挂号接口的代码看有没有在挂号后插入一条挂号费的收费记录。如果系统设计里挂号费是单独收那收费窗口要在“挂号费收费”模块里操作而不是普通收费列表。5.4 现象药品库存成负数发药后库存变成-3这种翻车属于业务逻辑没锁住。原因是发药接口的库存扣减逻辑是先查询再更新没有加锁或者乐观锁。两条并发请求同时读到库存为 5各自扣 3都写入库存 2实际应该变成 -1。解决方式是在drug表加一个version字段更新时带上WHERE version ?更新成功后version1。如果在联调阶段发现了这个问题最快的临时补救是把库存字段改成事务里SELECT ... FOR UPDATE但长期运行还是推荐乐观锁。5.5 现象修改端口后本机都连不上把server.port改成9090后访问http://127.0.0.1:9090显示拒绝连接。大概率是系统有防火墙或者你改了端口但启动脚本里硬编码了旧端口。查看启动日志如果看到Tomcat started on port 9090说明端口已经生效那就是防火墙拦截。Windows 上执行netsh advfirewall firewall add rule nameHIS9090 dirin actionallow protocolTCP localport9090放行即可。如果是 Linux用firewall-cmd --add-port9090/tcp --permanent再reload。6. 把这份源码吃透的训练方法从改菜单到加一张报表6.1 用半小时给收费菜单加一个“今日统计”入口不要急着全盘通读代码而是找一个具体功能去改。比如给左侧菜单加一个“今日统计”入口。前端菜单配置在webapp/static/config/menu.json里面是一个 JSON 数组每一项有name、path、icon。新增一项{ name: 今日统计, path: /report/today, icon: el-icon-data-analysis }保存后刷新页面你会发现菜单出现了但点击是空白页因为还没有对应的路由组件。在webapp/src/router/index.js里加一行{ path: /report/today, component: () import(/views/report/TodayReport.vue) }component用的是动态导入这样前端构建时会单独分包不会影响首屏加载速度。如果你不会写.vue文件可以先新建一个只显示“今日已收费金额”的简单组件功能后面再补齐。这一步的核心是让你理解菜单和路由的映射关系。6.2 加一张简单报表的完整步骤SQL、接口、前端假设你要做“今日已收费金额汇总”先写好 SQL 并确认结果符合预期SELECT DATE(charge_time) AS biz_date, SUM(total_amount) AS total FROM charge_record WHERE status 1 AND DATE(charge_time) CURDATE() GROUP BY DATE(charge_time);然后去后端controller包新建一个接口RestController RequestMapping(/api/report) public class TodayReportController { Autowired private ChargeRecordMapper chargeRecordMapper; GetMapping(/today/total) public BigDecimal todayTotal() { return chargeRecordMapper.sumTodayCharged(); } }这里的sumTodayCharged需要在 Mapper XML 里写对应的 SQL或者在注解里直接写。参数上没有什么神秘的东西唯一要注意的是金额字段用BigDecimal而不是Double因为Double会有浮点误差财务报表容不得差一分钱。写完后启动后端访问http://127.0.0.1:8081/api/report/today/total验证返回结果再把前端组件里异步请求这个接口渲染到页面上。6.3 验证改动是否破坏原有业务三张表的复核脚本改完之后不要只看功能正常就收工我每次都会执行三个复核 SQL查收费记录表有没有被误改数据、查处方表状态是否一致、查库存表是否有负数。具体来说收费记录总数和金额合计应该和改动前一致除新增的数据外库存表里所有stock字段不能有小于 0 的值处方表里每个已缴费处方必须有对应的收费记录。-- 复核库存 SELECT drug_code, drug_name, stock FROM drug WHERE stock 0; -- 复核处方和收费对应 SELECT pr.prescription_no, cr.status FROM prescription pr LEFT JOIN charge_record cr ON pr.id cr.prescription_id WHERE pr.prescription_no RC20240101001;如果复核发现库存负数说明你执行了发药但没做库存校验如果处方状态和收费状态对不上说明事务控制没做好。这套系统的坑基本都集中在事务和状态同步上你只要把这两个点盯住后续二开会顺畅很多。从那以后我每次接手一套新 HIS 源码包都强制走一遍“解压、看目录、导数据、改配置、跑通一条主链路、查三张表”这套流程半个下午就能确定系统能不能用、能改多深。希望这套流程对你也有用希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网