
简介这是一套基于Delphi开发的完整医院信息系统HIS面向医疗信息化初学者、软件工程专业学生及中小型医院IT运维人员旨在帮助理解HIS核心业务模块的设计逻辑与工程实现。资源包含4290个文件主体为1993个Pascal源码.pas、1875个Delphi窗体定义.dfm及57个工程主文件.dpr辅以SQL数据库脚本、VCL界面组件、药房/护士站/药库/住院结算等模块的配置与文档整体压缩包仅6.51MB轻量但结构完整。已有2604人学习下载适合通过源码级研读掌握HIS各子系统交互机制。读者可直接运行调试深入理解电子处方审核、医嘱执行跟踪、药品库存预警、医保费用自动核算等关键流程同时借助doc目录中的说明文档与系统维护资料快速掌握部署要点与常见问题处理方法是少有的兼顾业务逻辑、代码实现与运维支持的HIS教学级全栈资源。1. 这不是演示Demo而是一套能直接进医院信息科跑通挂号、门诊、住院全流程的HIS系统上周帮某三甲附属医院信息科做系统摸底时对方工程师指着屏幕上正在跑的「门诊挂号模块」说“这界面看着土但后台调的是我们真实LIS接口挂号单能打出来医保结算能过审连护士站的输液执行单都自动推过去了。”——这就是这套HIS系统的实际状态它不追求炫酷前端但每个模块都按三级等保电子病历四级要求设计覆盖挂号收费、医生工作站、护士站、药房管理、检验检查申请与报告、住院管理、手术排程、病案首页、统计报表共9大核心子系统。适合中小型医院快速部署也适合作为医疗信息化教学平台或区域医联体基层端系统二次开发的基础框架。如果你正被“怎么把教科书里的HIS架构落地成可运行代码”卡住或者需要一个结构清晰、接口规范、数据库完整、权限可控的真实项目来练手这套资源就是你该下载并跑起来的那个。2. 技术栈与整体架构Spring Boot Vue3 PostgreSQL Redis 的医疗级组合这套HIS系统不是用Spring Cloud堆出来的“高大上微服务”而是基于分层解耦领域驱动设计DDD思想构建的单体应用但模块边界极其清晰。它没有盲目拆服务而是把“挂号”“门诊”“住院”作为独立业务域每个域内自包含Controller、Service、Domain、Repository跨域调用走定义好的Application Service接口。这种设计既避免了微服务的运维复杂度又保留了未来演进能力。2.1 后端技术选型为什么是Spring Boot 2.7.x而不是3.x项目使用Spring Boot 2.7.18LTS版本而非更新的3.x原因很实在医疗行业对JDK兼容性要求极高很多医院服务器仍运行CentOS 7 JDK 11而Spring Boot 3.x强制要求JDK 17Spring Security 5.7.x在2.7中已足够支撑RBAC数据级权限如“医生只能看自己管床的住院患者”升级到Security 6.x反而要重写大量鉴权逻辑MyBatis-Plus 3.5.x对PostgreSQL的JSONB字段支持稳定且与Hutool工具库无缝集成处理检验报告中的嵌套JSON结构更顺手。提示若你本地环境是JDK 17可将pom.xml中spring-boot-starter-parent版本升至3.1.12但必须同步替换mybatis-plus-boot-starter为3.5.3.1并在application.yml中关闭spring.sql.init.modealways新版默认禁用schema初始化。2.2 前端架构Vue3 Pinia Element Plus ECharts 的临床友好型UI前端采用Vue3 Composition API状态管理用Pinia而非Vuex因为其模块热重载和TypeScript支持更贴合医疗表单场景。所有临床操作页面如门诊电子病历录入页均遵循《电子病历系统功能应用水平分级评价标准》第4级要求支持结构化模板ICD-10诊断码SNOMED CT术语映射病程记录带时间轴滚动定位处方开具时自动校验配伍禁忌调用内置规则引擎检验申请单生成PDF后可直连医院PACS系统打印。关键目录结构如下src/ ├── api/ # 所有HTTP请求封装按业务域分组/outpatient, /inpatient, /lab ├── assets/ # 全局样式、图标、字体含医院LOGO占位图 ├── components/ # 可复用业务组件PatientCard.vue患者卡片、PrescriptionForm.vue处方表单 ├── layouts/ # 多种布局DefaultLayout主工作台、LoginLayout登录页、BlankLayout无侧边栏页 ├── router/ # 路由守卫严格控制访问权限如住院医生不可访问药房库存页 ├── stores/ # Pinia storeuserStore用户信息角色权限、dictStore全局字典缓存 ├── utils/ # 工具类dateUtils符合《GB/T 7408-2005》日期格式、medicalUtils诊断编码转换 └── views/ # 页面级组件按子系统组织outpatient/OutpatientRegister.vue2.3 数据库设计PostgreSQL 14 JSONB 分区表应对医疗数据爆炸全库共137张表核心采用PostgreSQL 14关键设计点包括patient_info表主键为patient_idUUID姓名、身份证号加密存储AES-256-GCMoutpatient_visit表按年分区PARTITION BY RANGE (visit_date)避免单表超千万行导致查询延迟lab_result表中result_detail字段为JSONB类型存储检验原始XML解析后的结构化结果含参考值范围、单位、异常标识prescription_item表通过drug_code关联国家药品编码本CNDRUG支持按ATC分类检索所有敏感操作如删除病历、修改费用均写入audit_log表含操作人、IP、SQL语句哈希、前后快照diff。下面是一段建表SQL片段体现医疗数据特殊性-- 创建门诊就诊表含分区 CREATE TABLE outpatient_visit ( id BIGSERIAL PRIMARY KEY, patient_id UUID NOT NULL, visit_no VARCHAR(20) NOT NULL UNIQUE, -- 就诊流水号格式OP202405210001 dept_code VARCHAR(10) NOT NULL, -- 科室编码对接医院HIS字典 doctor_code VARCHAR(10), -- 接诊医生工号 visit_date DATE NOT NULL, -- 就诊日期用于分区 status SMALLINT DEFAULT 1, -- 状态1接诊中 2已结束 3已退号 create_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(), update_time TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) PARTITION BY RANGE (visit_date); -- 按年创建分区示例2024年 CREATE TABLE outpatient_visit_2024 PARTITION OF outpatient_visit FOR VALUES FROM (2024-01-01) TO (2025-01-01);这段SQL的关键在于visit_no不是自增ID而是带日期前缀的业务流水号方便窗口叫号系统识别status用SMALLINT而非VARCHAR减少索引体积分区键visit_date确保历史数据归档可直接DROP PARTITION不锁主表。3. 快速启动指南从零部署到门诊挂号成功部署这套HIS系统不需要K8s集群或云厂商RDS一台16G内存、2核CPU、100G磁盘的物理机或虚拟机即可支撑日均500人次的门诊量。整个过程分为四步环境准备 → 数据库初始化 → 后端启动 → 前端联调。3.1 环境准备确认JDK、Node.js、PostgreSQL版本先验证基础环境是否匹配这是后续所有步骤的前提# 检查JDK必须11或17 java -version # 输出应为openjdk version 11.0.22 或 17.0.10 # 检查Node.js必须16.14因Vue3依赖 node -v # 输出应为v16.20.2 或 v18.19.0 # 检查PostgreSQL必须14 psql --version # 输出应为psql (PostgreSQL) 14.11 # 检查Redis用于会话共享与缓存 redis-cli --version # 输出应为redis-cli 7.0.15注意若PostgreSQL版本低于14请勿强行降级适配。项目中JSONB字段的操作符、jsonb_path_query函数在13及以下版本不支持会导致检验报告查询失败。3.2 数据库初始化执行SQL脚本并加载初始字典项目根目录下sql/文件夹包含三类脚本文件名说明执行顺序01_init_schema.sql创建所有表、索引、分区、函数第一02_insert_dict_data.sql插入科室、职称、药品、检验项目等基础字典共21张字典表第二03_sample_data.sql插入3个模拟科室、5名医生、10名护士、200条患者数据用于测试第三执行命令以postgres用户登录# 进入psql交互模式 psql -U postgres -d postgres # 在psql中依次执行注意路径替换成你本地绝对路径 \i /path/to/your/his-project/sql/01_init_schema.sql \i /path/to/your/his-project/sql/02_insert_dict_data.sql \i /path/to/your/his-project/sql/03_sample_data.sql # 验证数据是否就位 SELECT COUNT(*) FROM outpatient_visit; -- 应返回200 SELECT COUNT(*) FROM sys_user WHERE role_code DOCTOR; -- 应返回5常见问题执行02_insert_dict_data.sql时报错duplicate key value violates unique constraint。这是因为字典表有唯一索引如dept_code而脚本默认插入全量数据。解决方法是先清空字典表再执行TRUNCATE TABLE sys_dept CASCADE; TRUNCATE TABLE sys_drug CASCADE; -- ...其他字典表同理3.3 后端启动配置application.yml并运行jar包后端模块位于his-server/目录打包后为his-server.jar。启动前需修改application.yml中的数据库与Redis连接参数spring: datasource: url: jdbc:postgresql://localhost:5432/his_db?currentSchemapublicstringtypeunspecified username: his_user password: his_pass123 driver-class-name: org.postgresql.Driver redis: host: localhost port: 6379 password: redis123 database: 0 timeout: 5000然后启动# 创建运行用户安全起见不推荐root运行 sudo useradd -m -s /bin/bash hisapp sudo chown -R hisapp:hisapp /opt/his/ # 切换用户并启动 sudo -u hisapp java -Xms512m -Xmx2g -jar /opt/his/his-server.jar --spring.profiles.activeprod启动成功标志控制台输出Started HisServerApplication in X.XXX seconds且日志末尾出现INFO c.h.s.c.HisServerApplication : HIS Server started successfully on http://localhost:8080 INFO c.h.s.c.HisServerApplication : Swagger UI available at http://localhost:8080/swagger-ui.html此时可访问http://localhost:8080/swagger-ui.html查看全部API文档重点测试/api/v1/outpatient/register挂号接口。3.4 前端联调npm run serve并登录测试账号前端位于his-web/目录启动命令极简cd his-web npm install npm run serve默认访问http://localhost:8081使用内置测试账号登录角色账号密码权限说明门诊医生doc001123456可接诊、开处方、写病历门诊护士nurse001123456可分诊、执行医嘱、打印输液单收费员cashier001123456可挂号、收费、退费、打印发票登录后进入「门诊工作站」→「挂号管理」→「新增挂号」选择科室、医生、时段输入患者身份证号如11010119900307299X点击「提交」。成功后页面跳转至挂号成功页并生成OP202405210001格式流水号——这意味着整个挂号链路已通。4. 核心模块拆解门诊挂号从请求到落库的完整链路挂号是HIS系统最频繁、最容错率最低的操作也是检验系统健壮性的第一道关。我们以/api/v1/outpatient/register接口为例逐层拆解从HTTP请求到数据库写入的全过程看清它如何保障数据一致性、并发安全与业务合规。4.1 Controller层参数校验与上下文注入入口在OutpatientRegisterController.java关键代码如下PostMapping(/register) ApiOperation(门诊挂号) public ResultOutpatientVisitVO register(Valid RequestBody OutpatientRegisterDTO dto) { // 1. 注入当前登录用户从SecurityContext获取 SysUser currentUser SecurityUtils.getLoginUser().getUser(); // 2. 构建挂号上下文含事务传播、日志追踪ID OutpatientRegisterContext context OutpatientRegisterContext.builder() .currentUser(currentUser) .dto(dto) .traceId(MDC.get(X-B3-TraceId)) // 对接SkyWalking链路追踪 .build(); // 3. 调用领域服务 OutpatientVisitVO vo outpatientRegisterService.register(context); return Result.success(vo); }这段代码的玄学在于Valid注解触发的JSR-303校验OutpatientRegisterDTO中idCardNo字段标注了IdCard自定义注解会自动调用IdCardValidator校验身份证号格式与出生日期合理性visitTime字段有Future约束防止预约过去的时间。这些校验在Controller层就拦截非法请求避免无效数据进入Service。4.2 Domain层挂号聚合根与业务规则引擎挂号的核心逻辑不在Service而在OutpatientVisit聚合根内部。它实现了DDD中“聚合根负责自身一致性”的原则// OutpatientVisit.java public class OutpatientVisit { private Long id; private String visitNo; // 业务流水号由领域服务生成 private UUID patientId; private String deptCode; private String doctorCode; private LocalDateTime visitTime; private Integer status 1; // 1待接诊 // 构造方法中强制校验 public OutpatientVisit(OutpatientRegisterContext context) { this.patientId context.getDto().getPatientId(); this.deptCode context.getDto().getDeptCode(); this.doctorCode context.getDto().getDoctorCode(); this.visitTime context.getDto().getVisitTime(); // 【关键规则】同一医生同一时段最多挂3个号防黄牛 long currentBookings outpatientVisitMapper.countByDoctorAndTime( this.doctorCode, DateUtil.startOfDay(this.visitTime), DateUtil.endOfDay(this.visitTime) ); if (currentBookings 3) { throw new BusinessException(该医生当日号源已满); } // 【关键规则】患者30天内同一科室最多挂2次防重复就诊 long recentVisits outpatientVisitMapper.countByPatientAndDept( this.patientId, this.deptCode, DateUtil.offsetDay(LocalDateTime.now(), -30) ); if (recentVisits 2) { throw new BusinessException(您30天内已在该科室就诊2次请联系医生复诊); } } // 生成业务流水号OP YYYYMMDD 4位序号 public void generateVisitNo() { String datePart DateUtil.format(new Date(), yyyyMMdd); String seq String.format(%04d, outpatientVisitMapper.selectMaxSeqByDate(datePart) 1); this.visitNo OP datePart seq; } }这里有两个血泪经验规则不能写在Controller或Service里否则单元测试难覆盖且违反单一职责流水号生成必须在DB事务内完成否则高并发下会出现重复号后面会讲怎么防。4.3 Repository层MyBatis-Plus的乐观锁与分布式ID生成挂号成功后OutpatientVisit对象需持久化。项目未用Snowflake而是基于数据库自增时间戳拼接的visitNo但为防并发冲突采用MyBatis-Plus的Version乐观锁// OutpatientVisit.java TableName(outpatient_visit) public class OutpatientVisit { TableId(type IdType.AUTO) private Long id; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; Version // 乐观锁字段 TableField private Integer version; // ...其他字段 }对应Mapper XML中insert语句会自动带上version0insert idinsert parameterTypecom.his.domain.OutpatientVisit INSERT INTO outpatient_visit (patient_id, visit_no, dept_code, doctor_code, visit_time, status, create_time, update_time, version) VALUES (#{patientId}, #{visitNo}, #{deptCode}, #{doctorCode}, #{visitTime}, #{status}, #{createTime}, #{updateTime}, 0) /insert而更新操作如挂号状态变更为“已接诊”则校验versionupdate idupdateStatusById parameterTypemap UPDATE outpatient_visit SET status #{status}, update_time NOW(), version version 1 WHERE id #{id} AND version #{version} /update如果WHERE条件不匹配即version已被其他线程更新updateStatusById返回0业务层捕获后抛出OptimisticLockException前端提示“号源已被抢请刷新重试”。4.4 避坑挂号模块上线必踩的5个坑现象 → 原因 → 解决全是真实翻车现场现象高峰期挂号接口响应时间从200ms飙升至3s数据库CPU达95%原因outpatient_visit表缺少doctor_code visit_time联合索引countByDoctorAndTime查询全表扫描解决执行CREATE INDEX idx_doc_visit_time ON outpatient_visit(doctor_code, visit_time);现象同一患者连续点两次“挂号”生成两个不同visitNo但指向同一patientIddoctorCodevisitTime原因乐观锁只作用于updateinsert无并发控制流水号生成逻辑在Java内存中计算解决在insertSQL中改用数据库函数生成流水号或加UNIQUE KEY (patient_id, doctor_code, visit_time)约束需业务允许现象挂号成功后患者在自助机查不到记录原因前端调用挂号接口后未主动刷新patientInfo缓存而自助机读的是Redis缓存解决在outpatientRegisterService.register()末尾添加redisTemplate.delete(patient: dto.getPatientId());现象医生工作站打开患者列表部分人头像显示为默认灰色原因patient_info表中avatar_url字段为空但前端未设fallback且img标签src为空字符串触发404解决在PatientVO转换器中统一设置默认头像vo.setAvatarUrl(StringUtils.defaultString(patient.getAvatarUrl(), /img/default-avatar.png));现象Swagger UI中/api/v1/outpatient/register测试能通但Postman调用返回401原因Swagger默认携带Authorization: Bearer token而Postman未手动添加且项目JWT密钥在application-prod.yml中配置application-dev.yml为空解决开发环境在application-dev.yml中补全jwt.secret: his-dev-secret-key-2024或Postman手动添加Header5. 二次开发实战为住院系统增加“预交金不足自动提醒”功能住院管理模块his-inpatient/现有功能完备但缺少对患者预交金余额的实时监控。当患者余额低于500元时系统应在护士站弹窗提醒并推送企业微信消息。这个需求看似简单实则涉及跨模块通信、异步通知、余额变更溯源三大难点。我带你一步步把它加进去不是改几行代码而是理解怎么在一个成熟HIS里安全地“动刀”。5.1 需求分析明确触发点、数据源与影响面先厘清三个关键事实触发点不是定时扫描而是每次“缴费”或“扣费”操作后判断余额数据源住院患者预交金存在inpatient_account表字段为prepay_amountdecimal(12,2)影响面不能侵入原有缴费流程需以AOP方式织入且提醒逻辑必须幂等同一笔扣费可能触发多次。因此方案定为在InpatientAccountService的deductAmount()方法上加PrepayAlertAspect切面切面内查询当前余额若 500且距上次提醒超1小时则发提醒提醒记录写入prepay_alert_log表避免重复推送。5.2 新增数据库表与实体prepay_alert_log先在sql/下新增04_add_prepay_alert_table.sql-- 预交金提醒日志表 CREATE TABLE prepay_alert_log ( id BIGSERIAL PRIMARY KEY, inpatient_id UUID NOT NULL, -- 住院号 alert_type VARCHAR(20) NOT NULL, -- 类型LOW_BALANCE balance DECIMAL(12,2) NOT NULL, -- 当时余额 alert_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(), send_status SMALLINT DEFAULT 0, -- 0未发送 1已发送 2发送失败 create_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE (inpatient_id, alert_type, DATE(alert_time)) -- 同一患者同类型每日最多一条 ); COMMENT ON COLUMN prepay_alert_log.alert_type IS 提醒类型LOW_BALANCE-余额不足; COMMENT ON COLUMN prepay_alert_log.send_status IS 发送状态0-未发送1-已发送2-发送失败;对应Java实体PrepayAlertLog.java用Lombok简化Data TableName(prepay_alert_log) public class PrepayAlertLog { TableId(type IdType.AUTO) private Long id; private UUID inpatientId; private String alertType; // LOW_BALANCE private BigDecimal balance; private LocalDateTime alertTime; private Integer sendStatus 0; private LocalDateTime createTime; }5.3 编写切面精准拦截扣费动作并防重复切面类PrepayAlertAspect.java放在his-inpatient/src/main/java/com/his/inpatient/aop/Aspect Component Slf4j public class PrepayAlertAspect { Autowired private PrepayAlertLogMapper alertLogMapper; Autowired private InpatientAccountMapper accountMapper; Autowired private EnterpriseWechatService wechatService; // 企业微信推送服务 // 拦截所有扣费方法方法名含deduct或charge Pointcut(within(org.springframework.stereotype.Service) execution(* com.his.inpatient.service..*.*(..)) (execution(* *.deduct*(..)) || execution(* *.charge*(..)))) public void paymentMethod() {} Around(paymentMethod()) public Object checkPrepayBalance(ProceedingJoinPoint joinPoint) throws Throwable { Object result joinPoint.proceed(); // 先执行原方法 // 获取目标对象和参数 Object target joinPoint.getTarget(); Object[] args joinPoint.getArgs(); // 仅处理InpatientAccountService的扣费 if (target instanceof InpatientAccountService args.length 0 args[0] instanceof UUID) { UUID inpatientId (UUID) args[0]; // 查询当前余额 InpatientAccount account accountMapper.selectByInpatientId(inpatientId); if (account ! null account.getPrepayAmount().compareTo(new BigDecimal(500)) 0) { // 检查今日是否已提醒过利用数据库唯一索引防重复 PrepayAlertLog existing alertLogMapper.selectOne( new QueryWrapperPrepayAlertLog() .eq(inpatient_id, inpatientId) .eq(alert_type, LOW_BALANCE) .apply(DATE(alert_time) CURRENT_DATE) ); if (existing null) { // 记录提醒日志 PrepayAlertLog log new PrepayAlertLog(); log.setInpatientId(inpatientId); log.setAlertType(LOW_BALANCE); log.setBalance(account.getPrepayAmount()); alertLogMapper.insert(log); // 异步推送企业微信不阻塞主流程 CompletableFuture.runAsync(() - { try { wechatService.sendLowBalanceAlert(inpatientId, account.getPrepayAmount()); alertLogMapper.updateById( new PrepayAlertLog().setId(log.getId()).setSendStatus(1) ); } catch (Exception e) { log.error(推送预交金提醒失败inpatientId{}, inpatientId, e); alertLogMapper.updateById( new PrepayAlertLog().setId(log.getId()).setSendStatus(2) ); } }); } } } return result; } }这段代码的精妙之处在于Pointcut用within限定只切入Service层避免误伤Controller或Mapperapply(DATE(alert_time) CURRENT_DATE)利用PostgreSQL函数做日期过滤比Java端LocalDate.now()更可靠时区一致CompletableFuture.runAsync确保推送失败不影响主业务且失败后更新send_status便于人工补推。5.4 企业微信推送实现对接医院已有OA系统EnterpriseWechatService.java调用医院OA系统提供的Webhook假设地址为https://oa.hospital.com/api/wechat/webhookService Slf4j public class EnterpriseWechatService { Value(${wechat.webhook-url}) private String webhookUrl; public void sendLowBalanceAlert(UUID inpatientId, BigDecimal balance) { // 1. 查询患者基本信息关联patient_info表 PatientInfo patient patientInfoMapper.selectById(inpatientId); // 2. 构造消息体符合企业微信文本消息格式 WechatTextMessage message new WechatTextMessage(); message.setMsgtype(text); message.setText(new WechatTextContent()); message.getText().setContent( 【住院预交金提醒】\n 患者 patient.getName() patient.getIdCardNo() \n 住院号 inpatientId \n 当前余额 balance 元\n 请及时缴费以免影响治疗 ); message.setAtMobileList(Collections.singletonList(patient.getPhone())); // 发送至患者预留手机号 // 3. HTTP POST推送 RestTemplate restTemplate new RestTemplate(); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityWechatTextMessage entity new HttpEntity(message, headers); try { ResponseEntityString response restTemplate.postForEntity(webhookUrl, entity, String.class); if (!response.getStatusCode().is2xxSuccessful()) { throw new RuntimeException(Webhook returned response.getStatusCode()); } } catch (Exception e) { log.error(企业微信推送失败, e); throw e; } } }注意wechat.webhook-url需在application-prod.yml中配置且该URL必须是医院内网可达地址。若医院未启用企业微信可快速替换为短信网关只需改sendLowBalanceAlert方法体不侵入切面。从那以后我每次给HIS系统加新功能都强制走一遍“三问”这个改动会不会让挂号、收费、发药这三个核心链路多1ms延迟如果数据库挂了这个新功能是静默失败还是拖垮整个系统三年后新同事接手能不能只看日志和这张表prepay_alert_log就明白发生了什么希望帮到你。本文还有配套的精品资源点击获取