
简介基于Java语言的门诊服务聚合系统源码面向医疗信息化开发人员与Java后端学习者定位于门诊服务的一体化管理核心场景覆盖用户管理、预约挂号、排队叫号与医疗记录。项目采用Spring Boot、Spring MVC、MyBatis等技术按模块化、服务化思想组织前后端分离分层结构清晰便于理解企业级医疗系统的构建方式。源码包共51个文件其中34个Java源文件承载业务逻辑8个XML配置与YAML、properties等文件负责Spring框架参数与安全控制jar包与Maven工程文件辅助依赖管理和构建压缩包约220KB。已有246人学习下载。从中可重点学习Spring Security认证授权、事务管理、配置分离等关键设计也能借鉴接口定义与模块解耦方法适合作为同类门诊信息系统的开发参考。1. 门诊系统最缺的不是业务代码而是一层聚合做过医院信息系统HIS相关开发的工程师应该都有同感门诊业务从来不是单系统能搞定的。挂号在一号系统候诊队列在分诊台缴费在收费系统检查报告在LIS/PACS电子病历又在另一个平台。前端每次打开一个门诊页面背后可能要拼五六个接口的数据而且每个接口的协议、返回格式、超时时间都不一样。这个基于 Java 的门诊服务聚合系统设计源码解决的就是这个痛点——它不是一套新的业务系统而是一层位于前端和各个业务系统之间的聚合服务层把分散的挂号、候诊、缴费、报告、病历数据统一收口以一套 REST 接口对外输出。它适合两类人一类是正在做医院信息化集成、被多系统联调折磨的后端开发另一类是准备做医疗方向毕业设计或课程项目、需要一份完整可运行 Java 源码作为脚手架的学生。你拿到的不是 PPT 式的设计文档而是一套能跑起来、能改、能演示的聚合层工程。2. 聚合层怎么设计BFF 模式与模块边界2.1 为什么门诊业务必须有一层专门做聚合医院的门诊场景和普通互联网业务有一个本质差别业务是跨系统跨数据库的。病人挂完号他的挂号记录在挂号系统里缴费状态在收费系统里候诊序号在分诊叫号系统里检查报告可能要等 LIS 回传。如果让前端直接去调这些系统会遇到三个很现实的问题。第一个问题是接口协议不统一。老的医院系统很多还是 SOAP 接口甚至有的还挂着 WebService新一些的科室系统可能是 HTTP JSON。前端要同时兼容两种协议代码会非常臃肿。第二个问题是数据粒度不匹配。业务系统返回的是它自己的完整数据模型比如挂号系统返回的是一个包含几十个字段的挂号记录对象但门诊首页只需要科室名、医生名、候诊序号、当前叫号四个字段。这个裁剪逻辑放在前端做页面一多就会失控。第三个问题更隐蔽——多次请求的事务边界。前端先查挂号再查候诊再查缴费中间任何一次网络抖动页面就白屏而且没有重试和降级的空间。所以聚合层本质上是一个 BFFBackend for Frontend模式的落地。它把「前端需要什么数据」定义成一组粗粒度的接口然后由服务端去编排下游系统的调用顺序、组装结果、处理异常和降级。对前端来说它只需要面对一个域名、一套 JSON 格式、一组合适的超时配置。2.2 模块怎么划按业务域拆不按技术层拆这套源码的模块划分思路值得抄。它不是按 controller / service / dao 这种技术层来拆的而是按业务域拆。整个工程分成了几个核心模块。hospital-aggregation ├── hospital-common // 通用工具、统一返回体、异常定义 ├── hospital-gateway // 聚合入口路由与鉴权 ├── hospital-aggregate-api // 对外暴露的聚合接口定义 ├── hospital-aggregate-core // 聚合编排核心服务调用与数据合并 ├── hospital-client-sso // 挂号系统客户端封装 ├── hospital-client-triage // 分诊候诊客户端封装 ├── hospital-client-charge // 缴费系统客户端封装 ├── hospital-client-report // 检查报告客户端封装 └── hospital-client-emr // 电子病历客户端封装每个hospital-client-*模块只干一件事封装某一个下游系统的对接细节。里面包括下游系统的 URL 配置、鉴权 token 的获取、请求参数转换、响应解析、超时设置。aggregate-core不关心某个下游系统内部怎么实现它只面向这些 client 模块暴露的统一接口编程。这样划分的收益是门诊系统如果换了厂商比如从 A 厂的挂号系统换成 B 厂的只需要改hospital-client-sso这个模块的内部实现聚合编排核心一行都不用动。这个边界清晰与否直接决定了这套源码是能落地还是只能看看。2.3 技术选型为什么是 Spring Boot MyBatis源码的技术栈是 Java 生态里最主流也最保守的组合Spring Boot 负责装配和接口暴露MyBatis 负责持久层MySQL 做聚合层自己的元数据库Redis 做缓存。没有引入 Spring Cloud 全家桶也没有用消息队列。这个选型是有意为之的——聚合层本身不产生业务数据它不需要强一致性的分布式事务也不需要通过 MQ 做异步削峰。聚合层自己的数据库只存三类数据下游系统的接口配置、调用日志、以及幂等校验用的凭证表。这类数据用不上复杂的 ORM 映射关系MyBatis 的手写 SQL 反而更直观。pom.xml 里的核心依赖也比较克制。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependencymybatis-spring-boot-starter 用 2.3.1 是因为它和 Spring Boot 2.7.x 的兼容性最稳再高版本容易遇到自动配置失效的问题。Redis 在这里做两件事缓存病人基础信息和挂号记录的热点数据以及存放缴费请求的幂等令牌。如果去掉 Redis这套聚合层也能跑但并发一上来下游系统的压力就全暴露了。2.4 聚合层数据库表设计接口配置表与调用日志表聚合层的数据表非常少核心就两张。一张是下游系统接口配置表另一张是聚合调用日志表。前者让系统管理员可以直接在库里调整某个下游接口的超时时间或开关状态不用改代码发版后者用来排查问题时追踪某一次聚合请求分别调了哪些下游、各自耗时多少。CREATE TABLE agg_service_config ( id bigint NOT NULL AUTO_INCREMENT, service_code varchar(64) NOT NULL COMMENT 下游服务编码, service_name varchar(128) DEFAULT NULL COMMENT 服务名称, base_url varchar(255) NOT NULL COMMENT 下游服务地址, timeout_ms int NOT NULL DEFAULT 3000 COMMENT 超时时间(毫秒), enabled tinyint NOT NULL DEFAULT 1 COMMENT 是否启用: 1启用 0停用, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_service_code (service_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT下游服务接口配置表; CREATE TABLE agg_invoke_log ( id bigint NOT NULL AUTO_INCREMENT, trace_id varchar(64) NOT NULL COMMENT 聚合请求追踪ID, service_code varchar(64) NOT NULL COMMENT 下游服务编码, invoke_status tinyint NOT NULL COMMENT 调用状态: 1成功 0失败, cost_ms int NOT NULL COMMENT 下游调用耗时(毫秒), error_msg varchar(512) DEFAULT NULL COMMENT 错误信息, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_trace_id (trace_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT聚合调用日志表;timeout_ms单独做成字段而不是写死在代码里是因为门诊不同下游系统的响应速度差异很大挂号系统通常 200 毫秒内就能返回但报告系统可能要等 LIS 回传3 秒都不一定够。把超时时间做成可配置运维才能在不发版的情况下调优。trace_id字段是排查多系统问题时最重要的线索——一次聚合请求从入口生成一个 traceId贯穿所有下游调用出了问题直接按这个 ID 查所有日志。3. 核心聚合接口实战当日门诊概览的合并逻辑3.1 聚合接口的分层调用链路这套源码里最有代表性的接口是「当日门诊概览」。前端在门诊医生工作站打开一个病人的工作台时需要同时看到病人挂的是哪个科室哪个医生、当前候诊队列排到几号了、有没有待缴费的项目、今天有没有已出的检查报告。这四个数据来自四个不同的下游系统。聚合接口的调用链路分为三层。入口是AggregateController它只负责接收请求、校验参数、调用聚合服务中间是OutpatientOverviewAggService这是核心编排层决定先调哪个下游、哪些可以并行、哪些必须串行、失败怎么降级最底下是四个ClientService每个封装一个下游系统的 HTTP 调用。这样的分层让每一层都能单独测试——你可以先 mock 掉下游把聚合编排逻辑调通再逐个接入真实系统。3.2 聚合编排核心代码并行调用与结果合并先看聚合服务层的核心编排代码。这里用了 Java 的CompletableFuture做并行调用因为挂号信息和候诊队列之间没依赖关系串行等会白白浪费几百毫秒并行可以把整体耗时压到最慢那个服务的耗时级别。public OutpatientOverviewVO getOverview(String patientId, String visitDate) { // 1. 先从缓存拿病人基础信息拿不到再调下游档案系统 PatientBaseDTO patient patientCache.get(patientId); if (patient null) { patient ssoClient.getPatientBaseInfo(patientId); patientCache.put(patientId, patient); } // 2. 并行发起三个无依赖的下游调用 CompletableFutureRegistrationDTO regFuture CompletableFuture.supplyAsync(() - ssoClient.getRegistration(patientId, visitDate), aggThreadPool); CompletableFutureTriageQueueDTO triageFuture CompletableFuture.supplyAsync(() - triageClient.getQueueStatus(patientId, visitDate), aggThreadPool); CompletableFutureListChargeItemDTO chargeFuture CompletableFuture.supplyAsync(() - chargeClient.getPendingChargeItems(patientId), aggThreadPool); // 3. 聚合结果并做局部降级 OutpatientOverviewVO vo new OutpatientOverviewVO(); vo.setPatient(patient); try { vo.setRegistration(regFuture.get(3000, TimeUnit.MILLISECONDS)); } catch (Exception e) { log.error(挂号信息获取失败, patientId{}, patientId, e); vo.setRegistration(null); } try { vo.setTriage(triageFuture.get(3000, TimeUnit.MILLISECONDS)); vo.setTriageAvailable(true); } catch (Exception e) { log.error(候诊队列获取失败, patientId{}, patientId, e); vo.setTriageAvailable(false); } try { vo.setChargeItems(chargeFuture.get(3000, TimeUnit.MILLISECONDS)); } catch (Exception e) { log.error(待缴费项目获取失败, patientId{}, patientId, e); vo.setChargeItems(Collections.emptyList()); } return vo; }这段代码的关键设计有两处。第一用了独立的aggThreadPool而不是ForkJoinPool.commonPool()——医院场景下接口会被低频高并发地调用用公共线程池容易被其他业务阻塞自定义线程池可以单独控制核心线程数和队列长度。第二三个下游调用分别 try-catch任何一个失败都不影响其他数据的展示。比如候诊队列服务挂了页面仍然能显示挂号和缴费信息只是候诊区域显示一个「暂不可用」的占位。get方法的三个参数值得注意第一个是等待时间第二个是时间单位第三个如果缺省就是永远等待。聚合服务里必须显式设置超时否则下游服务假死时你的线程池会被占满新请求全部排队整个聚合层就雪崩了。3.3 线程池参数怎么定看一个实际的配置案例。门诊高峰期通常是上午 9 点到 11 点聚合层每秒大约要处理 50 个概览请求每个请求会发 3 个并行下游调用也就是每秒 150 个下游 QPS。下游系统的平均响应时间在 300 毫秒左右。Bean(aggThreadPool) public ThreadPoolTaskExecutor aggThreadPool() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix(agg-worker-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; }核心线程 8、最大线程 16、队列 500 这组参数不是随便拍的。按照 Littles Law稳定状态下系统中请求数等于吞吐量乘以响应时间50 QPS 乘以 0.3 秒等于 15说明大概只需要 15 个线程就能覆盖峰值。核心线程 8 偏保守会让请求短暂排队但排队也意味着对下游系统的调用被平滑了。CallerRunsPolicy是最后一道保护线程池满了之后新任务不会被丢弃而是由调用方线程自己执行这样在极端流量下系统是变慢而不是报错。3.4 统一返回体与前端约定聚合接口的返回体设计也决定了前端好不好用。源码里统一用ApiResponseT包装结构非常扁平没有医院系统常见的那种层层嵌套。public class ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT success(T data) { ApiResponseT response new ApiResponse(); response.code 0; response.message success; response.data data; return response; } public static T ApiResponseT error(int code, String message) { ApiResponseT response new ApiResponse(); response.code code; response.message message; return response; } }code的定义和下游系统的错误码要解耦。聚合层只认自己的错误码体系0 表示成功1 表示参数错误2 表示部分数据获取失败3 表示全部下游都失败。这样前端只需要判断 code 等于 0 还是 2不需要关心具体是哪个下游出了问题。如果后续要扩展比如增加一个「报告解析中」的状态码只要后端加枚举前端加一个分支即可。4. 缓存与幂等让聚合服务在高峰期扛得住4.1 为什么必须缓存病人基础信息门诊场景有一个特别明显的特征同一个病人在一个就诊周期内会被反复查询。挂号时要查进诊室时医生工作台要查缴费时收费系统要查取药时药房系统还要查。如果每一次查询聚合层都转发到下游病人档案系统高峰期这个下游服务的负载会非常高而它返回的数据——姓名、性别、年龄、过敏史——在一天内几乎不变。源码里对病人基础信息的缓存策略很实用。缓存 key 直接用patientIdvalue 存的是 JSON 序列化后的PatientBaseDTOTTL 设为 12 小时。缓存更新的时机有两个一是手动失效接口用于档案维护后主动清理二是 TTL 自然过期。这个策略不需要引入 Caffeine 或者更复杂的多级缓存Redis 单层就够了。Service public class PatientCacheService { private static final String CACHE_KEY_PREFIX agg:patient:; Autowired private StringRedisTemplate redisTemplate; Autowired private ObjectMapper objectMapper; public PatientBaseDTO get(String patientId) { String json redisTemplate.opsForValue().get(CACHE_KEY_PREFIX patientId); if (json null) { return null; } try { return objectMapper.readValue(json, PatientBaseDTO.class); } catch (JsonProcessingException e) { log.error(缓存反序列化失败, patientId{}, patientId, e); return null; } } public void put(String patientId, PatientBaseDTO patient) { try { String json objectMapper.writeValueAsString(patient); redisTemplate.opsForValue().set(CACHE_KEY_PREFIX patientId, json, 12, TimeUnit.HOURS); } catch (JsonProcessingException e) { log.error(缓存序列化失败, patientId{}, patientId, e); } } }这里有个细节缓存读到了空值也要缓存。比如某病人档案系统里确实没有数据如果不做空值缓存每次请求都会穿透到下游这就是经典的缓存穿透问题。我一般会用CACHE_KEY_PREFIX patientId :empty存一个特殊标记TTL 设短一些比如 5 分钟避免下游刚写入数据后还要等很久才能查到。4.2 防止重复缴费幂等令牌的设计门诊缴费有一个非常具体的并发问题病人点了一次「去缴费」按钮但因为网络原因前端超时重试或者病人手快点了两次缴费请求就可能被提交两次。聚合层转发到收费系统收费系统如果没做幂等就会重复扣费。这是医疗场景里绝对不能出的事故。源码的解决方案是幂等令牌机制。前端在渲染缴费页面时先调用聚合层生成一个幂等令牌后续提交缴费时把这个令牌带上聚合层和收费系统都校验令牌是否已被使用。public String createChargeToken(String patientId, BigDecimal amount) { String token UUID.randomUUID().toString().replace(-, ); String key agg:charge:token: token; // Lua 脚本保证原子性: 不存在才写入, 写入成功返回1 String luaScript return redis.call(SET, KEYS[1], ARGV[1], NX, EX, ARGV[2]); Long result redisTemplate.execute( new DefaultRedisScript(luaScript, Long.class), Arrays.asList(key), patientId : amount.toString(), String.valueOf(600) ); if (result ! null result 1L) { return token; } return null; }用 Lua 脚本而不是先exists再set是为了避免并发下两个请求同时判断 token 不存在、同时写入导致同一个 token 被重复使用。这个是纯 Java 代码容易踩的坑——两个线程同时走到exists返回 false然后都往下执行。Lua 脚本让「检查并写入」成为一个原子操作这个问题就没了。缴费成功后聚合层会主动删除这个 token再次使用就会失败返回「该缴费请求已处理」。token 的 TTL 设为 600 秒超过 10 分钟未支付自动失效前端需要重新获取令牌。这个时间窗口是参考了门诊排队缴费的平均耗时设定的太短会导致病人还没走到收费窗口令牌就过期了。4.3 热点 key 与缓存击穿的应对门诊还有一个技术特征热门科室的号源被集中查询。比如周一早上心内科的号一放出来可能几秒钟内几百个请求同时查同一个科室的余号。这个科室的号源数据缓存在 Redis 里就是一个典型的 热点 key——大量请求集中访问同一个 key在缓存过期的那一瞬间所有请求全部打到下游就是缓存击穿。源码里的处理方式比较简单直接用分布式锁让单个请求去刷新缓存其他请求等锁释放后直接读新缓存。锁本身用 Redis 的SET NX实现锁的 TTL 设为 2 秒。public ListDoctorScheduleDTO getDoctorSchedules(String deptCode) { String cacheKey agg:dept:schedule: deptCode; ListDoctorScheduleDTO schedules queryFromCache(cacheKey); if (schedules ! null) { return schedules; } String lockKey agg:lock:dept:schedule: deptCode; String lockValue UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 2, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { schedules scheduleClient.getSchedulesByDept(deptCode); writeToCache(cacheKey, schedules, 30, TimeUnit.SECONDS); return schedules; } finally { redisTemplate.delete(lockKey); } } // 没抢到锁, 短暂休眠后读缓存, 最多重试3次 for (int i 0; i 3; i) { sleep(50); schedules queryFromCache(cacheKey); if (schedules ! null) { return schedules; } } return scheduleClient.getSchedulesByDept(deptCode); }这个方案有两个注意点。锁的 TTL 一定要大于下游服务的最长响应时间如果下游 3 秒才返回但锁 2 秒就过期了两个请求会同时刷新缓存。另外sleep(50)这种自旋等待是下策更好的办法是用 Redis 的发布订阅或者 Redisson 的tryLock带等待时间但为了保持源码的轻量自旋等待可以接受只是重试次数要控制不能无限循环。5. 避坑与排查门诊聚合最常见的五个实战问题5.1 聚合接口整体超时前端白屏现象门诊医生工作台打开时前端一直转圈最后提示请求超时整个页面空白。原因聚合服务默认超时时间设置过长或者下游服务有一个响应特别慢。最常见的情况是检查报告服务在高峰期要等 LIS 系统的数据回传响应时间从平时的几百毫秒飙升到 5 秒以上。聚合服务在等它的时候占用了线程池的线程后续请求全部排队。解决给每个下游服务单独配置超时时间报告服务这种慢服务单独调大超时其他服务保持短超时。同时聚合编排里用get(timeout)而不是无限等待。最关键的一点超时判定要放在下游调用的future.get上而不是等整个聚合接口结束后再检查。从那以后我每次配置下游超时都会问一句这个服务的最差响应时间是多少在agg_service_config表里把每个服务的timeout_ms按最差情况设置而不是按平均情况。5.2 一个下游服务失败整个聚合接口 500现象候诊叫号系统临时维护导致门诊概览接口直接返回 500挂号和缴费信息也显示不了。原因聚合编排代码里没有做局部异常隔离某个下游调用抛出异常后直接向外传播整个聚合接口失败。这属于典型的「全有或全无」的错误设计。解决聚合层必须接受「部分数据不可用」是一个正常状态。每个下游调用单独 try-catch失败时在返回体里标记对应字段不可用而不是让整个请求失败。前端的约定是接口返回 code0 或 code2只要不是 3 就正常渲染页面缺少的区域显示「暂无数据」或「暂时无法获取」。我在源码的OutpatientOverviewAggService里把四个下游调用全部改成独立 catch并且给每个失败场景打了错误日志——这个日志除了排查问题还有一个用途统计每个下游的可用率作为是否要降级的依据。5.3 下游系统返回的数据格式和聚合层定义的对不上现象接入新的检查报告系统后聚合接口解析 JSON 报错报Unrecognized field或者类型转换异常。原因不同厂商的系统返回的 JSON 字段名、嵌套结构不一致。比如老系统用report_time新系统用reportTime甚至有的系统返回的是数组套数组的结构。聚合层如果强依赖下游的返回结构换一个厂商就要改一段解析代码。解决Client 模块里做一层响应转换Transformer把下游的原始 DTO 转换成聚合层内部定义的统一 DTO。转换逻辑集中在 Client 模块内部这样下游格式变化只影响一个模块。另外在转换层做好字段校验拿不到关键字段时默认值兜底而不是直接抛异常。5.4 用了缓存后数据更新不及时现象病人改了手机号聚合接口还是返回旧手机号前台显示的始终是缓存里的老数据。原因缓存 TTL 设了 12 小时病人档案系统数据更新后没有主动刷新 Redis 缓存导致脏数据长时间存在。解决在缓存更新这里做一个「双删」操作。病案系统的数据变更接口调用聚合层的缓存失效接口先删除 Redis 里的旧缓存再调下游更新数据更新完成后再次删除缓存。为什么删两次因为第一次删除后如果有请求并发读到了旧值并写入缓存第二次删除才能保证这个脏值被清掉。这个操作是分布式缓存更新里的经典套路代码量不大但能避免绝大多数脏数据问题。5.5 测试环境一切正常生产环境频繁超时现象联调环境所有接口都 200一上生产就各种超时和报错。原因测试环境的数据库数据少、下游服务没有并发很多问题复现不出来。生产环境的典型差异是数据库连接池上限不够、Redis 连接池被占满、下游服务 QPS 达到瓶颈。解决上线前至少做三件事。第一用压测工具对聚合接口做 200 并发、持续 5 分钟的压测观察聚合层的线程池指标和下游系统的错误率。第二检查数据库连接池配置spring.datasource.hikari.maximum-pool-size默认 10 不一定够门诊聚合层并发查配置表和日志表时很容易打满。第三把日志级别调到 INFO生产环境至少保留调用日志表的写入方便事后按 traceId 排查。6. 进阶压测、自动降级与可观测性聚合层开发完成后验证它能不能扛住门诊高峰是最重要的事。我常用的压测方式是用 JMeter 模拟并发请求直接打聚合接口。但 JMeter 只能看到外部表现看不到内部每个下游的耗时分布。所以我一般在压测的同时查agg_invoke_log表按service_code分组统计平均耗时和错误率快速定位瓶颈在哪个下游。自动降级的阈值怎么定我习惯用「错误率 熔断时长」两个维度来设定。假设某个下游服务在 10 秒内错误率超过 30%就自动熔断 30 秒熔断期间聚合接口对该服务的调用直接返回降级数据。这个逻辑用一个简单的计数器就能实现每 10 秒统计一次窗口内的调用结果。熔断时长设 30 秒是经验值——太短下游还没恢复就又被放进来太长会导致可用率下降。可观测性的核心是日志链路。聚合层的每个入口生成一个 traceId通过MDC.put(traceId, traceId)放到日志上下文里这样整个请求经过的所有日志都会自动带上这个 ID。每次下游调用前记录一条进入日志调用结束后记录耗时和状态。出了问题拿 traceId 去agg_invoke_log表一查就能看到这个请求调了哪几个下游、每个花了多少毫秒、失败在哪一步。这套源码里有一个细节我觉得特别值得学它把降级方案的数据也作为正常数据返回而不是报错。比如候诊队列获取失败时返回一个空队列对象并标记triageAvailablefalse前端的表现是「候诊信息暂不可用」而不是白屏。这种设计思路不是我总结出来的是这套源码里本来就有的它体现的是聚合层作为「前端和数据源之间的缓冲带」这个定位——宁可给部分数据不能让页面完全不可用。从那以后我每次写聚合层接口都强制走一遍这个检查清单有没有每个下游单独的超时控制有没有局部降级而不是整体失败缓存更新有没有双删幂等校验是不是原子的traceId 贯穿了吗五条过完这个接口才算能上生产。希望这份基于 Java 的门诊服务聚合系统源码能帮你少走弯路把多系统集成的碎活变成一个清晰可维护的工程。本文还有配套的精品资源点击获取