# AppConfig.java

    package cloud.wconnect.rateshop.config;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.config.BaseConfig;
    import cloud.wconnect.commonlib.exception.handler.CustomAsyncExceptionHandler;
    import cloud.wconnect.commonlib.redis.*;
    import cloud.wconnect.commonlib.service.*;
    import cloud.wconnect.commonlib.util.LineMessageUtil;
    import cloud.wconnect.commonlib.util.S3ControllerUtil;
    import cloud.wconnect.commonlib.util.UpdateLogUtil;
    import cloud.wconnect.sns.http.processor.Sender;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.hc.client5.http.classic.HttpClient;
    import org.apache.hc.client5.http.config.ConnectionConfig;
    import org.apache.hc.client5.http.config.RequestConfig;
    import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
    import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
    import org.apache.hc.client5.http.impl.classic.HttpClients;
    import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
    import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
    import org.apache.hc.core5.http.HttpHeaders;
    import org.apache.hc.core5.http.message.BasicHeader;
    import org.apache.hc.core5.util.TimeValue;
    import org.apache.hc.core5.util.Timeout;
    import org.dozer.DozerBeanMapper;
    import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
    import org.springframework.lang.NonNull;
    import org.springframework.retry.annotation.EnableRetry;
    import org.springframework.scheduling.annotation.AsyncConfigurer;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.web.client.DefaultResponseErrorHandler;
    import org.springframework.web.client.RestTemplate;
    import software.amazon.awssdk.services.sns.model.PublishResponse;

    import java.util.Arrays;
    import java.util.concurrent.Executor;

    /**
     * @author kazaff
     */
    @Configuration
    @EnableAsync
    @EnableRetry
    @EnableScheduling
    @Slf4j
    public class AppConfig extends BaseConfig implements AsyncConfigurer {

        @Value("${spring.application.name}:${spring.profiles.active}:${spring.cloud.config.label}")
        private String configKey;

        @Value("http://localhost:${management.server.port}/actuator/refresh")
        private String configRefreshEndpoint;

        /**
         * Bean映射类
         */
        @Bean
        public BeanMapper beanMapper(DozerBeanMapper dozerBeanMapper) {
            return new BeanMapper(dozerBeanMapper);
        }

        /**
         * Dozer映射器
         */
        @Bean(name = "org.dozer.Mapper")
        public DozerBeanMapper dozerBean() {
            DozerBeanMapper dozerBeanMapper = new DozerBeanMapper();
            dozerBeanMapper.setMappingFiles(Lists.newArrayList(
                    "dozer/base.xml"
            ));

            return dozerBeanMapper;
        }

        /**
         * 异步执行器
         *
         * @return Executor
         * @author chenx
         * @date 2021/3/1
         */
        @Bean
        @Primary
        @Override
        @NonNull
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(25);
            executor.setMaxPoolSize(30);
            executor.setQueueCapacity(600);
            executor.setWaitForTasksToCompleteOnShutdown(true);
            executor.setAwaitTerminationSeconds(30);
            executor.setThreadNamePrefix("RateShopExecutor-");
            executor.initialize();

            return executor;
        }

        /**
         * 异步异常处理类
         *
         * @return AsyncUncaughtExceptionHandler
         * @author chenx
         * @date 2021/3/1
         */
        @Override
        @NonNull
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return new CustomAsyncExceptionHandler();
        }

        /**
         * 自定义RestTemplate
         */
        @Bean(name = "restTemplate")
        public RestTemplate restTemplate() {
            /*
             * 连接池配置
             *
             * ConnectionConfig.setConnectTimeout：
             * 定义建立连接的超时时间（即 TCP 握手的最长等待时间）。
             * 发生在获取连接后，客户端尝试与服务器建立连接的阶段。
             * 示例：服务器不可达时触发。
             * ConnectionConfig.setSocketTimeout / RequestConfig.setResponseTimeout：
             * 定义数据读取的超时时间（等待服务器响应的时间）。
             * 发生在连接建立后，请求发送和接收数据的阶段。
             * 示例：服务器响应过慢时触发。
             * ConnectionConfig.setTimeToLive
             * 配置连接池中连接的最大存活时间,一旦连接的存活时间超过这个值，HttpClient 会将其视为过期并关闭，即使它仍然有效。
             */
            PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                    .setMaxConnTotal(20) // 总连接数
                    .setMaxConnPerRoute(10) // 同路由的并发数
                    .setDefaultConnectionConfig(ConnectionConfig.custom()
                            .setConnectTimeout(Timeout.ofSeconds(5)) // 连接超时 5 秒
                            .setSocketTimeout(Timeout.ofSeconds(5)) // 数据读取超时 5 秒
                            .setTimeToLive(TimeValue.ofSeconds(30)) // 存活时间 30 秒
                            .build())
                    .build();

            /*
             * HttpClient配置
             * setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(500)))
             *
             * setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
             * 决定连接在完成请求后保持存活的具体时间，依赖服务器的 Keep-Alive 头。
             * 如果服务器返回 Keep-Alive: timeout=5，连接会在 5 秒后关闭（但不会超过 30 秒，因为 timeToLive 是 30 秒）。
             * 如果服务器未指定 Keep-Alive 头，连接会保持存活直到 timeToLive（30 秒）到期。
             * DefaultConnectionKeepAliveStrategy 适合大多数情况，因为它尊重服务器的 Keep-Alive 设置，灵活且兼容性强
             *
             * RequestConfig.setConnectionRequestTimeout：
             * 定义从连接池获取连接的超时时间。
             * 发生在请求执行前，客户端尝试从连接池中获取可用连接的阶段。
             * 示例：连接池中所有连接都被占用且无可用连接时触发。
             */
            HttpClient httpClient = HttpClients.custom()
                    .setConnectionManager(connectionManager)
                    .setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, TimeValue.ofMilliseconds(500))) // 重试 3 次，重试间隔500毫秒
                    .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE) // 设置默认的 KeepAlive 策略
                    .setDefaultRequestConfig(RequestConfig.custom()
                            .setConnectionRequestTimeout(Timeout.ofSeconds(2)) //设置从连接池获取连接的超时时间 2秒
                            .build())
                    .setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "gzip,deflate"),
                            new BasicHeader(HttpHeaders.CONNECTION, "keep-alive"))) // 设置默认请求头
                    .build();

            // region HttpComponentsClientHttpRequestFactory
            HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
            // 创建 RestTemplate
            RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
            restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
            return restTemplate;
        }

        @Bean
        @ConditionalOnProperty(name = "w-connect.configServer.enabled", havingValue = "true")
        public ConfigChangeWatcher getConfigChangeWatcher(StringRedisTemplate redisTemplate) {
            return new ConfigChangeWatcher(configKey, redisTemplate);
        }

        @Bean
        @ConditionalOnProperty(name = "w-connect.configServer.enabled", havingValue = "true")
        public ConfigChangeTrigger getConfigChangeTrigger(RestTemplate restTemplate) {
            return new ConfigChangeTrigger(configRefreshEndpoint, restTemplate);
        }

        /**
         * @return cloud.wconnect.commonlib.service.MailInternalService
         * @description 注入邮件内部bean
         * @author lichen
         * @date 2023/3/10 14:04
         */
        @Bean
        public MailInternalService mailInternalService() {
            return new MailInternalService();
        }

        /**
         * 资产远程服务类
         */
        @Bean
        public PropertyInternalService propertyInternalService() {
            return new PropertyInternalService();
        }

        /**
         * 租户远程服务类
         */
        @Bean
        public TenantInternalService tenantInternalService() {
            return new TenantInternalService();
        }

        /**
         * 用户远程服务类
         *
         * @return cloud.wconnect.commonlib.service.UserInternalService
         * @author lichen
         * @date 2023/4/27
         */
        @Bean
        public UserInternalService userInternalService() {
            return new UserInternalService();
        }

        /**
         * 权限远程服务类
         */
        @Bean
        public PermissionInternalService permissionInternalService() {
            return new PermissionInternalService();
        }

        /**
         * 角色远程服务类
         */
        @Bean
        public RoleInternalService roleInternalService() {
            return new RoleInternalService();
        }

        /**
         * 修改日志
         */
        @Bean
        public UpdateLogUtil updateLogUtil() {
            return new UpdateLogUtil();
        }

        /**
         * 通知远程服务类
         *
         * @return cloud.wconnect.commonlib.service.NotifyInternalService
         * @author lichen
         * @date 2023/11/22
         */
        @Bean
        public NotifyInternalService notifyInternalService() {
            return new NotifyInternalService();
        }

        /**
         * 生成line消息的工具类
         *
         * @return cloud.wconnect.commonlib.util.LineMessageUtil
         * @author lichen
         * @date 2023/11/22
         */
        @Bean
        public LineMessageUtil lineMessageUtil() {
            return new LineMessageUtil();
        }

        /**
         * 资产国际化配置服务类
         */
        @Bean
        public PropertyI18nRedisService propertyI18nRedisService() {
            return new PropertyI18nRedisService();
        }

        /**
         * 租户缓存
         */
        @Bean
        public TenantRedisService tenantRedisService() {
            return new TenantRedisService();
        }

        /**
         * 公共 service
         *
         * @author yyt
         * @date 2024/1/27
         **/
        @Bean
        public CommonService commonService() {
            return new CommonService();
        }

        /**
         * 用户组内部服务类
         *
         * @return cloud.wconnect.commonlib.service.UsergroupInternalService
         * @author lichen
         * @date 2024/3/8
         */
        @Bean
        public UsergroupInternalService usergroupInternalService() {
            return new UsergroupInternalService();
        }

        /**
         * 调度任务内部接口
         *
         * @return cloud.wconnect.commonlib.service.SchedulerInternalService
         * @author lichen
         * @date 2024/3/15
         */
        @Bean
        public SchedulerInternalService schedulerInternalService() {
            return new SchedulerInternalService();
        }

        /**
         * FeatureInternalService
         */
        @Bean
        public FeatureInternalService featureInternalService() {
            return new FeatureInternalService();
        }

        /**
         * 资产房间数配置
         */
        @Bean
        public RoomsConfigInternalService roomsConfigInternalService() {
            return new RoomsConfigInternalService();
        }

        /**
         * FeatureRedisService
         */
        @Bean
        public FeatureRedisService featureRedisService() {
            return new FeatureRedisService();
        }

        // --- 以下bean为测试环境使用，生产环境不会使用 ---
        @Bean("3pSender")
        @ConditionalOnProperty(prefix = "third-party", value = "enabled", havingValue = "false")
        public Sender getSender() {
            return new EmptySender();
        }

        static class EmptySender extends Sender {
            public EmptySender() {
                super(null, null);
            }

            @Override
            public PublishResponse publish(String subject, String messageJson) {
                return null;
            }
        }
        // --- 以上bean为测试环境使用，生产环境不会使用 -

        /**
         * s3ControllerUtil
         *
         * @return cloud.wconnect.commonlib.util.S3ControllerUtil
         * @author lichen
         * @date 2025/5/12
         */
        @Bean
        public S3ControllerUtil s3ControllerUtil() {
            return new S3ControllerUtil();
        }

        /**
         * 资产营业日服务类
         */
        @Bean
        public PropertyBusinessDateRedisService propertyBusinessDateRedisService() {
            return new PropertyBusinessDateRedisService();
        }

        /**
         * property room sold
         */
        @Bean
        public PropertyRoomSoldInternalService propertyRoomSoldInternalService() {
            return new PropertyRoomSoldInternalService();
        }

        /**
         * 公共 service
         *
         * @author yyt
         * @date 2026/3/10
         **/
        @Bean
        public CalculatePropertyOperationService calculateRoomsService() {
            return new CalculatePropertyOperationService();
        }

        /**
         * property revenue description
         */
        @Bean
        public PropertyRevenueDescriptionInternalService propertyRevenueDescriptionInternalService() {
            return new PropertyRevenueDescriptionInternalService();
        }

    }