简介:一套面向小型诊所和医疗机构的轻量级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.sql;config目录是配置中心,存放application.yml和日志配置;webapp目录是前端构建产物,也就是浏览器访问时加载的页面资源。
千万不要上来就改代码。第一步是确认你本机的环境版本:JDK 1.8 以上,MySQL 5.7 或 8.0,Node 的版本其实不需要,因为前端已经构建好,你只需要一个能访问的浏览器。这套系统对内存要求不高,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 'root'@'localhost'。把密码改成你自己的,连接池建议保持默认,因为这套系统并发量不大,maximum-pool-size: 10足够。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/his_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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.location=config/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表,你会看到一条status=1的记录。如果状态还是0,八成是收费接口里的事务没有提交,检查一下你的方法上有没有加@Transactional注解。这个注解的作用是让“插入收费记录”和“更新处方状态”在同一个数据库事务里,任何一个失败都会整体回滚,不会出现“钱收了但处方还是未收费”的尴尬情况。
4. 让新患者、新药品快速入门:字典与基础数据维护
4.1 药品字典和收费项目的分类设计
这套系统的drug表不只存药品,还存所有可收费项目,比如检查费、治疗费。区分方式是item_type字段:1代表药品,2代表检查项目,3代表治疗项目。为什么要这样设计?因为收费窗口在结算时,不管你是开药还是开检查,最终都要走进charge_record,统一编码能让报表统计更简单。
新增一条药品记录的典型 SQL:
INSERT 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 内存,查询接口直接读缓存,这样收费窗口响应快。
修改代码或手动改了数据库后,需要强制刷新缓存。这个服务提供了一个 API:
curl -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++ 打开,Ctrl+H 全局替换。
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 = ?,更新成功后version+1。如果在联调阶段发现了这个问题,最快的临时补救是把库存字段改成事务里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 name="HIS9090" dir=in action=allow protocol=TCP localport=9090放行即可。如果是 Linux,用firewall-cmd --add-port=9090/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 源码包,都强制走一遍“解压、看目录、导数据、改配置、跑通一条主链路、查三张表”这套流程,半个下午就能确定系统能不能用、能改多深。希望这套流程对你也有用,希望帮到你。
本文还有配套的精品资源,点击获取