package com.ruoyi.publicity.api; import com.ruoyi.common.core.service.PublicityApi; import com.ruoyi.publicity.config.PublicityProperties; import com.ruoyi.publicity.config.PublicitySwitch; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 宣教模块对外门面(常驻 Bean),实现 ruoyi-common 里的 {@link PublicityApi}。 * *

为什么实现 ruoyi-common 的接口,而不是自带一个

* 调用方是 {@code ruoyi-inhospvisit},而它只依赖 ruoyi-common(不能依赖 ruoyi-publicity, * 否则 {@code inhospvisit <-> publicity} 双向依赖 → Maven 构建失败)。 * 所以契约放在 ruoyi-common,本模块实现它; * 调用方用 {@code ObjectProvider}/{@code SpringUtils.getBean} 按类型查找 + 降级。 * —— 与满意度的 {@code SatisfactionFacade} 实现 ruoyi-common 的 * {@code ISatisfactionCategoryService} 完全同一手法。 * *

关闭时的行为

* 返回 0(没有宣教要做)并打 WARN,不抛异常。 * 宣教失败绝不能拖垮出入院随访主流程。 */ @Slf4j @Component public class PublicityFacade implements PublicityApi { private final PublicityProperties properties; private final PublicitySwitch publicitySwitch; /** * 内部实现({@link DefaultPublicityApi}); * 用 {@code ObjectProvider} 惰性取,即使实现缺失也不至于注入失败。 */ private final ObjectProvider apiProvider; /** * Spring 使用的构造器。 *

* ⚠️ 必须显式标注 {@code @Autowired}:本类有两个公开构造器, * 不标注会因"构造器不唯一"退回无参构造器 → 启动报 {@code No default constructor found}。 * —— 满意度模块实机踩到的坑,此处刻意规避。 */ @Autowired public PublicityFacade(PublicityProperties properties, PublicitySwitch publicitySwitch, ObjectProvider apiProvider) { this.properties = properties; this.publicitySwitch = publicitySwitch; this.apiProvider = apiProvider; } /** * 供测试/非 Spring 环境直接构造 */ public PublicityFacade(PublicityProperties properties, PublicityApi delegate, String sysConfigValue) { this(properties, new PublicitySwitch(properties, sysConfigValue), new FixedProvider(delegate)); } /** * 取运行时开关(供测试断言 / 将来若加拦截器时复用) */ public PublicitySwitch getPublicitySwitch() { return publicitySwitch; } /** * 取内部实现;关闭、缺失、或只找到自己时返回 {@code null}。 *

* ⚠️ 必须排除"自己":本类自身也实现了 {@link PublicityApi}, * Spring 按类型查找时若没有别的实现,会把自引用当兜底候选返回, * 于是 {@code delegate.dealInHospPublicity(...)} 变成无限递归 → {@code StackOverflowError}。 *

* 用 {@code stream()} 而不是 {@code getIfAvailable()}:后者候选 >1 时抛 * {@code NoUniqueBeanDefinitionException},会被吞成 null,掩盖配置错误。 */ private PublicityApi delegateOrNull() { if (apiProvider == null) { return null; } try { return apiProvider.stream() .filter(api -> api != this) .findFirst() .orElse(null); } catch (Exception e) { log.warn("【宣教】获取内部实现失败,按未启用处理:{}", e.toString()); return null; } } /** * 宣教模块是否可用(开关打开 且 实现存在) */ public boolean isEnabled() { return publicitySwitch.isEnabled() && delegateOrNull() != null; } @Override public int dealInHospPublicity(String config) { if (!isEnabled()) { log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),入院宣教跳过。config={}", config); return 0; } return delegateOrNull().dealInHospPublicity(config); } @Override public int dealMedicalPublicity(String config) { if (!isEnabled()) { log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),药品宣教跳过。config={}", config); return 0; } return delegateOrNull().dealMedicalPublicity(config); } /** * 固定值的 {@code ObjectProvider} 实现:仅用于非 Spring 环境/测试 */ private static final class FixedProvider implements ObjectProvider { private final PublicityApi api; private FixedProvider(PublicityApi api) { this.api = api; } @Override public PublicityApi getObject(Object... args) { return api; } @Override public PublicityApi getIfAvailable() { return api; } @Override public PublicityApi getIfUnique() { return api; } @Override public PublicityApi getObject() { return api; } @Override public java.util.stream.Stream stream() { return api == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(api); } } }