简介:这是一套基于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.18(LTS版本),而非更新的3.x,原因很实在:
- 医疗行业对JDK兼容性要求极高,很多医院服务器仍运行CentOS 7 + JDK 11,而Spring Boot 3.x强制要求JDK 17+;
- Spring 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.mode=always(新版默认禁用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 store:userStore(用户信息+角色权限)、dictStore(全局字典缓存) ├── utils/ # 工具类:dateUtils(符合《GB/T 7408-2005》日期格式)、medicalUtils(诊断编码转换) └── views/ # 页面级组件,按子系统组织(outpatient/OutpatientRegister.vue)2.3 数据库设计:PostgreSQL 14 + JSONB + 分区表应对医疗数据爆炸
全库共137张表,核心采用PostgreSQL 14,关键设计点包括:
patient_info表主键为patient_id(UUID),姓名、身份证号加密存储(AES-256-GCM);outpatient_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?currentSchema=public&stringtype=unspecified 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.active=prod启动成功标志:控制台输出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,使用内置测试账号登录:
| 角色 | 账号 | 密码 | 权限说明 |
|---|---|---|---|
| 门诊医生 | doc001 | 123456 | 可接诊、开处方、写病历 |
| 门诊护士 | nurse001 | 123456 | 可分诊、执行医嘱、打印输液单 |
| 收费员 | cashier001 | 123456 | 可挂号、收费、退费、打印发票 |
登录后进入「门诊工作站」→「挂号管理」→「新增挂号」,选择科室、医生、时段,输入患者身份证号(如11010119900307299X),点击「提交」。成功后页面跳转至挂号成功页,并生成OP202405210001格式流水号——这意味着整个挂号链路已通。
4. 核心模块拆解:门诊挂号从请求到落库的完整链路
挂号是HIS系统最频繁、最容错率最低的操作,也是检验系统健壮性的第一道关。我们以/api/v1/outpatient/register接口为例,逐层拆解从HTTP请求到数据库写入的全过程,看清它如何保障数据一致性、并发安全与业务合规。
4.1 Controller层:参数校验与上下文注入
入口在OutpatientRegisterController.java,关键代码如下:
@PostMapping("/register") @ApiOperation("门诊挂号") public Result<OutpatientVisitVO> 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语句会自动带上version=0:
<insert id="insert" parameterType="com.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>而更新操作(如挂号状态变更为“已接诊”)则校验version:
<update id="updateStatusById" parameterType="map"> 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但指向同一patientId+doctorCode+visitTime
原因:乐观锁只作用于update,insert无并发控制;流水号生成逻辑在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手动添加Header
5. 二次开发实战:为住院系统增加“预交金不足自动提醒”功能
住院管理模块(his-inpatient/)现有功能完备,但缺少对患者预交金余额的实时监控。当患者余额低于500元时,系统应在护士站弹窗提醒,并推送企业微信消息。这个需求看似简单,实则涉及跨模块通信、异步通知、余额变更溯源三大难点。我带你一步步把它加进去,不是改几行代码,而是理解怎么在一个成熟HIS里安全地“动刀”。
5.1 需求分析:明确触发点、数据源与影响面
先厘清三个关键事实:
- 触发点:不是定时扫描,而是每次“缴费”或“扣费”操作后判断余额;
- 数据源:住院患者预交金存在
inpatient_account表,字段为prepay_amount(decimal(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 QueryWrapper<PrepayAlertLog>() .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或Mapper;apply("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/webhook):
@Service @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); HttpEntity<WechatTextMessage> entity = new HttpEntity<>(message, headers); try { ResponseEntity<String> 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)就明白发生了什么?
希望帮到你。
本文还有配套的精品资源,点击获取