Sfoglia il codice sorgente

1、调整初始配置动态更新

yfh 3 settimane fa
parent
commit
131ecc2129
25 ha cambiato i file con 2596 aggiunte e 411 eliminazioni
  1. 0 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java
  2. 10 7
      fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java
  3. 24 4
      fs-doctor-app/src/main/java/com/fs/app/controller/DoctorController.java
  4. 0 2
      fs-doctor-app/src/main/java/com/fs/app/controller/DrugReportController.java
  5. 1 2
      fs-doctor-app/src/main/java/com/fs/app/controller/PrescribeController.java
  6. 1 1
      fs-quartz/src/main/java/com/fs/quartz/config/ScheduleConfig.java
  7. 344 0
      fs-service/src/main/java/com/fs/config/mq/RocketmqConfig.java
  8. 12 0
      fs-service/src/main/java/com/fs/config/saas/ProjectConfig.java
  9. 25 20
      fs-service/src/main/java/com/fs/config/tencent/TencentCOSClientConfig.java
  10. 220 0
      fs-service/src/main/java/com/fs/config/tencent/TencentProperties.java
  11. 3 13
      fs-service/src/main/java/com/fs/core/config/WxMpConfiguration.java
  12. 401 0
      fs-service/src/main/java/com/fs/core/config/WxMpProperties.java
  13. 143 18
      fs-service/src/main/java/com/fs/core/config/WxPayConfiguration.java
  14. 330 46
      fs-service/src/main/java/com/fs/core/config/WxPayProperties.java
  15. 4 30
      fs-service/src/main/java/com/fs/course/service/impl/TencentCloudCosService.java
  16. 8 38
      fs-service/src/main/java/com/fs/event/WeixinTemplateService.java
  17. 253 19
      fs-service/src/main/java/com/fs/his/config/WxMiniappTempConfig.java
  18. 0 1
      fs-service/src/main/java/com/fs/his/service/impl/FsDoctorServiceImpl.java
  19. 0 2
      fs-service/src/main/java/com/fs/his/service/impl/FsFollowServiceImpl.java
  20. 0 1
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderMsgServiceImpl.java
  21. 17 11
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  22. 98 35
      fs-service/src/main/java/com/fs/wx/cp/config/WxCpConfiguration.java
  23. 287 51
      fs-service/src/main/java/com/fs/wx/cp/config/WxCpProperties.java
  24. 290 0
      fs-service/src/main/java/com/fs/wx/miniapp/config/WxMaProperties.java
  25. 125 109
      fs-service/src/main/resources/application-config-dev.yml

+ 0 - 1
fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java

@@ -507,7 +507,6 @@ public class CompanyUserAllController extends BaseController {
     @ApiOperation("校验客服是否注册新的im")
     @PostMapping("/accountCheck")
     public R accountCheck(@RequestBody Map<String, String> userIdMap){
-       ObjectMapper objectMapper = new ObjectMapper();
 
         String configKey = "projectConfig";
         SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);

+ 10 - 7
fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java

@@ -15,6 +15,7 @@ import com.fs.store.dto.TemplateMessageSendRequestDTO;
 import com.fs.store.dto.WeXinAccessTokenDTO;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.wx.miniapp.config.WxMaProperties;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
@@ -36,18 +37,20 @@ public class MiniProgramSubTask {
     private final IWechatMiniProgrService wechatMiniProgrService;
 
     private final LiveMiniprogramSubNotifyTaskMapper notifyTaskMapper;
-
-    private final SysConfigMapper sysConfigMapper;
+    @Autowired
+    private WxMaProperties wxMaProperties;
     /**
      * 小程序订阅通知
      */
     public void notifyMiniLiveAppSub(){
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.Wx.Miniapp.Config config= projectConfig.getWx().getMiniapp().getConfigs().get(0);
 
+        List<WxMaProperties.Config> configs = wxMaProperties.getConfigs();
+        if (CollectionUtils.isEmpty(configs)) {
+            log.error("小程序配置为空,无法执行订阅通知任务");
+            return;
+        }
+        WxMaProperties.Config config = configs.get(0);
+        log.info("使用小程序配置,appid: {}", config.getAppid());
 
         log.info("小程序直播订阅通知定时任务");
         // 先获取所有可用待处理任务

+ 24 - 4
fs-doctor-app/src/main/java/com/fs/app/controller/DoctorController.java

@@ -11,6 +11,7 @@ import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.sign.Md5Utils;
+import com.fs.config.saas.ProjectConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.FsExtractDTO;
 import com.fs.his.enums.FsExtractTypeEnum;
@@ -21,10 +22,11 @@ import com.fs.his.param.FsDoctorExtractListSParam;
 import com.fs.his.service.*;
 import com.fs.his.vo.FsDoctorBillListSVO;
 import com.fs.his.vo.FsDoctorExtractListSVO;
-import com.fs.im.config.IMConfig;
 import com.fs.im.service.OpenIMService;
 import com.fs.im.service.OpenIMService;
 import com.fs.sms.service.SmsService;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -69,6 +71,24 @@ public class DoctorController extends  AppBaseController {
     private OpenIMService openIMService;
     @Autowired
     private SmsService smsService;
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    /**
+     * 查询配置文件
+     *
+     * @return
+     */
+    private ProjectConfig.OpenIM getImConfig(){
+
+        String configKey = "projectConfig";
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+        ProjectConfig projectConfig = com.alibaba.fastjson.JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
+
+        ProjectConfig.OpenIM im = projectConfig.getOpenIM();
+        return im;
+    }
+
     @ApiOperation("登录")
     @PostMapping("/login")
     public R login(@Validated  @RequestBody DoctorLoginParam param) {
@@ -121,7 +141,7 @@ public class DoctorController extends  AppBaseController {
             requestBody = new JSONObject();
             userIds.add(userId);
             requestBody.put("checkUserIDs", userIds);
-            String body = HttpRequest.post(IMConfig.URL+"/user/account_check")
+            String body = HttpRequest.post(getImConfig().getUrl()+"/user/account_check")
                     .header("operationID", String.valueOf(System.currentTimeMillis()))
                     .header("token", adminToken)
                     .body(requestBody.toString())
@@ -148,7 +168,7 @@ public class DoctorController extends  AppBaseController {
                     requestBody = new JSONObject();
                     userIds.add(userId);
                     requestBody.put("users", users);
-                    String body1 = HttpRequest.post(IMConfig.URL+"/user/user_register")
+                    String body1 = HttpRequest.post(getImConfig().getUrl()+"/user/user_register")
                             .header("operationID", String.valueOf(System.currentTimeMillis()))
                             .header("token", adminToken).body(requestBody.toString()).execute().body();
                     log.info("注册结果:"+body1);
@@ -162,7 +182,7 @@ public class DoctorController extends  AppBaseController {
             requestBody = new JSONObject();
             requestBody.put("platformID",5);
             requestBody.put("userID",userId);
-            String body1 = HttpRequest.post(IMConfig.URL+"/auth/get_user_token")
+            String body1 = HttpRequest.post(getImConfig().getUrl()+"/auth/get_user_token")
                     .header("operationID", String.valueOf(System.currentTimeMillis()))
                     .header("token", adminToken)
                     .body(requestBody.toString()).execute().body();

+ 0 - 2
fs-doctor-app/src/main/java/com/fs/app/controller/DrugReportController.java

@@ -21,8 +21,6 @@ import com.fs.his.service.IFsDrugReportService;
 import com.fs.his.service.IFsFollowService;
 import com.fs.his.vo.FsDrugReportDVO;
 import com.fs.his.vo.FsDrugReportListDVO;
-import com.fs.his.vo.FsFollowListDVO;
-import com.fs.im.config.IMConfig;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.dto.*;
 import com.fs.im.service.IImService;

+ 1 - 2
fs-doctor-app/src/main/java/com/fs/app/controller/PrescribeController.java

@@ -21,7 +21,6 @@ import com.fs.his.param.*;
 import com.fs.his.service.*;
 import com.fs.his.vo.FsDoctorPrescribeListDVO;
 import com.fs.his.vo.FsPrescribeListDVO;
-import com.fs.im.config.IMConfig;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.dto.MsgCustomDTO;
 import com.fs.im.dto.OpenImMsgDTO;
@@ -378,7 +377,7 @@ public class PrescribeController extends  AppBaseController {
         //fsUserCouponService.updateFsUserCouponStatusByLimtType2();
 //        openIMService.checkAndImportFriendByDianBo(Long.parseLong(map.get("sendId")),map.get("userId"),qwExternalContact.getCorpId());
         //OpenImResponseDTO openImResponseDTO = openIMService.sendCourse(Long.parseLong(map.get("userId")), Long.parseLong(map.get("sendId")), "/pages/courseAnswer/index?link=1932017457275338752", "《五仙传医2.0》","https://cos.his.cdwjyyh.com/fs/20241108/a8ed49ae9a264c7483cec5bdcbcf6060.png");
-//        log.info("请求地址{}",IMConfig.URL);
+//        log.info("请求地址{}",getImConfig().getUrl());
 ////        log.info("前缀{}",IMConfig.PREFIX);
 //        OpenImResponseDTO openImResponseDTO = openIMService.sendUserInformation(4050279479l,10086l,1l);
         CollectionInfoConfirmParam collectionInfoConfirmParam = new CollectionInfoConfirmParam();

+ 1 - 1
fs-quartz/src/main/java/com/fs/quartz/config/ScheduleConfig.java

@@ -49,7 +49,7 @@ public class ScheduleConfig
         // 可选,QuartzScheduler
         // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
         factory.setOverwriteExistingJobs(true);
-        // 设置自动启动,默认为true 切记调整为true
+        // 设置自动启动,默认为true 切记调整为true1
 //        factory.setAutoStartup(true);
         factory.setAutoStartup(false);
 

+ 344 - 0
fs-service/src/main/java/com/fs/config/mq/RocketmqConfig.java

@@ -0,0 +1,344 @@
+package com.fs.config.mq;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.client.producer.DefaultMQProducer;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Component
+@Slf4j
+@Configuration
+public class RocketmqConfig {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+
+    @Autowired(required = false)
+    private RedisCache redisCache;
+
+    // 本地缓存
+    private final Map<String, Object> localCache = new ConcurrentHashMap<>();
+    private static final String ROCKETMQ_CONFIG_CACHE_KEY = "rocketmq_config_cache";
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    private static final int LOCAL_CACHE_EXPIRE = 30 * 60 * 1000; // 30分钟
+
+    private final ReentrantLock lock = new ReentrantLock();
+    private long lastLoadTime = 0;
+
+    private final AtomicBoolean isInitialized = new AtomicBoolean(false);
+
+    @PostConstruct
+    public void init() {
+        log.info("RocketmqConfig初始化完成");
+        // 预加载配置
+        getRocketmqConfigFromDB();
+    }
+
+    /**
+     * 核心方法:获取RocketMQ配置
+     */
+    public ProjectConfig.Rocketmq getRocketmqConfigFromDB() {
+        try {
+            // 检查本地缓存
+            if (System.currentTimeMillis() - lastLoadTime < LOCAL_CACHE_EXPIRE) {
+                ProjectConfig.Rocketmq cached = (ProjectConfig.Rocketmq) localCache.get(ROCKETMQ_CONFIG_CACHE_KEY);
+                if (cached != null) {
+                    return cached;
+                }
+            }
+
+            // 检查Redis缓存
+            if (redisCache != null) {
+                ProjectConfig cachedConfig = getConfigFromRedisCache();
+                if (cachedConfig != null && cachedConfig.getRocketmq() != null) {
+                    log.debug("从Redis缓存获取RocketMQ配置");
+                    localCache.put(ROCKETMQ_CONFIG_CACHE_KEY, cachedConfig.getRocketmq());
+                    lastLoadTime = System.currentTimeMillis();
+                    return cachedConfig.getRocketmq();
+                }
+            }
+
+            // 查询数据库
+            lock.lock();
+            try {
+                // 双重检查
+                if (System.currentTimeMillis() - lastLoadTime < LOCAL_CACHE_EXPIRE) {
+                    ProjectConfig.Rocketmq cached = (ProjectConfig.Rocketmq) localCache.get(ROCKETMQ_CONFIG_CACHE_KEY);
+                    if (cached != null) {
+                        return cached;
+                    }
+                }
+
+                // 查询数据库
+                String configKey = "projectConfig";
+                SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+                if (sysConfig == null) {
+                    log.warn("数据库中没有找到配置: {}", configKey);
+                    return null;
+                }
+
+                if (StringUtils.isBlank(sysConfig.getConfigValue())) {
+                    log.warn("数据库配置值为空: {}", configKey);
+                    return null;
+                }
+
+                // 解析JSON
+                ProjectConfig projectConfig;
+                try {
+                    projectConfig = JSONObject.parseObject(
+                            sysConfig.getConfigValue(),
+                            ProjectConfig.class
+                    );
+                } catch (Exception e) {
+                    log.error("解析JSON失败", e);
+                    return null;
+                }
+
+                if (projectConfig == null) {
+                    log.warn("项目配置解析失败");
+                    return null;
+                }
+
+                ProjectConfig.Rocketmq rocketmqConfig = projectConfig.getRocketmq();
+                if (rocketmqConfig == null) {
+                    log.warn("ProjectConfig中没有rocketmq节点");
+                    return null;
+                }
+
+                log.info("从数据库加载RocketMQ配置成功");
+
+                // 更新缓存
+                localCache.put(ROCKETMQ_CONFIG_CACHE_KEY, rocketmqConfig);
+                lastLoadTime = System.currentTimeMillis();
+
+                // 保存到Redis
+                if (redisCache != null) {
+                    saveConfigToRedisCache(projectConfig);
+                }
+
+                return rocketmqConfig;
+
+            } finally {
+                lock.unlock();
+            }
+
+        } catch (Exception e) {
+            log.error("获取RocketMQ配置失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 从Redis获取配置
+     */
+    private ProjectConfig getConfigFromRedisCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (StringUtils.isNotBlank(cachedJson)) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取配置失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 保存配置到Redis
+     */
+    private void saveConfigToRedisCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, 30, java.util.concurrent.TimeUnit.MINUTES);
+            log.debug("配置已保存到Redis缓存");
+        } catch (Exception e) {
+            log.warn("保存配置到Redis缓存失败", e);
+        }
+    }
+
+    /**
+     * ===================== 配置获取方法 =====================
+     */
+
+    public String getNameServer() {
+        ProjectConfig.Rocketmq config = getRocketmqConfigFromDB();
+        return config != null && StringUtils.isNotBlank(config.getNameServer()) ?
+                config.getNameServer() : "";
+    }
+
+    public String getProducerGroup() {
+        ProjectConfig.Rocketmq config = getRocketmqConfigFromDB();
+        if (config == null || config.getProducer() == null) {
+            return "DEFAULT_PRODUCER_GROUP";
+        }
+        return StringUtils.defaultString(config.getProducer().getGroup(), "DEFAULT_PRODUCER_GROUP");
+    }
+
+    public boolean isConfigValid() {
+        return StringUtils.isNotBlank(getNameServer());
+    }
+
+    /**
+     * ===================== RocketMQ Bean定义 =====================
+     * 使用@Lazy注解延迟初始化
+     */
+
+    /**
+     * 创建DefaultMQProducer Bean
+     * 使用@Lazy确保只在需要时才创建
+     */
+    @Bean(name = "defaultMQProducer", destroyMethod = "shutdown")
+    @Lazy
+    public DefaultMQProducer defaultMQProducer() {
+        String nameServer = getNameServer();
+
+        if (StringUtils.isBlank(nameServer)) {
+            log.warn("RocketMQ NameServer未配置,创建dummy生产者");
+            return createDummyProducer();
+        }
+
+        try {
+            String producerGroup = getProducerGroup();
+
+            log.info("创建RocketMQ生产者: nameServer={}, group={}", nameServer, producerGroup);
+
+            DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
+            producer.setNamesrvAddr(nameServer);
+            producer.setSendMsgTimeout(getSendMessageTimeout());
+            producer.setRetryTimesWhenSendFailed(getRetryTimesWhenSendFailed());
+
+            // 启动生产者
+            producer.start();
+            isInitialized.set(true);
+
+            log.info("RocketMQ生产者启动成功");
+            return producer;
+
+        } catch (Exception e) {
+            log.error("初始化RocketMQ生产者失败", e);
+            return createDummyProducer();
+        }
+    }
+
+    /**
+     * 创建RocketMQTemplate Bean
+     * 使用@Lazy确保只在需要时才创建
+     */
+    @Bean(name = "rocketMQTemplate")
+    @Lazy
+    public RocketMQTemplate rocketMQTemplate() {
+        // 获取生产者(可能是dummy)
+        DefaultMQProducer producer = defaultMQProducer();
+
+        try {
+            log.info("创建RocketMQTemplate");
+
+            // 检查是否是dummy生产者
+            if ("DUMMY_PRODUCER_GROUP".equals(producer.getProducerGroup())) {
+                log.warn("使用dummy RocketMQTemplate,实际不会发送消息");
+                return createDummyTemplate(producer);
+            }
+
+            // 创建真正的Template
+            RocketMQTemplate template = new RocketMQTemplate() {
+                @Override
+                public void afterPropertiesSet() {
+                    // 重写,避免重复启动生产者
+                    log.debug("跳过自动启动,生产者已手动管理");
+                }
+            };
+            template.setProducer(producer);
+
+            log.info("RocketMQTemplate创建成功");
+            return template;
+
+        } catch (Exception e) {
+            log.error("创建RocketMQTemplate失败", e);
+            return createDummyTemplate(producer);
+        }
+    }
+
+    /**
+     * 创建dummy生产者
+     */
+    private DefaultMQProducer createDummyProducer() {
+        DefaultMQProducer dummyProducer = new DefaultMQProducer("DUMMY_PRODUCER_GROUP");
+        dummyProducer.setNamesrvAddr("127.0.0.1:9876");
+        // 不调用start()方法
+        return dummyProducer;
+    }
+
+    /**
+     * 创建dummy template
+     */
+    private RocketMQTemplate createDummyTemplate(DefaultMQProducer producer) {
+        RocketMQTemplate dummyTemplate = new RocketMQTemplate() {
+            @Override
+            public void afterPropertiesSet() {
+                // 什么都不做
+            }
+
+            @Override
+            public void convertAndSend(String destination, Object payload) {
+                log.warn("Dummy RocketMQTemplate被调用,实际不会发送消息: destination={}", destination);
+            }
+        };
+        dummyTemplate.setProducer(producer);
+        return dummyTemplate;
+    }
+
+    // 其他getter方法...
+    public Integer getSendMessageTimeout() {
+        ProjectConfig.Rocketmq config = getRocketmqConfigFromDB();
+        if (config == null || config.getProducer() == null) {
+            return 3000;
+        }
+        return config.getProducer().getSendMessageTimeout() != null ?
+                config.getProducer().getSendMessageTimeout() : 3000;
+    }
+
+    public Integer getRetryTimesWhenSendFailed() {
+        ProjectConfig.Rocketmq config = getRocketmqConfigFromDB();
+        if (config == null || config.getProducer() == null) {
+            return 2;
+        }
+        return config.getProducer().getRetryTimesWhenSendFailed() != null ?
+                config.getProducer().getRetryTimesWhenSendFailed() : 2;
+    }
+
+    /**
+     * 刷新配置
+     */
+    public void refresh() {
+        localCache.remove(ROCKETMQ_CONFIG_CACHE_KEY);
+        lastLoadTime = 0;
+
+        if (redisCache != null) {
+            try {
+                redisCache.deleteObject(PROJECT_CONFIG_CACHE_KEY);
+            } catch (Exception e) {
+                log.warn("清除Redis缓存失败", e);
+            }
+        }
+
+        log.info("RocketMQ配置已刷新");
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/config/saas/ProjectConfig.java

@@ -122,6 +122,16 @@ public class ProjectConfig {
             private String subMchId;
             private String keyPath;
             private String notifyUrl;
+
+            private String v3Key;
+
+            private String privateKeyPath;
+            private String privateCertPath;
+
+
+            private String certSerialNo;
+
+            private String nativeNotifyUrl;
         }
 
         @Data
@@ -135,6 +145,8 @@ public class ProjectConfig {
                 private String host;
                 private Integer port;
                 private Integer timeout;
+                private String password;
+
             }
 
             @Data

+ 25 - 20
fs-service/src/main/java/com/fs/config/tencent/TencentCOSClientConfig.java

@@ -1,40 +1,45 @@
 package com.fs.config.tencent;
 
-import com.alibaba.fastjson.JSONObject;
-import com.baidu.dev2.thirdparty.jackson.core.JsonProcessingException;
-import com.baidu.dev2.thirdparty.jackson.databind.ObjectMapper;
-import com.fs.config.saas.ProjectConfig;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
 import com.qcloud.cos.COSClient;
 import com.qcloud.cos.ClientConfig;
 import com.qcloud.cos.auth.BasicCOSCredentials;
 import com.qcloud.cos.auth.COSCredentials;
 import com.qcloud.cos.region.Region;
 import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
 @Component
 @AllArgsConstructor
+@Slf4j
 public class TencentCOSClientConfig {
-@Autowired
-private SysConfigMapper sysConfigMapper;
+
+    // 使用@Lazy打破循环依赖
+    @Lazy
+    private final TencentProperties tencentProperties;
+
     @Bean
-    private COSClient createClient() {
+    public COSClient createClient() {
+        log.info("创建COSClient...");
+        // 检查配置
+        if (!tencentProperties.isConfigValid()) {
+            log.error("腾讯云配置不完整,无法创建COS客户端");
+            throw new RuntimeException("腾讯云配置不完整,请检查数据库中的 projectConfig 配置");
+        }
+        String secretId = tencentProperties.getSecretId();
+        String secretKey = tencentProperties.getSecretKey();
+        String region = tencentProperties.getRegion();
 
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
+        if (secretId == null || secretKey == null || region == null) {
+            log.warn("腾讯云配置不完整,返回null");
+            return null;
+        }
+
+        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
+        ClientConfig clientConfig = new ClientConfig(new Region(region));
 
-        ProjectConfig.TencentCloudConfig tencentProperties = projectConfig.getTencentCloudConfig();
-        // 初始化用户身份信息(secretId, secretKey)
-        COSCredentials cred = new BasicCOSCredentials(tencentProperties.getSecretId(), tencentProperties.getSecretKey());
-        // 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
-        ClientConfig clientConfig = new ClientConfig(new Region(tencentProperties.getRegion()));
-        // 生成cos客户端
         return new COSClient(cred, clientConfig);
     }
-
 }

+ 220 - 0
fs-service/src/main/java/com/fs/config/tencent/TencentProperties.java

@@ -0,0 +1,220 @@
+package com.fs.config.tencent;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Data
+@Slf4j
+public class TencentProperties {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private RedisCache redisCache;
+
+    // Redis缓存Key
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    // 缓存时间:30分钟
+    private static final Integer CACHE_EXPIRE_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
+    /**
+     * 公用方法:查询数据库获取腾讯云配置
+     */
+    private ProjectConfig.TencentCloudConfig getTencentConfigFromDB() {
+        try {
+
+            // 1. 先从Redis缓存中获取
+            ProjectConfig cachedConfig = getConfigFromCache();
+            if (cachedConfig != null) {
+                log.debug("从Redis缓存中获取ProjectConfig配置");
+                return cachedConfig.getTencentCloudConfig();
+            }
+
+            String configKey = "projectConfig";
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+            if (sysConfig == null || sysConfig.getConfigValue() == null) {
+                log.warn("未找到腾讯云配置");
+                return null;
+            }
+
+            ProjectConfig projectConfig = JSONObject.parseObject(
+                    sysConfig.getConfigValue(),
+                    ProjectConfig.class
+            );
+
+            if (projectConfig == null) {
+                log.warn("配置解析失败");
+                return null;
+            }
+
+            ProjectConfig.TencentCloudConfig tencentConfig = projectConfig.getTencentCloudConfig();
+            if (tencentConfig == null) {
+                log.warn("配置中未找到tencentCloudConfig节点");
+                return null;
+            }
+            saveConfigToCache(projectConfig);
+            return tencentConfig;
+
+        } catch (Exception e) {
+            log.error("查询腾讯云配置失败", e);
+            return null;
+        }
+    }
+    /**
+     * 保存配置到Redis缓存
+     */
+    private void saveConfigToCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+            log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+        } catch (Exception e) {
+            log.warn("保存微信支付配置到Redis缓存失败", e);
+        }
+    }
+    /**
+     * 从Redis缓存获取ProjectConfig
+     */
+    private ProjectConfig getConfigFromCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (cachedJson != null && !cachedJson.isEmpty()) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取ProjectConfig失败", e);
+            return null;
+        }
+    }
+    /**
+     * 获取secretId(每次查询数据库)
+     */
+    public String getSecretId() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getSecretId() != null ? config.getSecretId() : "";
+    }
+
+    /**
+     * 获取secretKey(每次查询数据库)
+     */
+    public String getSecretKey() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getSecretKey() != null ? config.getSecretKey() : "";
+    }
+
+    /**
+     * 获取bucket(每次查询数据库)
+     */
+    public String getBucket() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getBucket() != null ? config.getBucket() : "";
+    }
+
+    /**
+     * 获取appId(每次查询数据库)
+     */
+    public String getAppId() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getAppId() != null ? config.getAppId() : "";
+    }
+
+    /**
+     * 获取region(每次查询数据库)
+     */
+    public String getRegion() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getRegion() != null ? config.getRegion() : "";
+    }
+
+    /**
+     * 获取proxy(每次查询数据库)
+     */
+    public String getProxy() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "";
+        }
+        return config.getProxy() != null ? config.getProxy() : "";
+    }
+
+    /**
+     * 检查配置是否完整(每次查询数据库)
+     */
+    public boolean isConfigValid() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return false;
+        }
+
+        return config.getSecretId() != null && !config.getSecretId().isEmpty() &&
+                config.getSecretKey() != null && !config.getSecretKey().isEmpty() &&
+                config.getRegion() != null && !config.getRegion().isEmpty();
+    }
+
+    /**
+     * 获取完整的 bucket 名称(如果需要拼接 appId)
+     */
+    public String getFullBucketName() {
+        String bucket = getBucket();
+        String appId = getAppId();
+
+        if (bucket == null || bucket.isEmpty() || appId == null || appId.isEmpty()) {
+            return bucket;
+        }
+        return bucket + "-" + appId;
+    }
+
+    /**
+     * 获取配置信息摘要(用于日志)
+     */
+    public String getConfigSummary() {
+        ProjectConfig.TencentCloudConfig config = getTencentConfigFromDB();
+        if (config == null) {
+            return "腾讯云配置为空";
+        }
+
+        return String.format("region: %s, bucket: %s, appId: %s",
+                config.getRegion(), config.getBucket(), config.getAppId());
+    }
+
+    /**
+     * 获取完整的腾讯云配置信息
+     */
+    public ProjectConfig.TencentCloudConfig getTencentCloudConfig() {
+        return getTencentConfigFromDB();
+    }
+
+    /**
+     * 刷新配置(实际是空方法,因为每次都是实时查询)
+     */
+    public void refresh() {
+        log.info("刷新腾讯云配置(每次都是实时查询,无需刷新缓存)");
+    }
+}

+ 3 - 13
fs-service/src/main/java/com/fs/core/config/WxMpConfiguration.java

@@ -1,16 +1,11 @@
 package com.fs.core.config;
 
-import com.alibaba.fastjson.JSONObject;
-import com.fs.config.saas.ProjectConfig;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
 import com.fs.wx.mp.handler.*;
 import lombok.AllArgsConstructor;
 import me.chanjar.weixin.mp.api.WxMpMessageRouter;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -41,18 +36,13 @@ public class WxMpConfiguration {
     private final UnsubscribeHandler unsubscribeHandler;
     private final SubscribeHandler subscribeHandler;
     private final ScanHandler scanHandler;
-    @Autowired
-    private SysConfigMapper sysConfigMapper;
+    private final WxMpProperties properties;
+
     @Bean
     @Scope("prototype") // 每次获取新的 WxMpService 实例,避免单例缓存导致多租户问题
     public WxMpService wxMpService() {
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        List<ProjectConfig.Wx.Mp.Config> configs= projectConfig.getWx().getMp().getConfigs();
         // 从 WxMpProperties 获取当前请求租户配置
-//        final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
+        final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
 
         if (configs == null || configs.isEmpty()) {
             throw new RuntimeException("当前租户未配置微信公众号!请检查 TenantConfigContext 或 yml 配置");

+ 401 - 0
fs-service/src/main/java/com/fs/core/config/WxMpProperties.java

@@ -0,0 +1,401 @@
+package com.fs.core.config;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Component
+@Data
+@Slf4j
+public class WxMpProperties {
+
+    /**
+     * 当前请求租户的配置副本,ThreadLocal
+     */
+    private static final ThreadLocal<List<MpConfig>> TENANT_CONFIGS = new ThreadLocal<>();
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private RedisCache redisCache;
+
+    // Redis缓存Key
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    // 缓存时间:30分钟
+    private static final Integer CACHE_EXPIRE_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
+    /**
+     * 公用方法:查询数据库获取公众号配置
+     */
+    private ProjectConfig.Wx.Mp getWxMpConfigFromDB() {
+        try {
+            ProjectConfig cachedConfig = getConfigFromCache();
+            if (cachedConfig != null) {
+                log.debug("从Redis缓存中获取ProjectConfig配置");
+                return cachedConfig.getWx().getMp();
+            }
+            String configKey = "projectConfig";
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+            if (sysConfig == null || sysConfig.getConfigValue() == null) {
+                log.warn("未找到公众号配置");
+                return null;
+            }
+
+            ProjectConfig projectConfig = JSONObject.parseObject(
+                    sysConfig.getConfigValue(),
+                    ProjectConfig.class
+            );
+
+            if (projectConfig == null || projectConfig.getWx() == null) {
+                log.warn("配置中未找到wx节点");
+                return null;
+            }
+
+            ProjectConfig.Wx.Mp mpConfig = projectConfig.getWx().getMp();
+            if (mpConfig == null) {
+                log.warn("配置中未找到mp节点");
+                return null;
+            }
+            saveConfigToCache(projectConfig);
+            return mpConfig;
+
+        } catch (Exception e) {
+            log.error("查询公众号配置失败", e);
+            return null;
+        }
+    }
+    /**
+     * 保存配置到Redis缓存
+     */
+    private void saveConfigToCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+            log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+        } catch (Exception e) {
+            log.warn("保存微信支付配置到Redis缓存失败", e);
+        }
+    }
+    /**
+     * 从Redis缓存获取ProjectConfig
+     */
+    private ProjectConfig getConfigFromCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (cachedJson != null && !cachedJson.isEmpty()) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取ProjectConfig失败", e);
+            return null;
+        }
+    }
+    /**
+     * 是否使用redis存储access token(每次查询数据库)
+     */
+    public boolean isUseRedis() {
+        ProjectConfig.Wx.Mp mpConfig = getWxMpConfigFromDB();
+        if (mpConfig == null) {
+            return false;
+        }
+        return Boolean.TRUE.equals(mpConfig.getUseRedis());
+    }
+
+    /**
+     * 获取redis配置(每次查询数据库)
+     */
+    public RedisConfig getRedisConfig() {
+        ProjectConfig.Wx.Mp mpConfig = getWxMpConfigFromDB();
+        if (mpConfig == null || !Boolean.TRUE.equals(mpConfig.getUseRedis()) ||
+                mpConfig.getRedisConfig() == null) {
+            return null;
+        }
+
+        ProjectConfig.Wx.Mp.RedisConfig dbRedisConfig = mpConfig.getRedisConfig();
+        RedisConfig redisConfig = new RedisConfig();
+        redisConfig.setHost(dbRedisConfig.getHost());
+        redisConfig.setPort(dbRedisConfig.getPort());
+        redisConfig.setPassword(dbRedisConfig.getPassword());
+        redisConfig.setTimeout(dbRedisConfig.getTimeout());
+
+        return redisConfig;
+    }
+
+    /**
+     * 获取所有公众号配置(每次查询数据库)
+     */
+    public List<MpConfig> getConfigs() {
+        List<MpConfig> tenantCfgs = TENANT_CONFIGS.get();
+        if (tenantCfgs != null) {
+            return tenantCfgs;
+        }
+
+        ProjectConfig.Wx.Mp mpConfig = getWxMpConfigFromDB();
+        if (mpConfig == null || mpConfig.getConfigs() == null) {
+            return new ArrayList<>();
+        }
+
+        List<ProjectConfig.Wx.Mp.Config> dbConfigs = mpConfig.getConfigs();
+        if (dbConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return dbConfigs.stream().map(dbConfig -> {
+            MpConfig mpCfg = new MpConfig();
+            mpCfg.setAppId(dbConfig.getAppId());
+            mpCfg.setSecret(dbConfig.getSecret());
+            mpCfg.setToken(dbConfig.getToken());
+            mpCfg.setAesKey(dbConfig.getAesKey());
+            return mpCfg;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 设置当前请求租户配置
+     */
+    public static void setTenantConfigs(List<MpConfig> tenantConfigs) {
+        TENANT_CONFIGS.set(tenantConfigs);
+    }
+
+    /**
+     * 清理 ThreadLocal
+     */
+    public static void clearTenantConfigs() {
+        TENANT_CONFIGS.remove();
+    }
+
+    /**
+     * 获取指定appId的配置(每次查询数据库)
+     */
+    public MpConfig getConfig(String appId) {
+        List<MpConfig> allConfigs = getConfigs();
+        if (allConfigs.isEmpty()) {
+            return null;
+        }
+
+        return allConfigs.stream()
+                .filter(cfg -> appId.equals(cfg.getAppId()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * 获取第一个配置(适用于单个公众号)
+     */
+    public MpConfig getFirstConfig() {
+        List<MpConfig> allConfigs = getConfigs();
+        if (allConfigs.isEmpty()) {
+            throw new RuntimeException("未找到公众号配置");
+        }
+        return allConfigs.get(0);
+    }
+
+    /**
+     * 检查是否包含指定appId的配置
+     */
+    public boolean hasConfig(String appId) {
+        return getConfig(appId) != null;
+    }
+
+    /**
+     * 获取所有appId列表
+     */
+    public List<String> getAllAppIds() {
+        List<MpConfig> allConfigs = getConfigs();
+        if (allConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return allConfigs.stream()
+                .map(MpConfig::getAppId)
+                .filter(appId -> appId != null && !appId.isEmpty())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 检查是否有可用的配置
+     */
+    public boolean hasAvailableConfigs() {
+        List<MpConfig> allConfigs = getConfigs();
+        return !allConfigs.isEmpty();
+    }
+
+    /**
+     * 检查配置是否完整
+     */
+    public boolean isConfigValid() {
+        List<MpConfig> allConfigs = getConfigs();
+        return !allConfigs.isEmpty();
+    }
+
+    /**
+     * 获取配置摘要
+     */
+    public String getConfigSummary() {
+        List<MpConfig> allConfigs = getConfigs();
+        if (allConfigs.isEmpty()) {
+            return "公众号配置为空";
+        }
+
+        List<String> appIds = allConfigs.stream()
+                .map(MpConfig::getAppId)
+                .collect(Collectors.toList());
+
+        return String.format("共 %d 个公众号: %s",
+                allConfigs.size(), String.join(", ", appIds));
+    }
+
+    /**
+     * 获取有效的配置(检查配置完整性)
+     */
+    public List<MpConfig> getValidConfigs() {
+        List<MpConfig> allConfigs = getConfigs();
+        if (allConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return allConfigs.stream()
+                .filter(MpConfig::isValid)
+                .collect(Collectors.toList());
+    }
+
+    @Data
+    public static class RedisConfig {
+        private String host;
+        private Integer port;
+        private String password;
+        private Integer timeout;
+    }
+
+    @Data
+    public static class MpConfig {
+        private String appId;
+        private String secret;
+        private String token;
+        private String aesKey;
+
+        /**
+         * 检查配置是否完整
+         */
+        public boolean isValid() {
+            return appId != null && !appId.isEmpty() &&
+                    secret != null && !secret.isEmpty();
+        }
+
+        /**
+         * 获取配置摘要
+         */
+        public String getSummary() {
+            return String.format("appId: %s", appId);
+        }
+    }
+
+    /**
+     * 从 TenantConfigContext 生成当前请求租户 configs 副本
+     */
+    public static void loadTenantConfigsFromContext() {
+        JSONObject tenantCfg = TenantConfigContext.get();
+        List<MpConfig> mpConfigs = new ArrayList<>();
+
+        if (tenantCfg != null) {
+            JSONObject wx = tenantCfg.getJSONObject("wx");
+            if (wx != null) {
+                JSONObject mp = wx.getJSONObject("mp");
+                if (mp != null) {
+                    JSONArray arr = mp.getJSONArray("configs");
+                    if (arr != null) {
+                        for (int i = 0; i < arr.size(); i++) {
+                            JSONObject cfgJson = arr.getJSONObject(i);
+                            MpConfig cfg = new MpConfig();
+                            cfg.setAppId(cfgJson.getString("appId"));
+                            cfg.setSecret(cfgJson.getString("secret"));
+                            cfg.setToken(cfgJson.getString("token"));
+                            cfg.setAesKey(cfgJson.getString("aesKey"));
+                            mpConfigs.add(cfg);
+                        }
+                    }
+                }
+            }
+        }
+
+        setTenantConfigs(mpConfigs);
+    }
+
+    /**
+     * 创建微信公众号服务
+     */
+    public WxMpService createWxMpService(String appId) {
+        MpConfig config = getConfig(appId);
+        if (config == null) {
+            throw new RuntimeException("未找到appId为" + appId + "的公众号配置");
+        }
+
+        return createWxMpService(config);
+    }
+
+    /**
+     * 使用第一个配置创建微信公众号服务
+     */
+    public WxMpService createFirstWxMpService() {
+        MpConfig config = getFirstConfig();
+        return createWxMpService(config);
+    }
+
+    /**
+     * 使用指定配置创建微信公众号服务
+     */
+    private WxMpService createWxMpService(MpConfig config) {
+        if (!config.isValid()) {
+            throw new RuntimeException("公众号配置不完整: " + config.getSummary());
+        }
+
+        WxMpDefaultConfigImpl wxConfig = new WxMpDefaultConfigImpl();
+        wxConfig.setAppId(config.getAppId());
+        wxConfig.setSecret(config.getSecret());
+        wxConfig.setToken(config.getToken());
+        wxConfig.setAesKey(config.getAesKey());
+
+        // 如果配置了redis
+        boolean useRedis = isUseRedis();
+        RedisConfig redisConfig = getRedisConfig();
+        if (useRedis && redisConfig != null) {
+            // 设置redis存储(需要根据具体实现调整)
+            // wxConfig.setRedisTemplate(redisTemplate);
+        }
+
+        WxMpService wxMpService = new WxMpServiceImpl();
+        wxMpService.setWxMpConfigStorage(wxConfig);
+        return wxMpService;
+    }
+
+    /**
+     * 刷新配置(实际是空方法,因为每次都是实时查询)
+     */
+    public void refresh() {
+        log.info("刷新公众号配置(每次都是实时查询,无需刷新缓存)");
+    }
+
+    /**
+     * 获取完整的配置对象
+     */
+    public ProjectConfig.Wx.Mp getWxMpConfig() {
+        return getWxMpConfigFromDB();
+    }
+}

+ 143 - 18
fs-service/src/main/java/com/fs/core/config/WxPayConfiguration.java

@@ -4,39 +4,164 @@ import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
 
+@Slf4j
 @Configuration
 @ConditionalOnClass(WxPayService.class)
-@EnableConfigurationProperties(WxPayProperties.class)
 @AllArgsConstructor
 public class WxPayConfiguration {
   private WxPayProperties properties;
 
   @Bean
   @ConditionalOnMissingBean
+  @Scope("prototype")  // 每次获取新的实例,支持动态配置
   public WxPayService wxService() {
-    WxPayConfig payConfig = new WxPayConfig();
-    payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
-    payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
-    payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
-    payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
-    payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
-    payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
-//    payConfig.setApiV3Key("Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC");
-//    payConfig.setPrivateKeyPath("C:\\cert\\apiclient_key.pem");
-//    payConfig.setPrivateCertPath("C:\\cert\\apiclient_cert.pem");
-    // 可以指定是否使用沙箱环境
-    payConfig.setUseSandboxEnv(false);
-
-    WxPayService wxPayService = new WxPayServiceImpl();
-    wxPayService.setConfig(payConfig);
-    return wxPayService;
+    try {
+      // 每次创建服务时都获取最新的配置
+      log.info("创建微信支付服务,检查配置...");
+
+      WxPayConfig payConfig = new WxPayConfig();
+
+      // 获取实时配置
+      String appId = StringUtils.trimToNull(this.properties.getAppId());
+      String mchId = StringUtils.trimToNull(this.properties.getMchId());
+      String mchKey = StringUtils.trimToNull(this.properties.getMchKey());
+      String subAppId = StringUtils.trimToNull(this.properties.getSubAppId());
+      String subMchId = StringUtils.trimToNull(this.properties.getSubMchId());
+      String keyPath = StringUtils.trimToNull(this.properties.getKeyPath());
+      String v3Key = StringUtils.trimToNull(this.properties.getV3Key());
+      String privateKeyPath = StringUtils.trimToNull(this.properties.getPrivateKeyPath());
+      String privateCertPath = StringUtils.trimToNull(this.properties.getPrivateCertPath());
+      String certSerialNo = StringUtils.trimToNull(this.properties.getCertSerialNo());
+
+      // 设置基本配置
+      payConfig.setAppId(appId);
+      payConfig.setMchId(mchId);
+      payConfig.setMchKey(mchKey);
+      payConfig.setSubAppId(subAppId);
+      payConfig.setSubMchId(subMchId);
+      payConfig.setKeyPath(keyPath);
+
+      // 设置V3配置
+      if (StringUtils.isNotBlank(v3Key)) {
+        payConfig.setApiV3Key(v3Key);
+      }
+      if (StringUtils.isNotBlank(privateKeyPath)) {
+        payConfig.setPrivateKeyPath(privateKeyPath);
+      }
+      if (StringUtils.isNotBlank(privateCertPath)) {
+        payConfig.setPrivateCertPath(privateCertPath);
+      }
+      if (StringUtils.isNotBlank(certSerialNo)) {
+        payConfig.setCertSerialNo(certSerialNo);
+      }
+
+      // 检查配置是否完整
+      if (StringUtils.isBlank(appId) || StringUtils.isBlank(mchId)) {
+        log.error("微信支付配置不完整,appId: {}, mchId: {}", appId, mchId);
+        throw new RuntimeException("微信支付配置不完整,请检查数据库配置");
+      }
+
+      // 设置其他配置
+      payConfig.setUseSandboxEnv(false);
+
+      WxPayService wxPayService = new WxPayServiceImpl();
+      wxPayService.setConfig(payConfig);
+
+      log.info("微信支付服务创建成功,appId: {}, mchId: {}", appId, mchId);
+      return wxPayService;
+
+    } catch (Exception e) {
+      log.error("创建微信支付服务失败", e);
+      throw new RuntimeException("创建微信支付服务失败: " + e.getMessage(), e);
+    }
+  }
+
+  /**
+   * 根据指定的appId创建支付服务(支持多小程序)
+   */
+  @Bean
+  @Scope("prototype")
+  public WxPayService wxService(String appId) {
+    try {
+      log.info("为appId: {} 创建微信支付服务", appId);
+
+      // 验证当前配置是否包含指定的appId
+      String currentAppId = StringUtils.trimToNull(this.properties.getAppId());
+      if (!StringUtils.equals(currentAppId, appId)) {
+        log.warn("当前配置的appId({})与请求的appId({})不匹配,使用当前配置", currentAppId, appId);
+      }
+
+      // 创建支付服务
+      WxPayService wxPayService = wxService();
+
+      log.info("为appId: {} 创建微信支付服务成功", appId);
+      return wxPayService;
+
+    } catch (Exception e) {
+      log.error("为appId: {} 创建微信支付服务失败", appId, e);
+      throw new RuntimeException("创建微信支付服务失败: " + e.getMessage(), e);
+    }
   }
 
+  /**
+   * 创建V3支付服务(如果配置了V3)
+   */
+  @Bean
+  @ConditionalOnMissingBean
+  @Scope("prototype")
+  public WxPayService wxPayV3Service() {
+    try {
+      // 检查V3配置是否完整
+      if (!this.properties.isV3ConfigValid()) {
+        log.warn("微信支付V3配置不完整,创建普通支付服务");
+        return wxService();
+      }
+
+      log.info("创建微信支付V3服务");
+
+      WxPayConfig payConfig = new WxPayConfig();
+
+      // 获取实时配置
+      String appId = StringUtils.trimToNull(this.properties.getAppId());
+      String mchId = StringUtils.trimToNull(this.properties.getMchId());
+      String v3Key = StringUtils.trimToNull(this.properties.getV3Key());
+      String privateKeyPath = StringUtils.trimToNull(this.properties.getPrivateKeyPath());
+      String privateCertPath = StringUtils.trimToNull(this.properties.getPrivateCertPath());
+      String certSerialNo = StringUtils.trimToNull(this.properties.getCertSerialNo());
+
+      // 设置V3配置
+      payConfig.setAppId(appId);
+      payConfig.setMchId(mchId);
+      payConfig.setApiV3Key(v3Key);
+      payConfig.setPrivateKeyPath(privateKeyPath);
+      payConfig.setPrivateCertPath(privateCertPath);
+      payConfig.setCertSerialNo(certSerialNo);
+
+      // 也可以同时设置V2密钥(如果需要)
+      String mchKey = StringUtils.trimToNull(this.properties.getMchKey());
+      if (StringUtils.isNotBlank(mchKey)) {
+        payConfig.setMchKey(mchKey);
+      }
+
+      payConfig.setUseSandboxEnv(false);
+
+      WxPayService wxPayService = new WxPayServiceImpl();
+      wxPayService.setConfig(payConfig);
+
+      log.info("微信支付V3服务创建成功,appId: {}, mchId: {}", appId, mchId);
+      return wxPayService;
+
+    } catch (Exception e) {
+      log.error("创建微信支付V3服务失败", e);
+      throw new RuntimeException("创建微信支付V3服务失败: " + e.getMessage(), e);
+    }
+  }
 }

+ 330 - 46
fs-service/src/main/java/com/fs/core/config/WxPayProperties.java

@@ -1,108 +1,392 @@
 package com.fs.core.config;
 
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
 import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
 
-@ConfigurationProperties(prefix = "wx.pay")
+import java.util.concurrent.TimeUnit;
+
+@Component
 @Data
+@Slf4j
 public class WxPayProperties {
-  /**
-   * 设置微信公众号或者小程序等的appid
-   */
-  private String appId;
-
-  /**
-   * 微信支付商户号
-   */
-  private String mchId;
 
-  /**
-   * 微信支付商户密钥
-   */
-  private String mchKey;
+  @Autowired
+  private SysConfigMapper sysConfigMapper;
+  @Autowired
+  private RedisCache redisCache;
 
-  private String v3Key;
+  // Redis缓存Key
+  private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+  // 缓存时间:30分钟
+  private static final Integer CACHE_EXPIRE_TIME = 30;
+  private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
 
   /**
-   * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
+   * 公用方法:查询数据库获取微信支付配置
    */
-  private String subAppId;
+  private ProjectConfig.Wx.Pay getWxPayConfigFromDB() {
+    try {
+      // 1. 先从Redis缓存中获取
+      ProjectConfig cachedConfig = getConfigFromCache();
+      if (cachedConfig != null) {
+        log.debug("从Redis缓存中获取ProjectConfig配置");
+        return cachedConfig.getWx().getPay();
+      }
 
-  /**
-   * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
-   */
-  private String subMchId;
+      String configKey = "projectConfig";
+      SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
 
-  /**
-   * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
-   */
-  private String keyPath;
+      if (sysConfig == null || sysConfig.getConfigValue() == null) {
+        log.warn("未找到微信支付配置");
+        return null;
+      }
 
-  private String notifyUrl;
+      ProjectConfig projectConfig = JSONObject.parseObject(
+              sysConfig.getConfigValue(),
+              ProjectConfig.class
+      );
 
-  private String privateKeyPath;
+      if (projectConfig == null || projectConfig.getWx() == null) {
+        log.warn("配置中未找到wx节点");
+        return null;
+      }
 
-  private String privateCertPath;
+      ProjectConfig.Wx.Pay payConfig = projectConfig.getWx().getPay();
+      if (payConfig == null) {
+        log.warn("配置中未找到pay节点");
+        return null;
+      }
 
-  private String certSerialNo;
+      log.debug("查询到微信支付配置: appId={}, mchId={}",
+              payConfig.getAppId(), payConfig.getMchId());
 
-  private String nativeNotifyUrl;
+      saveConfigToCache(projectConfig);
 
-  /* ===================== 动态 getter ===================== */
+      return payConfig;
 
+    } catch (Exception e) {
+      log.error("查询微信支付配置失败", e);
+      return null;
+    }
+  }
+  /**
+   * 保存配置到Redis缓存
+   */
+  private void saveConfigToCache(ProjectConfig config) {
+    try {
+      String configJson = JSONObject.toJSONString(config);
+      redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+      log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+    } catch (Exception e) {
+      log.warn("保存微信支付配置到Redis缓存失败", e);
+    }
+  }
+  /**
+   * 从Redis缓存获取ProjectConfig
+   */
+  private ProjectConfig getConfigFromCache() {
+    try {
+      String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+      if (cachedJson != null && !cachedJson.isEmpty()) {
+        return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+      }
+      return null;
+    } catch (Exception e) {
+      log.warn("从Redis缓存获取ProjectConfig失败", e);
+      return null;
+    }
+  }
+
+  /**
+   * 获取appId(每次查询数据库)
+   */
   public String getAppId() {
-    return tenantOrDefault("wx.pay.appId", appId);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.appId", "");
+    }
+    String value = payConfig.getAppId() != null ? payConfig.getAppId() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.appId", "");
   }
 
+  /**
+   * 获取商户号(每次查询数据库)
+   */
   public String getMchId() {
-    return tenantOrDefault("wx.pay.mchId", mchId);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.mchId", "");
+    }
+    String value = payConfig.getMchId() != null ? payConfig.getMchId() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.mchId", "");
   }
 
+  /**
+   * 获取商户密钥(每次查询数据库)
+   */
   public String getMchKey() {
-    return tenantOrDefault("wx.pay.mchKey", mchKey);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.mchKey", "");
+    }
+    String value = payConfig.getMchKey() != null ? payConfig.getMchKey() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.mchKey", "");
   }
 
+  /**
+   * 获取V3密钥(每次查询数据库)
+   */
   public String getV3Key() {
-    return tenantOrDefault("wx.pay.v3Key", v3Key);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.v3Key", "");
+    }
+    String value = payConfig.getV3Key() != null ? payConfig.getV3Key() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.v3Key", "");
   }
 
+  /**
+   * 获取子商户appId(每次查询数据库)
+   */
   public String getSubAppId() {
-    return tenantOrDefault("wx.pay.subAppId", subAppId);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.subAppId", "");
+    }
+    String value = payConfig.getSubAppId() != null ? payConfig.getSubAppId() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.subAppId", "");
   }
 
+  /**
+   * 获取子商户号(每次查询数据库)
+   */
   public String getSubMchId() {
-    return tenantOrDefault("wx.pay.subMchId", subMchId);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.subMchId", "");
+    }
+    String value = payConfig.getSubMchId() != null ? payConfig.getSubMchId() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.subMchId", "");
   }
 
+  /**
+   * 获取密钥文件路径(每次查询数据库)
+   */
   public String getKeyPath() {
-    return tenantOrDefault("wx.pay.keyPath", keyPath);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.keyPath", "");
+    }
+    String value = payConfig.getKeyPath() != null ? payConfig.getKeyPath() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.keyPath", "");
   }
 
+  /**
+   * 获取回调URL(每次查询数据库)
+   */
   public String getNotifyUrl() {
-    return tenantOrDefault("wx.pay.notifyUrl", notifyUrl);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.notifyUrl", "");
+    }
+    String value = payConfig.getNotifyUrl() != null ? payConfig.getNotifyUrl() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.notifyUrl", "");
   }
 
+  /**
+   * 获取私钥路径(每次查询数据库)
+   */
   public String getPrivateKeyPath() {
-    return tenantOrDefault("wx.pay.privateKeyPath", privateKeyPath);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.privateKeyPath", "");
+    }
+    String value = payConfig.getPrivateKeyPath() != null ? payConfig.getPrivateKeyPath() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.privateKeyPath", "");
   }
 
+  /**
+   * 获取私钥证书路径(每次查询数据库)
+   */
   public String getPrivateCertPath() {
-    return tenantOrDefault("wx.pay.privateCertPath", privateCertPath);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.privateCertPath", "");
+    }
+    String value = payConfig.getPrivateCertPath() != null ? payConfig.getPrivateCertPath() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.privateCertPath", "");
   }
 
+  /**
+   * 获取证书序列号(每次查询数据库)
+   */
   public String getCertSerialNo() {
-    return tenantOrDefault("wx.pay.certSerialNo", certSerialNo);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.certSerialNo", "");
+    }
+    String value = payConfig.getCertSerialNo() != null ? payConfig.getCertSerialNo() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.certSerialNo", "");
   }
 
+  /**
+   * 获取Native支付回调URL(每次查询数据库)
+   */
   public String getNativeNotifyUrl() {
-    return tenantOrDefault("wx.pay.nativeNotifyUrl", nativeNotifyUrl);
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return tenantOrDefault("wx.pay.nativeNotifyUrl", "");
+    }
+    String value = payConfig.getNativeNotifyUrl() != null ? payConfig.getNativeNotifyUrl() : "";
+    return StringUtils.isNotBlank(value) ? value : tenantOrDefault("wx.pay.nativeNotifyUrl", "");
+  }
+
+  /**
+   * 检查配置是否完整(每次查询数据库)
+   */
+  public boolean isConfigValid() {
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return false;
+    }
+
+    String appId = payConfig.getAppId();
+    String mchId = payConfig.getMchId();
+    String mchKey = payConfig.getMchKey();
+
+    boolean basicValid = StringUtils.isNotBlank(appId) &&
+            StringUtils.isNotBlank(mchId) &&
+            StringUtils.isNotBlank(mchKey);
+
+    log.debug("微信支付配置有效性检查: {}", basicValid);
+    return basicValid;
+  }
+
+  /**
+   * 检查V3配置是否完整(每次查询数据库)
+   */
+  public boolean isV3ConfigValid() {
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return false;
+    }
+
+    String appId = payConfig.getAppId();
+    String mchId = payConfig.getMchId();
+    String v3Key = payConfig.getV3Key();
+    String certSerialNo = payConfig.getCertSerialNo();
+    String privateKeyPath = payConfig.getPrivateKeyPath();
+
+    boolean v3Valid = StringUtils.isNotBlank(appId) &&
+            StringUtils.isNotBlank(mchId) &&
+            StringUtils.isNotBlank(v3Key) &&
+            StringUtils.isNotBlank(certSerialNo) &&
+            StringUtils.isNotBlank(privateKeyPath);
+
+    log.debug("微信支付V3配置有效性检查: {}", v3Valid);
+    return v3Valid;
+  }
+
+  /**
+   * 获取配置摘要(每次查询数据库)
+   */
+  public String getConfigSummary() {
+    ProjectConfig.Wx.Pay payConfig = getWxPayConfigFromDB();
+    if (payConfig == null) {
+      return "微信支付配置为空";
+    }
+
+    return String.format("appId: %s, mchId: %s",
+            payConfig.getAppId(), payConfig.getMchId());
+  }
+
+  /**
+   * 获取完整的配置对象(每次查询数据库)
+   */
+  public ProjectConfig.Wx.Pay getWxPayConfig() {
+    return getWxPayConfigFromDB();
+  }
+
+  /**
+   * 刷新配置(实际是空方法,因为每次都是实时查询)
+   */
+  public void refresh() {
+    log.info("刷新微信支付配置(每次都是实时查询,无需刷新缓存)");
+  }
+
+  /**
+   * 安全获取方法 - 返回非null值
+   */
+  public String getSafeAppId() {
+    String appId = getAppId();
+    return StringUtils.isNotBlank(appId) ? appId : "";
+  }
+
+  public String getSafeMchId() {
+    String mchId = getMchId();
+    return StringUtils.isNotBlank(mchId) ? mchId : "";
+  }
+
+  public String getSafeMchKey() {
+    String mchKey = getMchKey();
+    return StringUtils.isNotBlank(mchKey) ? mchKey : "";
+  }
+
+  public String getSafeV3Key() {
+    String v3Key = getV3Key();
+    return StringUtils.isNotBlank(v3Key) ? v3Key : "";
+  }
+
+  public String getSafeNotifyUrl() {
+    String notifyUrl = getNotifyUrl();
+    return StringUtils.isNotBlank(notifyUrl) ? notifyUrl : "";
+  }
+
+  public String getSafeNativeNotifyUrl() {
+    String nativeNotifyUrl = getNativeNotifyUrl();
+    return StringUtils.isNotBlank(nativeNotifyUrl) ? nativeNotifyUrl : "";
+  }
+
+  /**
+   * 获取完整的回调URL(拼接基础URL)
+   */
+  public String getFullNotifyUrl(String baseUrl) {
+    String notifyPath = getSafeNotifyUrl();
+    if (StringUtils.isBlank(notifyPath)) {
+      return "";
+    }
+
+    if (notifyPath.startsWith("http")) {
+      return notifyPath;
+    }
+
+    if (StringUtils.isBlank(baseUrl)) {
+      return notifyPath;
+    }
+
+    String base = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
+    String path = notifyPath.startsWith("/") ? notifyPath : "/" + notifyPath;
+
+    return base + path;
   }
 
   /* ===================== 公共兜底方法 ===================== */
 
   private String tenantOrDefault(String jsonPath, String defaultValue) {
-    String tenantValue = TenantConfigContext.getString(jsonPath);
-    return StringUtils.isNotBlank(tenantValue) ? tenantValue : defaultValue;
+    try {
+      String tenantValue = TenantConfigContext.getString(jsonPath);
+      return StringUtils.isNotBlank(tenantValue) ? tenantValue : defaultValue;
+    } catch (Exception e) {
+      log.warn("获取租户配置失败: {}", jsonPath, e);
+      return defaultValue;
+    }
   }
 }

+ 4 - 30
fs-service/src/main/java/com/fs/course/service/impl/TencentCloudCosService.java

@@ -1,16 +1,12 @@
 package com.fs.course.service.impl;
 
-import com.baidu.dev2.thirdparty.jackson.core.JsonProcessingException;
-import com.baidu.dev2.thirdparty.jackson.databind.ObjectMapper;
 import com.fs.common.core.domain.R;
-import com.fs.config.saas.ProjectConfig;
+import com.fs.config.tencent.TencentProperties;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.domain.FsVideoResource;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.mapper.FsVideoResourceMapper;
 import com.fs.course.service.ITencentCloudCosService;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
 import com.qcloud.cos.COSClient;
 import com.qcloud.cos.exception.CosClientException;
 import com.qcloud.cos.exception.CosServiceException;
@@ -42,8 +38,8 @@ import java.util.*;
 @AllArgsConstructor
 public class TencentCloudCosService implements ITencentCloudCosService {
     private final COSClient cosClient;
-    @Autowired
-    private SysConfigMapper sysConfigMapper;
+    private final TencentProperties tencentProperties;
+
 
     @Override
     public R getKeyAndCredentials() {
@@ -74,27 +70,8 @@ public class TencentCloudCosService implements ITencentCloudCosService {
         return cosKey;
     }
 
-    /**
-     * 查询配置文件
-     *
-     * @return
-     */
-    private ProjectConfig.TencentCloudConfig getTencentCloudConfig(){
-        ObjectMapper objectMapper = new ObjectMapper();
-
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-
-        ProjectConfig projectConfig = com.alibaba.fastjson.JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.TencentCloudConfig tencentProperties = projectConfig.getTencentCloudConfig();
-        return tencentProperties;
-    }
-
     public TreeMap<String,Object> getConfig(){
-
-        ProjectConfig.TencentCloudConfig tencentProperties =getTencentCloudConfig();
-
+        tencentProperties.refresh();
         String secretId = tencentProperties.getSecretId();//用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
         String secretKey = tencentProperties.getSecretKey(); ;//用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140
 
@@ -244,7 +221,6 @@ public class TencentCloudCosService implements ITencentCloudCosService {
     }
 
     private byte[] getObjectInputStream() throws IOException {
-        ProjectConfig.TencentCloudConfig tencentProperties =getTencentCloudConfig();
         String key = "test/my_test.json";
         GetObjectRequest getObjectRequest = new GetObjectRequest(tencentProperties.getBucket(), key);
         InputStream cosObjectInput = null;
@@ -300,7 +276,6 @@ public class TencentCloudCosService implements ITencentCloudCosService {
     }
 
     private MpsClient createMpsClient() {
-        ProjectConfig.TencentCloudConfig tencentProperties =getTencentCloudConfig();
         Credential cred = new Credential(tencentProperties.getSecretId(), tencentProperties.getSecretKey());
         HttpProfile httpProfile = new HttpProfile();
         httpProfile.setEndpoint("mps.tencentcloudapi.com");
@@ -316,7 +291,6 @@ public class TencentCloudCosService implements ITencentCloudCosService {
     }
 
     public void submitTranscodeJob(String inputPath,String outputPath) {
-        ProjectConfig.TencentCloudConfig tencentProperties =getTencentCloudConfig();
         try {
             // 输入文件配置(COS路径)
             MpsClient client = createMpsClient();

+ 8 - 38
fs-service/src/main/java/com/fs/event/WeixinTemplateService.java

@@ -4,14 +4,11 @@ package com.fs.event;
 import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
 import cn.hutool.core.util.StrUtil;
-import com.alibaba.fastjson.JSONObject;
-import com.fs.config.saas.ProjectConfig;
 import com.fs.core.config.WxMaConfiguration;
+import com.fs.his.config.WxMiniappTempConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.ConfigUtil;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -33,18 +30,12 @@ public class WeixinTemplateService {
     private ConfigUtil configUtil;
     @Autowired
     private IFsUserService userService;
-    @Autowired
-    private SysConfigMapper sysConfigMapper;
 
+    @Autowired
+    WxMiniappTempConfig wxMiniappTempConfig;
 
 
     public void cancelOrderNotice(String orderId,String title,String remark,Long uid){
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
-
         String openId = this.getUserOpenid(uid);
         if(StrUtil.isBlank(openId)) {
             return;
@@ -53,27 +44,21 @@ public class WeixinTemplateService {
         //订单号
         map.put("thing1",title);
         map.put("thing4",remark);
-        String tempId = wxMiniappTempConfig.getInquiryTempId();
+        String tempId = wxMiniappTempConfig.getInquiryTempID();
         if(StrUtil.isNotBlank(tempId)) {
             this.sendWxMpTemplateMessage(openId, tempId, "pages_order/inquiryOrderDetails?orderId=" + orderId, map);
         }
     }
     public void receiveOrderNotice(String orderId, String title, String remark, Long uid){
         String openId = this.getUserOpenid(uid);
-
         if(StrUtil.isBlank(openId)) {
             return;
         }
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
         Map<String,String> map = new HashMap<>();
         //订单号
         map.put("thing1",title);
         map.put("thing4",remark);
-        String tempId = wxMiniappTempConfig.getInquiryTempId();;
+        String tempId = wxMiniappTempConfig.getInquiryTempID();;
         if(StrUtil.isNotBlank(tempId)) {
             this.sendWxMpTemplateMessage(openId, tempId, "pages_order/inquiryOrderDetails?orderId=" + orderId, map);
         }
@@ -84,18 +69,13 @@ public class WeixinTemplateService {
         if(StrUtil.isBlank(openId)) {
             return;
         }
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
         Map<String,String> map = new HashMap<>();
         //订单号 2020年5月10日 14:35:40 2020年5月10日14:35:40
         SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
         map.put("time7",df.format(createTime));
         map.put("character_string9",orderCode);
         map.put("thing4",remark);
-        String tempId = wxMiniappTempConfig.getPayOrderTempId();
+        String tempId = wxMiniappTempConfig.getPayOrderTempID();
         if(StrUtil.isNotBlank(tempId)) {
             this.sendWxMpTemplateMessage(openId, tempId, "pages_order/packageOrderDetails?orderId=" + orderId, map);
         }
@@ -106,15 +86,10 @@ public class WeixinTemplateService {
         if(StrUtil.isBlank(openId)) {
             return;
         }
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
         Map<String,String> map = new HashMap<>();
         map.put("thing1",title);
         map.put("thing4",remark);
-        String tempId = wxMiniappTempConfig.getInquiryTempId();
+        String tempId = wxMiniappTempConfig.getInquiryTempID();
         if(StrUtil.isNotBlank(tempId)) {
             this.sendWxMpTemplateMessage(openId, tempId, "pages_user/followList", map);
         }
@@ -125,15 +100,10 @@ public class WeixinTemplateService {
         if(StrUtil.isBlank(openId)) {
             return;
         }
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
         Map<String,String> map = new HashMap<>();
         map.put("thing1",title);
         map.put("thing4",remark);
-        String tempId = wxMiniappTempConfig.getInquiryTempId();
+        String tempId = wxMiniappTempConfig.getInquiryTempID();
         if(StrUtil.isNotBlank(tempId)) {
             this.sendWxMpTemplateMessage(openId, tempId, "pages/TUIKit/TUIPages/TUIConversation/index" , map);
         }

+ 253 - 19
fs-service/src/main/java/com/fs/his/config/WxMiniappTempConfig.java

@@ -1,19 +1,253 @@
-//package com.fs.his.config;
-//
-//import lombok.Data;
-//import org.springframework.beans.factory.annotation.Value;
-//import org.springframework.stereotype.Component;
-//
-//import java.io.Serializable;
-//
-//@Data
-//@Component
-//public class WxMiniappTempConfig implements Serializable {
-//
-//
-//    @Value("${wx_miniapp_temp.pay_order_temp_id}")
-//    private String payOrderTempID;
-//
-//    @Value("${wx_miniapp_temp.inquiry_temp_id}")
-//    private String inquiryTempID;
-//}
+package com.fs.his.config;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.Serializable;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Component
+@Slf4j
+public class WxMiniappTempConfig implements Serializable {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private RedisCache redisCache;
+
+    // Redis缓存Key
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    // 缓存时间:30分钟
+    private static final Integer CACHE_EXPIRE_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
+
+    /**
+     * 公用方法:查询数据库获取配置对象
+     */
+    private ProjectConfig.WxMiniappTemp getTempConfigFromDB() {
+        try {
+            // 1. 先从Redis缓存中获取
+            ProjectConfig cachedConfig = getConfigFromCache();
+            if (cachedConfig != null) {
+                log.debug("从Redis缓存中获取ProjectConfig配置");
+                return cachedConfig.getWxMiniappTemp();
+            }
+            String configKey = "projectConfig";
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+            if (sysConfig == null || sysConfig.getConfigValue() == null) {
+                log.warn("未找到微信小程序模板消息配置");
+                return null;
+            }
+
+            ProjectConfig projectConfig = JSONObject.parseObject(
+                    sysConfig.getConfigValue(),
+                    ProjectConfig.class
+            );
+
+            if (projectConfig == null) {
+                log.warn("配置解析失败");
+                return null;
+            }
+
+            ProjectConfig.WxMiniappTemp wxMiniappTempConfig = projectConfig.getWxMiniappTemp();
+            if (wxMiniappTempConfig == null) {
+                log.warn("配置中未找到wxMiniappTemp节点");
+                return null;
+            }
+            saveConfigToCache(projectConfig);
+            return wxMiniappTempConfig;
+
+        } catch (Exception e) {
+            log.error("查询微信小程序模板消息配置失败", e);
+            return null;
+        }
+    }
+    /**
+     * 保存配置到Redis缓存
+     */
+    private void saveConfigToCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+            log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+        } catch (Exception e) {
+            log.warn("保存微信支付配置到Redis缓存失败", e);
+        }
+    }
+    /**
+     * 从Redis缓存获取ProjectConfig
+     */
+    private ProjectConfig getConfigFromCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (cachedJson != null && !cachedJson.isEmpty()) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取ProjectConfig失败", e);
+            return null;
+        }
+    }
+    /**
+     * 获取支付订单模板ID(每次查询数据库)
+     */
+    public String getPayOrderTempID() {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return "";
+        }
+
+        String tempId = tempConfig.getPayOrderTempId();
+        log.debug("获取支付订单模板ID: {}", tempId);
+        return tempId != null ? tempId : "";
+    }
+
+    /**
+     * 获取问诊模板ID(每次查询数据库)
+     */
+    public String getInquiryTempID() {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return "";
+        }
+
+        String tempId = tempConfig.getInquiryTempId();
+        log.debug("获取问诊模板ID: {}", tempId);
+        return tempId != null ? tempId : "";
+    }
+
+    /**
+     * 检查配置是否完整(每次查询数据库)
+     */
+    public boolean isConfigValid() {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return false;
+        }
+
+        String payOrderTempID = tempConfig.getPayOrderTempId();
+        String inquiryTempID = tempConfig.getInquiryTempId();
+
+        boolean isValid = payOrderTempID != null && !payOrderTempID.isEmpty() &&
+                inquiryTempID != null && !inquiryTempID.isEmpty();
+
+        log.debug("配置有效性检查: {}, payOrderTempID: {}, inquiryTempID: {}",
+                isValid, payOrderTempID, inquiryTempID);
+
+        return isValid;
+    }
+
+    /**
+     * 获取配置摘要(每次查询数据库)
+     */
+    public String getConfigSummary() {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return "配置为空";
+        }
+
+        return String.format("payOrderTempID: %s, inquiryTempID: %s",
+                tempConfig.getPayOrderTempId(),
+                tempConfig.getInquiryTempId());
+    }
+
+    /**
+     * 检查是否包含指定的模板ID(每次查询数据库)
+     */
+    public boolean containsTemplate(String templateId) {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null || templateId == null) {
+            return false;
+        }
+
+        String payOrderTempID = tempConfig.getPayOrderTempId();
+        String inquiryTempID = tempConfig.getInquiryTempId();
+
+        return templateId.equals(payOrderTempID) || templateId.equals(inquiryTempID);
+    }
+
+    /**
+     * 根据模板ID获取模板类型(每次查询数据库)
+     */
+    public String getTemplateType(String templateId) {
+        if (templateId == null) {
+            return "unknown";
+        }
+
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return "unknown";
+        }
+
+        if (templateId.equals(tempConfig.getPayOrderTempId())) {
+            return "pay_order";
+        } else if (templateId.equals(tempConfig.getInquiryTempId())) {
+            return "inquiry";
+        } else {
+            return "other";
+        }
+    }
+
+    /**
+     * 验证模板ID是否有效(每次查询数据库)
+     */
+    public boolean isValidTemplateId(String templateId) {
+        return containsTemplate(templateId);
+    }
+
+    /**
+     * 获取所有模板ID列表(每次查询数据库)
+     */
+    public String[] getAllTemplateIds() {
+        ProjectConfig.WxMiniappTemp tempConfig = getTempConfigFromDB();
+        if (tempConfig == null) {
+            return new String[0];
+        }
+
+        java.util.List<String> templateIds = new java.util.ArrayList<>();
+        String payOrderTempID = tempConfig.getPayOrderTempId();
+        String inquiryTempID = tempConfig.getInquiryTempId();
+
+        if (payOrderTempID != null && !payOrderTempID.isEmpty()) {
+            templateIds.add(payOrderTempID);
+        }
+        if (inquiryTempID != null && !inquiryTempID.isEmpty()) {
+            templateIds.add(inquiryTempID);
+        }
+
+        return templateIds.toArray(new String[0]);
+    }
+
+    /**
+     * 安全获取支付订单模板ID(处理null值)
+     */
+    public String getSafePayOrderTempID() {
+        String tempId = getPayOrderTempID();
+        return tempId != null ? tempId : "";
+    }
+
+    /**
+     * 安全获取问诊模板ID(处理null值)
+     */
+    public String getSafeInquiryTempID() {
+        String tempId = getInquiryTempID();
+        return tempId != null ? tempId : "";
+    }
+
+    /**
+     * 刷新配置(实际是空方法,因为每次都是实时查询)
+     */
+    public void refresh() {
+        log.info("刷新配置(每次都是实时查询,无需刷新缓存)");
+    }
+}

+ 0 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsDoctorServiceImpl.java

@@ -23,7 +23,6 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.HttpUtil;
 import com.fs.his.utils.qrcode.QRCodeUtils;
 import com.fs.his.vo.*;
-import com.fs.im.config.IMConfig;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.dto.*;
 import com.fs.im.service.IImService;

+ 0 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsFollowServiceImpl.java

@@ -18,9 +18,7 @@ import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.param.FsFollowListDParam;
 import com.fs.his.param.FsFollowListUParam;
 import com.fs.his.param.FsFollowParam;
-import com.fs.his.param.FsStoreOrderParam;
 import com.fs.his.vo.*;
-import com.fs.im.config.IMConfig;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.dto.*;
 import com.fs.im.service.IImService;

+ 0 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderMsgServiceImpl.java

@@ -30,7 +30,6 @@ import com.fs.his.param.ImMsgParam;
 import com.fs.his.service.IFsFollowReportService;
 import com.fs.his.service.IFsInquiryOrderReportService;
 import com.fs.his.vo.FsInquiryOrderMsgListDVO;
-import com.fs.im.config.IMConfig;
 import com.fs.im.dto.OpenImMsgCallBackResponse;
 import com.fs.im.service.IImService;
 import com.fs.im.vo.OpenImMsgCallBackVO;

+ 17 - 11
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -111,6 +111,7 @@ import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysDictTypeService;
+import com.fs.wx.miniapp.config.WxMaProperties;
 import com.fs.wx.order.domain.FsWxExpressTask;
 import com.fs.wx.order.dto.*;
 import com.fs.wx.order.mapper.FsWxExpressTaskMapper;
@@ -185,6 +186,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private SysConfigMapper sysConfigMapper;
     @Autowired
+    private WxMaProperties wxMaProperties;
+    @Autowired
     private CompanyUserUserMapper companyUserUserMapper;
     @Autowired
     private FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper;
@@ -4552,12 +4555,13 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRED)
     public R otherPaymentRemain(FsStoreOrderOtherPayParam param) {
 
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.Wx.Miniapp.Config config= projectConfig.getWx().getMiniapp().getConfigs().get(0);
-
+        List<WxMaProperties.Config> configs = wxMaProperties.getConfigs();
+        if (org.apache.commons.collections.CollectionUtils.isEmpty(configs)) {
+            log.error("小程序配置为空,无法执行订阅通知任务");
+            return R.error("小程序配置为空,无法执行订阅通知任务");
+        }
+        WxMaProperties.Config config = configs.get(0);
+        log.info("使用小程序配置,appid: {}", config.getAppid());
         final WxMaService wxService = WxMaConfiguration.getMaService(config.getAppid());
         try {
             String ip = IpUtil.getRequestIp();
@@ -4690,11 +4694,13 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Override
     @Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRED)
     public R otherPayment(FsStoreOrderOtherPayParam param) {
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
-
-        ProjectConfig.Wx.Miniapp.Config config= projectConfig.getWx().getMiniapp().getConfigs().get(0);
+        List<WxMaProperties.Config> configs = wxMaProperties.getConfigs();
+        if (org.apache.commons.collections.CollectionUtils.isEmpty(configs)) {
+            log.error("小程序配置为空,无法执行订阅通知任务");
+            return R.error("小程序配置为空,无法执行订阅通知任务");
+        }
+        WxMaProperties.Config config = configs.get(0);
+        log.info("使用小程序配置,appid: {}", config.getAppid());
         final WxMaService wxService = WxMaConfiguration.getMaService(config.getAppid());
         try {
             String ip = IpUtil.getRequestIp();

+ 98 - 35
fs-service/src/main/java/com/fs/wx/cp/config/WxCpConfiguration.java

@@ -1,12 +1,9 @@
 package com.fs.wx.cp.config;
 
-import com.alibaba.fastjson.JSONObject;
-import com.fs.config.saas.ProjectConfig;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.mapper.SysConfigMapper;
 import com.fs.wx.cp.handler.*;
 import com.google.common.collect.Maps;
 import lombok.val;
+import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.cp.api.WxCpService;
 import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
@@ -14,14 +11,13 @@ import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
 import me.chanjar.weixin.cp.constant.WxCpConsts;
 import me.chanjar.weixin.cp.message.WxCpMessageRouter;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 
 import javax.annotation.PostConstruct;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-
+@Slf4j
 @Configuration
 public class WxCpConfiguration {
     private LogHandler logHandler;
@@ -31,15 +27,16 @@ public class WxCpConfiguration {
     private MsgHandler msgHandler;
     private UnsubscribeHandler unsubscribeHandler;
     private SubscribeHandler subscribeHandler;
-    @Autowired
-    private SysConfigMapper sysConfigMapper;
+
+    private WxCpProperties properties;
+
     private static Map<Integer, WxCpMessageRouter> routers = Maps.newHashMap();
     private static Map<Integer, WxCpService> cpServices = Maps.newHashMap();
 
     @Autowired
     public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler,
                              MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler,
-                             SubscribeHandler subscribeHandler) {
+                             SubscribeHandler subscribeHandler, WxCpProperties properties) {
         this.logHandler = logHandler;
         this.nullHandler = nullHandler;
         this.locationHandler = locationHandler;
@@ -47,9 +44,9 @@ public class WxCpConfiguration {
         this.msgHandler = msgHandler;
         this.unsubscribeHandler = unsubscribeHandler;
         this.subscribeHandler = subscribeHandler;
+        this.properties = properties;
     }
 
-
     public static Map<Integer, WxCpMessageRouter> getRouters() {
         return routers;
     }
@@ -58,27 +55,91 @@ public class WxCpConfiguration {
         return cpServices.get(agentId);
     }
 
+    public static WxCpMessageRouter getMessageRouter(Integer agentId) {
+        return routers.get(agentId);
+    }
+
     @PostConstruct
     public void initServices() {
+        if (!properties.isConfigValid()) {
+            log.error("企业微信配置不完整,无法初始化服务");
+            return;
+        }
+
+        log.info("初始化企业微信服务,corpId: {}, 共 {} 个应用",
+                properties.getCorpId(), properties.getAppConfigs().size());
+
+        cpServices = properties.getAppConfigs().stream()
+                .filter(appConfig -> appConfig.isValid())
+                .map(a -> {
+                    val configStorage = new WxCpDefaultConfigImpl();
+                    configStorage.setCorpId(properties.getCorpId());
+                    configStorage.setAgentId(a.getAgentId());
+                    configStorage.setCorpSecret(a.getSecret());
+                    configStorage.setToken(a.getToken());
+                    configStorage.setAesKey(a.getAesKey());
+                    val service = new WxCpServiceImpl();
+                    service.setWxCpConfigStorage(configStorage);
+                    routers.put(a.getAgentId(), this.newRouter(service));
+                    log.debug("创建企业微信服务成功,agentId: {}", a.getAgentId());
+                    return service;
+                })
+                .collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
+
+        log.info("企业微信服务初始化完成,共 {} 个应用", cpServices.size());
+    }
+
+    /**
+     * 刷新服务配置
+     */
+    public void refreshServices() {
+        log.info("刷新企业微信服务配置");
 
-        String configKey = "projectConfig";
-        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
-        ProjectConfig projectConfig = JSONObject.parseObject(sysConfig.getConfigValue(),ProjectConfig.class);
+        // 刷新配置
+        properties.refresh();
 
-        ProjectConfig.Wx.Cp properties= projectConfig.getWx().getCp();
+        // 清理旧的配置
+        routers.clear();
+        cpServices.clear();
+
+        // 重新初始化
+        initServices();
+    }
+
+    /**
+     * 获取或创建服务(如果静态方法中不存在则动态创建)
+     */
+    public WxCpService getOrCreateService(Integer agentId) {
+        WxCpService service = cpServices.get(agentId);
+        if (service == null) {
+            log.info("动态创建企业微信服务,agentId: {}", agentId);
+
+            WxCpProperties.AppConfig appConfig = properties.getAppConfig(agentId);
+            if (appConfig == null || !appConfig.isValid()) {
+                log.error("未找到有效的企业微信应用配置,agentId: {}", agentId);
+                throw new RuntimeException("未找到 agentId 为 " + agentId + " 的企业微信应用配置");
+            }
 
-        cpServices = properties.getAppConfigs().stream().map(a -> {
             val configStorage = new WxCpDefaultConfigImpl();
             configStorage.setCorpId(properties.getCorpId());
-            configStorage.setAgentId(a.getAgentId());
-            configStorage.setCorpSecret(a.getSecret());
-            configStorage.setToken(a.getToken());
-            configStorage.setAesKey(a.getAesKey());
-            val service = new WxCpServiceImpl();
+            configStorage.setAgentId(appConfig.getAgentId());
+            configStorage.setCorpSecret(appConfig.getSecret());
+            configStorage.setToken(appConfig.getToken());
+            configStorage.setAesKey(appConfig.getAesKey());
+
+            service = new WxCpServiceImpl();
             service.setWxCpConfigStorage(configStorage);
-            routers.put(a.getAgentId(), this.newRouter(service));
-            return service;
-        }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
+
+            // 创建对应的路由器
+            WxCpMessageRouter router = this.newRouter(service);
+
+            // 保存到静态Map中
+            cpServices.put(agentId, service);
+            routers.put(agentId, router);
+
+            log.info("动态创建企业微信服务成功,agentId: {}", agentId);
+        }
+        return service;
     }
 
     private WxCpMessageRouter newRouter(WxCpService wxCpService) {
@@ -89,40 +150,42 @@ public class WxCpConfiguration {
 
         // 自定义菜单事件
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end();
+                .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end();
 
         // 点击菜单链接事件(这里使用了一个空的处理器,可以根据自己需要进行扩展)
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end();
+                .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end();
 
         // 关注事件
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler)
-            .end();
+                .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler)
+                .end();
 
         // 取消关注事件
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.EventType.UNSUBSCRIBE)
-            .handler(this.unsubscribeHandler).end();
+                .event(WxConsts.EventType.UNSUBSCRIBE)
+                .handler(this.unsubscribeHandler).end();
 
         // 上报地理位置事件
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.EventType.LOCATION).handler(this.locationHandler)
-            .end();
+                .event(WxConsts.EventType.LOCATION).handler(this.locationHandler)
+                .end();
 
         // 接收地理位置消息
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION)
-            .handler(this.locationHandler).end();
+                .handler(this.locationHandler).end();
 
         // 扫码事件(这里使用了一个空的处理器,可以根据自己需要进行扩展)
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxConsts.EventType.SCAN).handler(this.nullHandler).end();
+                .event(WxConsts.EventType.SCAN).handler(this.nullHandler).end();
 
+        // 通讯录变更事件(使用 new 创建,保持原有方式)
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end();
+                .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end();
 
+        // 进入应用事件(使用 new 创建,保持原有方式)
         newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
-            .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end();
+                .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end();
 
         // 默认
         newRouter.rule().async(false).handler(this.msgHandler).end();

+ 287 - 51
fs-service/src/main/java/com/fs/wx/cp/config/WxCpProperties.java

@@ -1,51 +1,287 @@
-//package com.fs.wx.cp.config;
-//
-//
-//import com.fs.wx.utils.JsonUtils;
-//import lombok.Data;
-//import lombok.Getter;
-//import lombok.Setter;
-//import org.springframework.boot.context.properties.ConfigurationProperties;
-//
-//import java.util.List;
-//
-//@Data
-//@ConfigurationProperties(prefix = "wx.cp")
-//public class WxCpProperties {
-//  /**
-//   * 设置企业微信的corpId
-//   */
-//  private String corpId;
-//
-//  private List<AppConfig> appConfigs;
-//
-//  @Getter
-//  @Setter
-//  public static class AppConfig {
-//    /**
-//     * 设置企业微信应用的AgentId
-//     */
-//    private Integer agentId;
-//
-//    /**
-//     * 设置企业微信应用的Secret
-//     */
-//    private String secret;
-//
-//    /**
-//     * 设置企业微信应用的token
-//     */
-//    private String token;
-//
-//    /**
-//     * 设置企业微信应用的EncodingAESKey
-//     */
-//    private String aesKey;
-//
-//  }
-//
-//  @Override
-//  public String toString() {
-//    return JsonUtils.toJson(this);
-//  }
-//}
+package com.fs.wx.cp.config;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Data
+@Component
+@Slf4j
+public class WxCpProperties {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private RedisCache redisCache;
+
+    // Redis缓存Key
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    // 缓存时间:30分钟
+    private static final Integer CACHE_EXPIRE_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
+
+    /**
+     * 公用方法:查询数据库获取企业微信配置
+     */
+    private ProjectConfig.Wx.Cp getWxCpConfigFromDB() {
+        try {
+            // 1. 先从Redis缓存中获取
+            ProjectConfig cachedConfig = getConfigFromCache();
+            if (cachedConfig != null) {
+                log.debug("从Redis缓存中获取ProjectConfig配置");
+                return cachedConfig.getWx().getCp();
+            }
+            String configKey = "projectConfig";
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+            if (sysConfig == null || sysConfig.getConfigValue() == null) {
+                log.warn("未找到企业微信配置");
+                return null;
+            }
+
+            ProjectConfig projectConfig = JSONObject.parseObject(
+                    sysConfig.getConfigValue(),
+                    ProjectConfig.class
+            );
+
+            if (projectConfig == null || projectConfig.getWx() == null) {
+                log.warn("配置中未找到wx节点");
+                return null;
+            }
+
+            ProjectConfig.Wx.Cp cpProperties = projectConfig.getWx().getCp();
+            if (cpProperties == null) {
+                log.warn("配置中未找到cp节点");
+                return null;
+            }
+            saveConfigToCache(projectConfig);
+            return cpProperties;
+
+        } catch (Exception e) {
+            log.error("查询企业微信配置失败", e);
+            return null;
+        }
+    }
+    /**
+     * 保存配置到Redis缓存
+     */
+    private void saveConfigToCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+            log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+        } catch (Exception e) {
+            log.warn("保存微信支付配置到Redis缓存失败", e);
+        }
+    }
+    /**
+     * 从Redis缓存获取ProjectConfig
+     */
+    private ProjectConfig getConfigFromCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (cachedJson != null && !cachedJson.isEmpty()) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取ProjectConfig失败", e);
+            return null;
+        }
+    }
+    /**
+     * 获取企业微信corpId(每次查询数据库)
+     */
+    public String getCorpId() {
+        ProjectConfig.Wx.Cp cpProperties = getWxCpConfigFromDB();
+        if (cpProperties == null) {
+            return "";
+        }
+        return cpProperties.getCorpId() != null ? cpProperties.getCorpId() : "";
+    }
+
+    /**
+     * 获取所有应用配置(每次查询数据库)
+     */
+    public List<AppConfig> getAppConfigs() {
+        ProjectConfig.Wx.Cp cpProperties = getWxCpConfigFromDB();
+        if (cpProperties == null || cpProperties.getAppConfigs() == null) {
+            return new ArrayList<>();
+        }
+
+        List<ProjectConfig.Wx.Cp.AppConfig> dbAppConfigs = cpProperties.getAppConfigs();
+        if (dbAppConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return dbAppConfigs.stream().map(dbConfig -> {
+            AppConfig appConfig = new AppConfig();
+            appConfig.setAgentId(dbConfig.getAgentId());
+            appConfig.setSecret(dbConfig.getSecret());
+            appConfig.setToken(dbConfig.getToken());
+            appConfig.setAesKey(dbConfig.getAesKey());
+            return appConfig;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 检查配置是否完整(每次查询数据库)
+     */
+    public boolean isConfigValid() {
+        ProjectConfig.Wx.Cp cpProperties = getWxCpConfigFromDB();
+        if (cpProperties == null) {
+            return false;
+        }
+
+        String corpId = cpProperties.getCorpId();
+        List<ProjectConfig.Wx.Cp.AppConfig> appConfigs = cpProperties.getAppConfigs();
+
+        return corpId != null && !corpId.isEmpty() &&
+                appConfigs != null && !appConfigs.isEmpty();
+    }
+
+    /**
+     * 获取指定agentId的应用配置(每次查询数据库)
+     */
+    public AppConfig getAppConfig(Integer agentId) {
+        List<AppConfig> appConfigs = getAppConfigs();
+        if (appConfigs.isEmpty()) {
+            return null;
+        }
+
+        return appConfigs.stream()
+                .filter(config -> config.getAgentId() != null && config.getAgentId().equals(agentId))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * 获取第一个应用配置(每次查询数据库)
+     */
+    public AppConfig getFirstAppConfig() {
+        List<AppConfig> appConfigs = getAppConfigs();
+        if (appConfigs.isEmpty()) {
+            throw new RuntimeException("未找到企业微信应用配置");
+        }
+        return appConfigs.get(0);
+    }
+
+    /**
+     * 获取所有agentId列表(每次查询数据库)
+     */
+    public List<Integer> getAllAgentIds() {
+        List<AppConfig> appConfigs = getAppConfigs();
+        if (appConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return appConfigs.stream()
+                .map(AppConfig::getAgentId)
+                .filter(agentId -> agentId != null)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 检查是否包含指定agentId的配置(每次查询数据库)
+     */
+    public boolean hasAppConfig(Integer agentId) {
+        return getAppConfig(agentId) != null;
+    }
+
+    /**
+     * 获取配置摘要(每次查询数据库)
+     */
+    public String getConfigSummary() {
+        ProjectConfig.Wx.Cp cpProperties = getWxCpConfigFromDB();
+        if (cpProperties == null) {
+            return "企业微信配置为空";
+        }
+
+        List<ProjectConfig.Wx.Cp.AppConfig> dbAppConfigs = cpProperties.getAppConfigs();
+        int appCount = (dbAppConfigs != null) ? dbAppConfigs.size() : 0;
+
+        return String.format("corpId: %s, 共 %d 个应用", cpProperties.getCorpId(), appCount);
+    }
+
+    /**
+     * 获取所有有效的应用配置(过滤掉无效的)
+     */
+    public List<AppConfig> getValidAppConfigs() {
+        List<AppConfig> appConfigs = getAppConfigs();
+        if (appConfigs.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return appConfigs.stream()
+                .filter(AppConfig::isValid)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 获取完整的配置对象(每次查询数据库)
+     */
+    public ProjectConfig.Wx.Cp getWxCpConfig() {
+        return getWxCpConfigFromDB();
+    }
+
+    /**
+     * 刷新配置(实际是空方法,因为每次都是实时查询)
+     */
+    public void refresh() {
+        log.info("刷新企业微信配置(每次都是实时查询,无需刷新缓存)");
+    }
+
+    @Getter
+    @Setter
+    public static class AppConfig {
+        /**
+         * 设置企业微信应用的AgentId
+         */
+        private Integer agentId;
+
+        /**
+         * 设置企业微信应用的Secret
+         */
+        private String secret;
+
+        /**
+         * 设置企业微信应用的token
+         */
+        private String token;
+
+        /**
+         * 设置企业微信应用的EncodingAESKey
+         */
+        private String aesKey;
+
+        /**
+         * 检查应用配置是否完整
+         */
+        public boolean isValid() {
+            return agentId != null &&
+                    secret != null && !secret.isEmpty() &&
+                    token != null && !token.isEmpty() &&
+                    aesKey != null && !aesKey.isEmpty();
+        }
+
+        /**
+         * 获取配置摘要
+         */
+        public String getSummary() {
+            return String.format("agentId: %d", agentId);
+        }
+    }
+}

+ 290 - 0
fs-service/src/main/java/com/fs/wx/miniapp/config/WxMaProperties.java

@@ -0,0 +1,290 @@
+package com.fs.wx.miniapp.config;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.config.saas.ProjectConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class WxMaProperties {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private RedisCache redisCache;
+
+    // Redis缓存Key
+    private static final String PROJECT_CONFIG_CACHE_KEY = "project:config:data";
+    // 缓存时间:30分钟
+    private static final Integer CACHE_EXPIRE_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRE_UNIT = TimeUnit.MINUTES;
+
+    /**
+     * 公用方法:查询数据库获取小程序配置
+     */
+    private List<ProjectConfig.Wx.Miniapp.Config> getWxMaConfigsFromDB() {
+        try {
+            // 1. 先从Redis缓存中获取
+            ProjectConfig cachedConfig = getConfigFromCache();
+            if (cachedConfig != null) {
+                return cachedConfig.getWx().getMiniapp().getConfigs();
+            }
+
+            String configKey = "projectConfig";
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+
+            if (sysConfig == null || StringUtils.isBlank(sysConfig.getConfigValue())) {
+                return new ArrayList<>();
+            }
+
+            // 解析JSON配置
+            ProjectConfig projectConfig = JSONObject.parseObject(
+                    sysConfig.getConfigValue(),
+                    ProjectConfig.class
+            );
+
+            if (projectConfig == null || projectConfig.getWx() == null ||
+                    projectConfig.getWx().getMiniapp() == null) {
+                return new ArrayList<>();
+            }
+
+            List<ProjectConfig.Wx.Miniapp.Config> dbConfigs =
+                    projectConfig.getWx().getMiniapp().getConfigs();
+
+            saveConfigToCache(projectConfig);
+            return dbConfigs != null ? dbConfigs : new ArrayList<>();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            return new ArrayList<>();
+        }
+    }
+    /**
+     * 保存配置到Redis缓存
+     */
+    private void saveConfigToCache(ProjectConfig config) {
+        try {
+            String configJson = JSONObject.toJSONString(config);
+            redisCache.setCacheObject(PROJECT_CONFIG_CACHE_KEY, configJson, CACHE_EXPIRE_TIME, CACHE_EXPIRE_UNIT);
+            log.debug("微信支付配置已存入Redis缓存,有效期{}分钟", CACHE_EXPIRE_TIME);
+        } catch (Exception e) {
+            log.warn("保存微信支付配置到Redis缓存失败", e);
+        }
+    }
+    /**
+     * 从Redis缓存获取ProjectConfig
+     */
+    private ProjectConfig getConfigFromCache() {
+        try {
+            String cachedJson = redisCache.getCacheObject(PROJECT_CONFIG_CACHE_KEY);
+            if (cachedJson != null && !cachedJson.isEmpty()) {
+                return JSONObject.parseObject(cachedJson, ProjectConfig.class);
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("从Redis缓存获取ProjectConfig失败", e);
+            return null;
+        }
+    }
+    /**
+     * 获取所有配置(每次查询数据库)
+     */
+    public List<Config> getConfigs() {
+        List<ProjectConfig.Wx.Miniapp.Config> dbConfigs = getWxMaConfigsFromDB();
+
+        return dbConfigs.stream().map(dbConfig -> {
+            Config config = new Config();
+            config.setAppid(dbConfig.getAppid());
+            config.setSecret(dbConfig.getSecret());
+            config.setToken(dbConfig.getToken());
+            config.setAesKey(dbConfig.getAesKey());
+            config.setMsgDataFormat(dbConfig.getMsgDataFormat());
+            return config;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取第一个配置(适用于单个小程序)
+     */
+    public Config getFirstConfig() {
+        List<Config> configs = getConfigs();
+        if (configs.isEmpty()) {
+            throw new RuntimeException("未找到微信小程序配置");
+        }
+        return configs.get(0);
+    }
+
+    /**
+     * 根据appid获取配置
+     */
+    public Config getConfig(String appid) {
+        List<Config> configs = getConfigs();
+        if (configs.isEmpty()) {
+            return null;
+        }
+
+        return configs.stream()
+                .filter(c -> appid.equals(c.getAppid()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * 检查配置是否存在
+     */
+    public boolean isConfigValid() {
+        List<Config> configs = getConfigs();
+        return !configs.isEmpty();
+    }
+
+    /**
+     * 获取配置摘要
+     */
+    public String getConfigSummary() {
+        List<Config> configs = getConfigs();
+        if (configs.isEmpty()) {
+            return "微信小程序配置为空";
+        }
+
+        List<String> appIds = configs.stream()
+                .map(Config::getAppid)
+                .collect(Collectors.toList());
+
+        return String.format("共 %d 个小程序: %s",
+                configs.size(), String.join(", ", appIds));
+    }
+
+    /**
+     * 创建微信小程序服务
+     */
+    public WxMaService createWxMaService(String appid) {
+        Config config = getConfig(appid);
+        if (config == null) {
+            throw new RuntimeException("未找到appid为" + appid + "的配置");
+        }
+
+        return createWxMaService(config);
+    }
+
+    /**
+     * 使用第一个配置创建微信小程序服务
+     */
+    public WxMaService createFirstWxMaService() {
+        Config config = getFirstConfig();
+        return createWxMaService(config);
+    }
+
+    /**
+     * 使用指定配置创建微信小程序服务
+     */
+    private WxMaService createWxMaService(Config config) {
+        WxMaDefaultConfigImpl wxConfig = new WxMaDefaultConfigImpl();
+        wxConfig.setAppid(config.getAppid());
+        wxConfig.setSecret(config.getSecret());
+        wxConfig.setToken(config.getToken());
+        wxConfig.setAesKey(config.getAesKey());
+        wxConfig.setMsgDataFormat(config.getMsgDataFormat());
+
+        WxMaService service = new WxMaServiceImpl();
+        service.setWxMaConfig(wxConfig);
+        return service;
+    }
+
+    /**
+     * 检查是否存在配置
+     */
+    public boolean hasConfig(String appid) {
+        return getConfig(appid) != null;
+    }
+
+    /**
+     * 获取所有appid列表
+     */
+    public List<String> getAllAppIds() {
+        List<Config> configs = getConfigs();
+        return configs.stream()
+                .map(Config::getAppid)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 获取有效的配置(检查配置完整性)
+     */
+    public List<Config> getValidConfigs() {
+        List<Config> configs = getConfigs();
+        return configs.stream()
+                .filter(config -> config.getAppid() != null && !config.getAppid().isEmpty() &&
+                        config.getSecret() != null && !config.getSecret().isEmpty())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 获取原始的数据库配置
+     */
+    public List<ProjectConfig.Wx.Miniapp.Config> getOriginalConfigs() {
+        return getWxMaConfigsFromDB();
+    }
+
+    /**
+     * 刷新配置(实际是空方法,因为每次都是实时查询)
+     */
+    public void refresh() {
+        // 空实现,因为每次都是实时查询数据库
+    }
+
+    // ============= Config 内部类 =============
+
+    public static class Config {
+        private String appid;
+        private String secret;
+        private String token;
+        private String aesKey;
+        private String msgDataFormat;
+
+        public String getAppid() { return appid; }
+        public void setAppid(String appid) { this.appid = appid; }
+
+        public String getSecret() { return secret; }
+        public void setSecret(String secret) { this.secret = secret; }
+
+        public String getToken() { return token; }
+        public void setToken(String token) { this.token = token; }
+
+        public String getAesKey() { return aesKey; }
+        public void setAesKey(String aesKey) { this.aesKey = aesKey; }
+
+        public String getMsgDataFormat() { return msgDataFormat; }
+        public void setMsgDataFormat(String msgDataFormat) {
+            this.msgDataFormat = msgDataFormat;
+        }
+
+        /**
+         * 检查配置是否完整
+         */
+        public boolean isValid() {
+            return appid != null && !appid.isEmpty() &&
+                    secret != null && !secret.isEmpty();
+        }
+
+        /**
+         * 获取配置摘要
+         */
+        public String getSummary() {
+            return String.format("appid: %s", appid);
+        }
+    }
+}

+ 125 - 109
fs-service/src/main/resources/application-config-dev.yml

@@ -7,115 +7,131 @@ logging:
     org.springframework.web: debug
     com.github.binarywang.demo.wx.cp: DEBUG
     me.chanjar.weixin: DEBUG
-wx:
-  miniapp:
-    configs:
-      - appid: wx29d26f63f836be7f
-        secret: 7542db9774355a89b1adce24defb6013
-        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
-        msgDataFormat: JSON
-#      - appid: wx4115995705bb0ea0   #中康智慧
-#        secret: 58910ae743005c396012b029c7def579
+#wx:
+#  miniapp:
+#    configs:
+#      - appid: wx29d26f63f836be7f
+#        secret: 7542db9774355a89b1adce24defb6013
 #        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
 #        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
 #        msgDataFormat: JSON
-#      - appid: wxedde588767b358b1   #中康未来智慧药房
-#        secret: 928d2961c81610d8f64b019597212fcd
-#        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-#        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
-#        msgDataFormat: JSON
-#      - appid: wxa73f0d48f1f2f66c   #金康健
-#        secret: 93d342d00b2d7126a044408fb7082798
-#        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-#        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
-#        msgDataFormat: JSON
-#      - appid: wx29d26f63f836be7f  #中康智慧商城APP
-#        secret: a85bfaf0d8e243817f265a321684f6ec
-#        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-#        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
-#        msgDataFormat: JSON
-  cp:
-    corpId: wwb2a1055fb6c9a7c2
-    appConfigs:
-      - agentId: 1000005
-        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
-        token: PPKOdAlCoMO
-        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
-  pay:
-    appId: wx73f85f8d62769119 #微信公众号或者小程序等的appid
-    mchId: 1611402045 #微信支付商户号
-    mchKey: 8cab128997a3547c1363b0898b877f38 #微信支付商户密钥
-    subAppId:  #服务商模式下的子商户公众账号ID
-    subMchId:  #服务商模式下的子商户号
-    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
-    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
-  mp:
-    useRedis: false
-    redisConfig:
-      host: 127.0.0.1
-      port: 6379
-      timeout: 2000
-    configs:
-      - appId: wx93ce67750e3cfba3 # 第一个公众号的appid  //公众号名称:云联融智
-        secret: c172884087264160563bfe5775ca0f6f # 公众号的appsecret
-        token: PPKOdAlCoMO # 接口配置里的Token值
-        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
-aifabu:  #爱链接
-  appKey: 7b471be905ab17e00f3b858c6710dd117601d008
-watch:
-  watchUrl: watch.ylrzcloud.com/prod-api
-  #  account: tcloud
-  #  password: mdf-m2h_6yw2$hq
-  account1: ccif #866655060138751
-  password1: cp-t5or_6xw7$mt
-  account2: tcloud #rt500台
-  password2: mdf-m2h_6yw2$hq
-  account3: whr
-  password3: v9xsKuqn_$d2y
-
-fs :
-  commonApi: http://172.16.0.16:8010
-  h5CommonApi: http://119.29.195.254:8010
-  jwt:
-    # 加密秘钥
-    secret: e10adc3949ba59abbe56e057f20f883e
-    # token有效时长,7天,单位秒
-    expire: 31536000
-    header: AppToken
-nuonuo:
-  key: 10924508
-  secret: A2EB20764D304D16
-
-# 存储捅配置
-tencent_cloud_config:
-  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
-  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
-  bucket: myhk-1323137866
-  app_id: 1323137866
-  region: ap-chongqing
-  proxy: myhk
-cloud_host:
-  company_name: 金康健
-  projectCode: DEV
-  spaceName:
-  volcengineUrl:
-headerImg:
-  imgUrl: https://jz-cos-1356808054.cos.ap-chengdu.myqcloud.com/fs/20250515/0877754b59814ea8a428fa3697b20e68.png
-ipad:
-  url:
-  ipadUrl: http://ipad.cdwjyyh.com
-  aiApi: http://152.136.202.157:3000/api
-  voiceApi:
-  commonApi:
-wx_miniapp_temp:
-  pay_order_temp_id:
-  inquiry_temp_id:
-# 聚水潭API配置
-jst:
-#  app_key: a4b1fab173c84f67b3873857eea11d90 #聚水潭2025-07-25
-  app_key: 871348458a964548a72bf8124cf917a4 #聚水潭2025-08-14
-  app_secret: 5b7d9369dbcd414db45089bc047ebe1a #聚水潭2025-08-14
-#  app_secret: dfce1f8dc8a64ddc91212fc3fcdd9349 #聚水潭2025-07-25
-  authorization_code: 666666
-  shop_code: "18461733"
+#  cp:
+#    corpId: wwb2a1055fb6c9a7c2
+#    appConfigs:
+#      - agentId: 1000005
+#        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
+#        token: PPKOdAlCoMO
+#        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+#  pay:
+#    appId: wx73f85f8d62769119 #微信公众号或者小程序等的appid
+#    mchId: 1611402045 #微信支付商户号
+#    mchKey: 8cab128997a3547c1363b0898b877f38 #微信支付商户密钥
+#    subAppId:  #服务商模式下的子商户公众账号ID
+#    subMchId:  #服务商模式下的子商户号
+#    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+#    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
+#  mp:
+#    useRedis: false
+#    redisConfig:
+#      host: 127.0.0.1
+#      port: 6379
+#      timeout: 2000
+#    configs:
+#      - appId: wx93ce67750e3cfba3 # 第一个公众号的appid  //公众号名称:云联融智
+#        secret: c172884087264160563bfe5775ca0f6f # 公众号的appsecret
+#        token: PPKOdAlCoMO # 接口配置里的Token值
+#        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+#aifabu:  #爱链接
+#  appKey: 7b471be905ab17e00f3b858c6710dd117601d008
+#watch:
+#  watchUrl: watch.ylrzcloud.com/prod-api
+#  #  account: tcloud
+#  #  password: mdf-m2h_6yw2$hq
+#  account1: ccif #866655060138751
+#  password1: cp-t5or_6xw7$mt
+#  account2: tcloud #rt500台
+#  password2: mdf-m2h_6yw2$hq
+#  account3: whr
+#  password3: v9xsKuqn_$d2y
+#
+#fs :
+#  commonApi: http://172.16.0.16:8010
+#  h5CommonApi: http://119.29.195.254:8010
+#  jwt:
+#    # 加密秘钥
+#    secret: e10adc3949ba59abbe56e057f20f883e
+#    # token有效时长,7天,单位秒
+#    expire: 31536000
+#    header: AppToken
+#nuonuo:
+#  key: 10924508
+#  secret: A2EB20764D304D16
+#
+## 存储捅配置
+#tencent_cloud_config:
+#  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+#  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+#  bucket: myhk-1323137866
+#  app_id: 1323137866
+#  region: ap-chongqing
+#  proxy: myhk
+#cloud_host:
+#  company_name: 金康健
+#  projectCode: DEV
+#  spaceName:
+#  volcengineUrl:
+#headerImg:
+#  imgUrl: https://jz-cos-1356808054.cos.ap-chengdu.myqcloud.com/fs/20250515/0877754b59814ea8a428fa3697b20e68.png
+#ipad:
+#  url:
+#  ipadUrl: http://ipad.cdwjyyh.com
+#  aiApi: http://152.136.202.157:3000/api
+#  voiceApi:
+#  commonApi:
+#wx_miniapp_temp:
+#  pay_order_temp_id:
+#  inquiry_temp_id:
+## 聚水潭API配置
+#jst:
+##  app_key: a4b1fab173c84f67b3873857eea11d90 #聚水潭2025-07-25
+#  app_key: 871348458a964548a72bf8124cf917a4 #聚水潭2025-08-14
+#  app_secret: 5b7d9369dbcd414db45089bc047ebe1a #聚水潭2025-08-14
+##  app_secret: dfce1f8dc8a64ddc91212fc3fcdd9349 #聚水潭2025-07-25
+#  authorization_code: 666666
+#  shop_code: "18461733"
+#
+## RocketMQ配置
+#rocketmq:
+#  name-server: 127.0.0.1:9876
+#  producer:
+#    group: event-feedback-producer
+#    send-message-timeout: 3000
+#    retry-times-when-send-failed: 2
+#    retry-times-when-send-async-failed: 2
+#    max-message-size: 4194304
+#    compress-message-body-threshold: 4096
+#    retry-next-server: true
+#custom:
+#  token: "1o62d3YxvdHd4LEUiltnu7sK"
+#  encoding-aes-key: "UJfTQ5qKTKlegjkXtp1YuzJzxeHlUKvq5GyFbERN1iU"
+#  corp-id: "ww51717e2b71d5e2d3"configValue
+#  secret: "6ODAmw-8W4t6h9mdzHh2Z4Apwj8mnsyRnjEDZOHdA7k"
+#  private-key-path: "privatekey.pem"
+#  webhook-url: "https://your-server.com/wecom/archive"
+## token配置
+#token:
+#  # 令牌自定义标识
+#  header: Authorization
+#  # 令牌密钥
+#  secret: abcdefghijklmnopqrstuvwxyz
+#  # 令牌有效期(默认30分钟)
+#  expireTime: 180
+#openIM:
+#  secret: openIM123
+#  userID: imAdmin
+#  url: https://web.jnmyim.ylrzfs.com/api
+##是否为新商户,新商户不走mpOpenId
+#isNewWxMerchant: true
+##是否使用新im
+#im:
+#  type: OPENIM