简介:一份面向小型诊所与医疗信息化初学者的轻量级HIS系统源码包,简化了传统医院信息系统的复杂度,围绕病患管理、挂号、药品、收费、排班等常见业务模块组织代码与页面,适合快速部署试用或作为二次开发蓝本。压缩包共451个文件,约7.05MB,其中包含C#工程源码(cs、csproj)、ASP.NET页面与处理程序(aspx、ashx)、前端样式与脚本(css、js)、图片资源(png、jpg、gif)以及编译后的exe、dll和配置文件(config),并有Excel数据样例与文档辅助理解,目录结构清晰,便于定位业务模块。已有285人学习下载。通过阅读源码和示例数据,可以了解基础HIS的表单交互、业务逻辑与接口调用方式,进而扩展预约、统计报表、患者追踪等模块;对想要快速搭建医院信息系统原型或学习ASP.NET Web开发的人来说,是一份实用性很强的起步资料。
1. 商业HIS系统“超级简单版”落地记:解压就能复现的门诊业务闭环
如果你印象中的HIS(医院信息系统)还停在“大厂级项目、几十张表、部署半年”的体量,这个资源会颠覆你的预期。所谓超级简单的商业HIS,就是一个保留了挂号、医生工作站、收费、药房库存与基础统计的核心闭环,没有引入微服务、消息队列这类重组件,解压后初始化数据库,就能在浏览器里走通一个患者从进院到取药的完整路径。我拆这套工程时最大的感受是:它没有为了简单而阉割业务,反而把退费冲正、日结报表这些最容易写翻车的逻辑都留了样例代码。适合中小规模门诊快速搭业务,也适合想进HIS二次开发的工程师拿来做底稿研究。
2. 技术栈与工程结构:拆开RAR之后先看清四层设计
拿到压缩包第一件事不是急着跑mvn spring-boot:run,而是先把工程里的技术栈和目录结构摸清楚。这套HIS不走微服务,后端是一个标准的 Spring Boot 单体应用,前端是独立的 Vue 工程,数据库脚本单独放在sql/目录里。前后端分离、代码分层清晰,是它敢叫“简单版”的底气。
2.1 后端技术选型与分层:Spring Boot + MyBatis + MySQL
后端是基于 Spring Boot 2.x 搭建的 Maven 工程,Java 版本 8,持久层用 MyBatis,数据库用 MySQL。没有引入 Redis 做缓存,也没用 RabbitMQ 这类中间件,登录态用 JWT 生成 token,请求拦截在过滤器中完成鉴权。这种组合的好处是部署成本低,一台 2G 内存的云主机就能跑,出现问题也好排查。
代码分层很规矩,controller -> service -> mapper三层。控制层只负责参数接收和结果封装,不写业务;业务层用@Transactional管理事务;数据层通过 MyBatis 的 XML 文件写 SQL。我一般会在拿到这类 HIS 源码后,先看pom.xml里依赖了哪些包。这个项目里值得注意的有spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、jjwt和lombok,基本就是一套没有多余依赖的清爽组合。
2.2 前端独立工程:Vue + Element UI 的构建产物与后端集成
前端在frontend/目录下,用的是 Vue 2 + Element UI。页面包括登录、挂号收银台、医生工作台、收费窗口、药房发药、报表中心这几个核心视图。日常开发的时候,前端通过 devServer 代理请求到后端;如果不想开两个服务,也可以直接npm run build,构建出的dist目录会被复制到后端的static目录下,由 Spring Boot 统一托管。
判断一个工程是不是这种做法,看后端resources/static下有没有 index.html 和js/css目录就行。有的话,生产环境打出 jar 包就能直接跑,不用再配 Nginx。前端路由用的 Vue Router 的 history 模式,但生产环境如果直接打包进后端,注意路由模式改成 hash,否则刷新页面会出现 404。这套系统的默认配置我印象中是用的 hash 模式,省了很多部署时的血泪麻烦。
2.3 目录结构与配置约定:拿到RAR先看这几个文件
解开 RAR 后,整个工程的主干结构大致长这样:
his-super-simple/ ├── sql/ │ ├── init_his.sql # 库表结构初始化 │ └── demo_data.sql # 演示数据与初始账号 ├── src/main/java/com/his/ │ ├── controller/ # 控制层,按模块分包 │ ├── service/ # 业务层 │ ├── mapper/ # MyBatis接口 │ └── entity/ # 数据实体 ├── src/main/resources/ │ ├── application.yml # 数据源与端口配置 │ └── mapper/ # MyBatis XML文件 ├── frontend/ │ ├── src/ │ │ ├── views/ # 页面级组件 │ │ ├── api/ # 请求封装 │ │ └── router/ # 路由表 │ └── dist/ # 已构建好的前端产物 └── README.mdapplication.yml是启动前必须改的文件。数据库连接、端口、JWT 密钥都在里面,我把常用配置整理成一张表:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| server.port | 8080 | 后端HTTP端口 |
| server.servlet.context-path | /his | 接口统一前缀 |
| spring.datasource.url | jdbc:mysql://localhost:3306/his_db | 数据库地址 |
| spring.datasource.username | root | 数据库账号 |
| mybatis.mapper-locations | classpath:mapper/*.xml | SQL映射位置 |
| jwt.secret | your-secret-key | token签名密钥,生产必须改 |
这里有个容易忽略的细节:context-path配了/his,前端所有请求就会带上/his前缀。如果自己二次开发时把前缀去掉,记得同步修改前端request.js里的 baseURL,不然接口全部 404。
3. 核心模块拆解:一个门诊患者从挂号到收费的完整链路
这一章是最值得细读的部分。HIS 的核心不是界面有多好看,而是数据在挂号、医生站、收费这三个节点之间的流转是否正确。我从这套源码里挑三条主线逐段拆给你看。
3.1 挂号模块:排队号生成与号源状态判断
挂号是整个门诊流程的入口,也是号源状态最敏感的地方。这套系统的挂号表设计得很直接:一张his_register表记录一条挂号记录,字段包括register_no、patient_id、dept_id、doctor_id、status。status用整型表示状态机,0是待就诊,1是已就诊,2是已退号。
挂号核心逻辑在RegisterService里,关键代码是这段:
@Service public class RegisterService { @Resource private RegisterMapper registerMapper; @Resource private SeqGenerator seqGenerator; @Transactional(rollbackFor = Exception.class) public RegisterInfo register(RegisterRequest req) { // 1. 判断该医生当天号源是否已满 int usedCount = registerMapper.countTodayByDoctor( req.getDoctorId(), DateUtil.today()); if (usedCount >= req.getMaxNumber()) { throw new BizException("该医生今日号源已约满"); } // 2. 生成当天排队号,例如 "20240520-001" String registerNo = seqGenerator.nextDailySeq( "REG", DateUtil.today()); RegisterInfo info = new RegisterInfo(); info.setRegisterNo(registerNo); info.setPatientId(req.getPatientId()); info.setDeptId(req.getDeptId()); info.setDoctorId(req.getDoctorId()); info.setStatus(0); registerMapper.insert(info); return info; } }这段代码有两个值得关注的参数:maxNumber是医生当日放号上限,在科室排班表里维护;nextDailySeq生成器则负责把REG前缀、日期和一个三位流水号拼成可读的挂号单号。流水号用数据库表加synchronized关键字控制并发,在生产环境如果挂号量很大,这一步需要换成 Redis 自增,但作为“简单版”这个实现够用。
为什么register方法要加@Transactional?因为号源判断和插入挂号记录必须是原子操作。如果先判断再插入、中间不做事务控制,两个患者同时挂最后一个号时就会超卖。这个坑在日活不高的系统里不容易暴露,但在做压力测试时一定会翻车。
3.2 医生工作站:处方如何拆成可收费的费用明细
患者挂号后进入医生工作站。医生选择患者、录入诊断、开出药品或检查项目,系统会把每条医嘱转成一条待收费明细。这里的核心表是his_prescription和his_prescription_item,一张处方对多条明细,明细里记录项目、数量、单价、金额和状态。
开立处方的核心代码在PrescriptionService:
@Transactional(rollbackFor = Exception.class) public String createPrescription(PrescriptionDTO dto) { String rxNo = seqGenerator.nextDailySeq("RX", DateUtil.today()); Prescription rx = new Prescription(); rx.setRxNo(rxNo); rx.setRegisterNo(dto.getRegisterNo()); rx.setDoctorId(dto.getDoctorId()); rx.setStatus(0); // 0-未收费 prescriptionMapper.insert(rx); List<FeeItem> items = new ArrayList<>(); for (PrescriptionItemDTO itemDto : dto.getItems()) { FeeItem item = new FeeItem(); item.setRxNo(rxNo); item.setItemCode(itemDto.getItemCode()); item.setItemName(itemDto.getItemName()); item.setPrice(itemDto.getPrice()); item.setQuantity(itemDto.getQuantity()); // 金额 = 单价 * 数量,保留两位小数 item.setAmount(itemDto.getPrice() .multiply(BigDecimal.valueOf(itemDto.getQuantity()))); item.setStatus(0); items.add(item); feeItemMapper.insert(item); } return rxNo; }代码里有一个在商业 H IS 开发里常见但新手容易写错的地方:金额计算必须用BigDecimal,绝不能用double。药品单价和数量做乘法,0.1 元乘以 3 在 double 下会得到0.30000000000000004,一旦累计到整单收费,一分钱对不上账就麻烦了。
这里status又出现了,含义变成费用的支付状态:0未收费,1已收费,2已退费。一套系统里多个业务表各自维护自己的status,这是 HIS 源码里很常见的约定。二次开发时看到status字段,第一反应应该是去查它对应的状态机注释,而不是猜。
3.3 收费与退费:金额汇总、发票号回填与冲正逻辑
收费窗口拿着处方号去收费,ChargeService负责汇总所有待收费明细并写入收费记录。这个模块的代码是整个工程里事务边界最值得学习的部分:
@Transactional(rollbackFor = Exception.class) public ChargeResult charge(String rxNo, String cashierId) { List<FeeItem> items = feeItemMapper.selectUnpaidByRxNo(rxNo); if (items.isEmpty()) { throw new BizException("没有待收费明细,请勿重复收费"); } // 汇总明细金额 BigDecimal total = items.stream() .map(FeeItem::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); // 生成收费流水与发票号 String chargeNo = seqGenerator.nextDailySeq("CH", DateUtil.today()); ChargeRecord record = new ChargeRecord(); record.setChargeNo(chargeNo); record.setRxNo(rxNo); record.setCashierId(cashierId); record.setTotalAmount(total); record.setInvoiceNo("FP" + chargeNo); // 简单版把流水号当发票号 chargeMapper.insert(record); // 把明细状态改为已收费 feeItemMapper.updateStatusByRxNo(rxNo, 0, 1); ChargeResult result = new ChargeResult(); result.setChargeNo(chargeNo); result.setInvoiceNo(record.getInvoiceNo()); result.setTotalAmount(total); return result; }selectUnpaidByRxNo有个隐藏的防重设计:它只选出status = 0的明细。如果收费请求被前端重复提交,第二次进来时items为空,直接抛异常,天然挡住了重复收费。这种用业务状态而不是用锁来控制并发的方式,在低并发场景下是足够可靠的。
退费逻辑就更有意思了。它不是直接删除收费记录,而是插入一条金额为负数的冲正流水。这样设计的好处是财务对账时每一笔流水都有迹可循,正数和负数自然抵消,不会因为物理删除造成账实不符。你在源码里搜索refund关键字,会看到它调用了chargeMapper.insert,但金额取负数,同时把对应明细状态改回0。
4. 本地跑通全流程:从初始化数据库到完成首笔挂号收费
静态看代码是一种理解方式,但真正让这套 HIS “活”起来,必须把服务跑起来走一遍业务。下面是我实际操作时的完整路径,照着做基本一次能通。
4.1 环境准备与数据库初始化
本地需要 JDK 8、Maven 3.6+、MySQL 5.7 或 8.0、Node.js 12+。MySQL 准备好之后,用命令行导入两个脚本:
mysql -uroot -p < sql/init_his.sql mysql -uroot -p < sql/demo_data.sqlinit_his.sql建库建表,demo_data.sql插入演示数据。演示数据里包含了四个科室、三名医生、两个挂号窗口账号、一批药品字典和患者档案。这里提醒一句:MySQL 8 默认的认证插件是caching_sha2_password,如果连不上库,先检查驱动版本是不是 8.x,项目里如果用老版本mysql-connector-java会报认证协议错误。我一般会把pom.xml里的驱动升到 8.0.20 以上,同时把application.yml时区参数配上,避免后面启动时报时区错误。
4.2 启动后端并验证接口
数据库就绪后,改application.yml里的数据库密码,然后启动:
mvn spring-boot:run启动日志里看到Tomcat started on port 8080就算成功。先用 curl 验证登录接口,确认 JWT 能正常签发:
curl -X POST http://localhost:8080/his/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'返回值里拿到token字段,后续请求在 Header 里带Authorization: Bearer <token>。如果返回 401,多半是数据库里的账号初始密码不是 123456,或者demo_data.sql没有导入成功,去his_user表里查一下账号是否存在。
4.3 启动前端并用演示账号走通业务流程
后端在跑,前端我用开发模式启动,这样改页面代码能实时生效:
cd frontend npm install npm run serve前端默认跑在 8081 端口,开发环境的代理配置会把/his开头的请求转发到 8080 后端。访问http://localhost:8081,用内置演示账号登录:
| 角色 | 账号 | 主要权限 |
|---|---|---|
| 系统管理员 | admin | 基础数据、报表、账号管理 |
| 挂号收费员 | reg | 挂号、退号、收费 |
| 门诊医生 | doctor | 接诊、开处方 |
| 药房操作员 | pharmacy | 发药、库存查询 |
完整走一遍业务的路径是:用reg账号登录,在挂号页选择“内科-张医生”,为演示患者李某某挂一个号,拿到挂号单号;然后退出登录,用doctor账号登录,在医生工作台找到这个挂号记录,填写诊断并开两张药品处方,保存;再次退出,用reg登录进入收费窗口,输入处方号,点击收费,回显发票号;最后用pharmacy登录药房发药界面,确认发药。这五步走通,说明系统的表关系和状态流转没有配置问题。
5. 避坑手册:这套商业HIS最常见的问题与排查记录
我拆这套工程时在本地跑了三轮样例,过程中踩了几个典型的坑,按场景分两组记录下来。这些都是真实会遇到的启动与业务问题,提前知道能省不少排查时间。
5.1 数据库初始化与启动阶段的高发问题
坑一:MySQL 8 时区导致启动直接报错
现象:后端启动时抛The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,服务起不来。
原因:MySQL 8 驱动对时区参数更敏感,application.yml里没有指定serverTimezone。
解决:在数据库连接串上追加参数:?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。顺手把连接串里的useSSL=false也加上,避免 SSL 告警。
坑二:导入 SQL 脚本时提示DEFINER权限不足
现象:用 Navicat 或命令行导入init_his.sql时,报ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER privilege(s) for this operation。
原因:脚本里可能带有创建视图或存储过程时的DEFINER='root'@'localhost'信息,当前导入账号不是管理员,无权指定 DEFINER。
解决:用 root 导入,或者把脚本里的DEFINER段落去掉。我一般直接用命令行导入,避开图形工具对脚本执行权限的差异。
坑三:8080 端口被占用,启动日志却只报“端口被占用”后退出
现象:mvn spring-boot:run跑起来几秒钟又停了,日志只给一句Port 8080 was already in use。
原因:本地有其他 Java 服务或 Docker 容器占用了 8080。
解决:快速找出占用进程并清理:lsof -i:8080(Mac/Linux)或netstat -ano | findstr 8080(Windows)。也可以直接用配置逃生:把application.yml的server.port改成 8082,前端vue.config.js里代理目标同步改成 8082,省到连杀进程都免了。
5.2 业务逻辑与并发场景下容易翻车的地方
坑四:退费请求被双击提交,造成两条负数流水
现象:收费员点击退费按钮两次,费用明细状态还没刷新,第二次又成功插入一条冲正记录,最终账目多冲了一笔。
原因:退费接口没有做幂等校验,前端重复提交后,后端把“插入负数流水”当成独立业务执行了两次。
解决:在退费接口入口先查询原始收费记录的status,如果已经是“已冲正”,直接抛BizException("该收费单已退费")。这也是这套系统charge_record表里设计了status字段的缘故。
坑五:挂号高峰时段排队号出现重复
现象:并发挂号时日志里出现两条相同的register_no,患者取号时窗口“撞号”。
原因:nextDailySeq生成器的实现是先查当天最大值再加一,但多个请求并发执行查询时读到同一个最大值。
解决:常见做法是在生成器的mapper方法上加synchronized,或者用数据库行锁SELECT ... FOR UPDATE锁住日期流水行。如果项目后续要加大并发,我一般会把这个生成器替换成 Redis incr。
坑六:前端页面能打开,但列表数据全部 404
现象:登录页显示正常,登录后每张列表都请求失败,Network 面板看到请求 URL 是http://localhost:8081/api/...,而后端配置了/his前缀。
原因:前端request.js里的 baseURL 没有和context-path统一。开发环境代理只转发了/his开头的地址,/api开头的直接请求到了前端 devServer 自己,返回 404。
解决:把request.js的 baseURL 改为/his/api,或者修改代理规则将'/api'也转发到 8080。核心原则是前端实际请求路径 =context-path + controller 路由,两边必须保持一致。
6. 二次开发手记:不破坏原有数据结构地扩展业务模块
拿到这套 HIS 源码,最值钱的不是跑通,而是基于它扩展出自己的业务模块。下面是我会采取的两种落地路径,分别对应“加字段”和“加功能”。
6.1 新增业务字段的标准操作路径
假设要给挂号表增加一个“医保类型”字段。最稳的做法是沿着数据层往上层逐级加,而不是直接在 controller 里写死:
第一步,在his_register表增加insurance_type字段;第二步,在RegisterInfo实体类增加对应属性;第三步,在RegisterMapper.xml的insert语句里增加字段映射;第四步,在挂号请求入参 DTO 里接收前端传值;第五步,前端挂号页的弹窗表单增加一个下拉框。每一步只动一个文件,出问题用git diff一眼能定位。我一般会特意先改数据库、再改后端、最后改前端,避免出现“前端已经在传新字段,后端还没接住”的中间态。
6.2 新增一个日结算统计报表的实现示例
原项目带了基础报表,如果想按自己的需求加一个“按医生统计日收费金额”的报表,可以直接复用现有的fee_item和charge_record两张表,不用改数据结构。Mapper XML 里加一段 SQL:
<select id="sumDailyByDoctor" resultType="java.util.Map"> SELECT d.doctor_name AS doctorName, COUNT(DISTINCT cr.charge_no) AS chargeCount, SUM(fi.amount) AS totalAmount FROM charge_record cr INNER JOIN fee_item fi ON cr.rx_no = fi.rx_no INNER JOIN prescription rx ON cr.rx_no = rx.rx_no INNER JOIN doctor d ON rx.doctor_id = d.doctor_id WHERE cr.create_time >= #{startTime} AND cr.create_time < DATE_ADD(#{endTime}, INTERVAL 1 DAY) AND fi.status = 1 GROUP BY d.doctor_name, rx.doctor_id </select>三个参数的含义分别是startTime统计开始日期、endTime统计结束日期,为了把 “某天” 扩成 “某天 00:00:00 到次日 00:00:00”,我在 SQL 里对endTime加了一天。fi.status = 1这条条件必须保留,否则会把已退费的明细也算进收入。
改造完成后,验证方式不是打开页面看一眼就收工,而是先在数据库手工构造一张测试处方:一条收费明细、一条退费明细,然后调用报表接口核对统计出来的金额是否一致。这套系统里最容易被忽视的验证死角是日期边界——跨天挂号、跨天收费时,统计口径很容易差一笔。从那以后,我每次给 HIS 加报表,第一件事必定是拿真实业务数据跑一遍日结对账,而不是拿演示数据糊弄自己。
希望这份拆解能帮你更快地把这套系统跑起来,也让你在后面积累 HIS 业务代码时少走一点弯路。
本文还有配套的精品资源,点击获取