zyy 1 день назад
Родитель
Сommit
4d6e5cbff8
48 измененных файлов с 3075 добавлено и 647 удалено
  1. 2 1
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  2. 8 2
      fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java
  3. 33 0
      fs-service/src/main/java/com/fs/sop/params/WxSopTagsParam.java
  4. 33 0
      fs-service/src/main/java/com/fs/sop/vo/QwSopTempRulesWithDayVO.java
  5. 63 0
      fs-service/src/main/java/com/fs/sop/vo/WxFilterSopCustomersResult.java
  6. 26 97
      fs-service/src/main/java/com/fs/wx/cp/config/WxCpConfiguration.java
  7. 25 256
      fs-service/src/main/java/com/fs/wx/cp/config/WxCpProperties.java
  8. 0 2
      fs-service/src/main/java/com/fs/wx/kf/service/IWeixinKfService.java
  9. 0 3
      fs-service/src/main/java/com/fs/wx/kf/service/impl/WeixinKfServiceImpl.java
  10. 0 1
      fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfAddEditAccountVO.java
  11. 0 1
      fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfMsgSendVO.java
  12. 0 1
      fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfUserItemVO.java
  13. 55 251
      fs-service/src/main/java/com/fs/wx/miniapp/config/WxMaProperties.java
  14. 25 0
      fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java
  15. 29 0
      fs-service/src/main/java/com/fs/wx/order/service/IFsWxExpressTaskService.java
  16. 23 1
      fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java
  17. 0 3
      fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthFactory.java
  18. 43 0
      fs-service/src/main/java/com/fs/wx/order/service/impl/FsWxExpressTaskServiceImpl.java
  19. 0 10
      fs-service/src/main/java/com/fs/wx/order/service/impl/LiveExpressToWxService.java
  20. 2 6
      fs-service/src/main/java/com/fs/wx/order/service/impl/ShopExpressToWxService.java
  21. 76 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java
  22. 92 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java
  23. 53 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java
  24. 72 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java
  25. 88 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  26. 94 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java
  27. 79 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java
  28. 91 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  29. 13 0
      fs-service/src/main/java/com/fs/wx/sop/params/SendWxSopMsgParam.java
  30. 57 0
      fs-service/src/main/java/com/fs/wx/sop/params/WxSopLogsParam.java
  31. 42 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopExecuteService.java
  32. 86 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java
  33. 79 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java
  34. 62 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserInfoService.java
  35. 62 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java
  36. 555 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java
  37. 317 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  38. 240 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java
  39. 102 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java
  40. 115 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java
  41. 110 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java
  42. 11 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopMsgVo.java
  43. 27 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopUserMsgGenVO.java
  44. 0 1
      fs-service/src/main/java/com/fs/wx/utils/WxPayUtils.java
  45. 8 8
      fs-service/src/main/resources/application-config-dev.yml
  46. 33 0
      fs-service/src/main/resources/mapper/sop/QwSopTempRulesMapper.xml
  47. 124 3
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java
  48. 20 0
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

+ 2 - 1
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -18,5 +18,6 @@ public enum DataSourceType
      */
     SLAVE,
     EASYCALL,
-    SopREAD
+    SopREAD,
+    SOP
 }

+ 8 - 2
fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTempRules;
+import com.fs.sop.vo.QwSopTempRulesWithDayVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
@@ -69,7 +70,7 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules> {
     @Select("select * from qw_sop_temp_rules where day_num is null ")
     List<QwSopTempRules> rulesNull();
 
-    
+
     @Select("<script>" +
             "select tr.* from qw_sop_temp_rules tr " +
             "left join qw_sop_temp st on tr.temp_id=st.id " +
@@ -78,7 +79,7 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules> {
             "</script>")
     List<QwSopTempRules> listByCourseId(@Param("courseId") Long courseId);
 
-    
+
     @Select("<script>" +
             "select tr.* from qw_sop_temp_rules tr " +
             "left join qw_sop_temp st on tr.temp_id=st.id " +
@@ -104,4 +105,9 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules> {
     List<Long> getTempOfficialIdsForClose(@Param("tempId") String tempId);
 
     int updateTempRulesOfficialBatch(@Param("ids") List<Long> ids,@Param("official") Integer official);
+
+    /**
+     * 查询模板规则并关联 day_num(通过 qw_sop_temp_day)
+     */
+    List<QwSopTempRulesWithDayVO> listByTempIdWithDayNum(@Param("id") String id);
 }

+ 33 - 0
fs-service/src/main/java/com/fs/sop/params/WxSopTagsParam.java

@@ -0,0 +1,33 @@
+package com.fs.sop.params;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 个微SOP标签筛选参数类
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class WxSopTagsParam {
+
+    /**
+     * 执行账号ID集合
+     */
+    private List<String> accountIdsSelectList;
+
+    /**
+     * 标签过滤类型(排除标签默认 只要有其一的就排除)
+     */
+    private Integer filterType;
+
+    /** 按标签筛选客户 */
+    private List<String> tagsIdsSelectList;
+
+    /** 按排除标签筛选客户 */
+    private List<String> outTagsIdsSelectList;
+
+    private String corpId;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/sop/vo/QwSopTempRulesWithDayVO.java

@@ -0,0 +1,33 @@
+package com.fs.sop.vo;
+
+import lombok.Data;
+
+/**
+ * qw_sop_temp_rules 关联 day_num(content) 
+ * 用于个微消息生成流程
+ */
+@Data
+public class QwSopTempRulesWithDayVO {
+
+    private Long id;
+    private String tempId;
+    private Long dayId;
+    private String name;
+    private String time;
+    private String isOfficial;
+    private Integer contentType;
+    private Integer type;
+    private Integer courseType;
+    private Long courseId;
+    private Long videoId;
+    private String aiTouch;
+    private String addTag;
+    private String delTag;
+    private Integer sorts;
+    private Integer isAtAll;
+    private Long liveId;
+
+    private Integer dayNum;
+
+    private String textContent;
+}

+ 63 - 0
fs-service/src/main/java/com/fs/sop/vo/WxFilterSopCustomersResult.java

@@ -0,0 +1,63 @@
+package com.fs.sop.vo;
+
+import lombok.Data;
+
+/**
+ * 个微SOP筛选客户结果对象
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class WxFilterSopCustomersResult {
+
+    /**
+     * 微信号ID
+     */
+    private String weixinId;
+
+    /**
+     * 客户ID
+     */
+    private String id;
+
+    /**
+     * CRM客户ID
+     */
+    private String customerId;
+
+    /**
+     * 客户名称
+     */
+    private String name;
+
+    /**
+     * 小程序用户ID
+     */
+    private String fsUserId;
+
+    /**
+     * 公司用户ID
+     */
+    private Long cuCompanyUserId;
+
+    /**
+     * 公司ID
+     */
+    private Long cuCompanyId;
+
+    /**
+     * 执行账号ID
+     */
+    private String accountId;
+
+    /**
+     * 企微用户ID
+     */
+    private String qwUserId;
+
+    /**
+     * 企业ID
+     */
+    private String corpId;
+}

+ 26 - 97
fs-service/src/main/java/com/fs/wx/cp/config/WxCpConfiguration.java

@@ -3,7 +3,6 @@ package com.fs.wx.cp.config;
 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;
@@ -11,14 +10,16 @@ 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
+@EnableConfigurationProperties(WxCpProperties.class)
 public class WxCpConfiguration {
     private LogHandler logHandler;
     private NullHandler nullHandler;
@@ -47,6 +48,7 @@ public class WxCpConfiguration {
         this.properties = properties;
     }
 
+
     public static Map<Integer, WxCpMessageRouter> getRouters() {
         return routers;
     }
@@ -55,91 +57,20 @@ 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("刷新企业微信服务配置");
-
-        // 刷新配置
-        properties.refresh();
-
-        // 清理旧的配置
-        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 = this.properties.getAppConfigs().stream().map(a -> {
             val configStorage = new WxCpDefaultConfigImpl();
-            configStorage.setCorpId(properties.getCorpId());
-            configStorage.setAgentId(appConfig.getAgentId());
-            configStorage.setCorpSecret(appConfig.getSecret());
-            configStorage.setToken(appConfig.getToken());
-            configStorage.setAesKey(appConfig.getAesKey());
-
-            service = new WxCpServiceImpl();
+            configStorage.setCorpId(this.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);
-
-            // 创建对应的路由器
-            WxCpMessageRouter router = this.newRouter(service);
-
-            // 保存到静态Map中
-            cpServices.put(agentId, service);
-            routers.put(agentId, router);
-
-            log.info("动态创建企业微信服务成功,agentId: {}", agentId);
-        }
-        return service;
+            routers.put(a.getAgentId(), this.newRouter(service));
+            return service;
+        }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
     }
 
     private WxCpMessageRouter newRouter(WxCpService wxCpService) {
@@ -150,42 +81,40 @@ 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();

+ 25 - 256
fs-service/src/main/java/com/fs/wx/cp/config/WxCpProperties.java

@@ -1,282 +1,51 @@
 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 com.fs.wx.utils.JsonUtils;
 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 org.springframework.boot.context.properties.ConfigurationProperties;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 @Data
-@Component
-@Slf4j
+@ConfigurationProperties(prefix = "wx.cp")
 public class WxCpProperties {
+  /**
+   * 设置企业微信的corpId
+   */
+  private String corpId;
 
-    @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<AppConfig> appConfigs;
 
+  @Getter
+  @Setter
+  public static class AppConfig {
     /**
-     * 公用方法:查询数据库获取企业微信配置
+     * 设置企业微信应用的AgentId
      */
-    private ProjectConfig.Wx.Cp getWxCpConfigFromDB() {
-        try {
-            // 1. 先从Redis缓存中获取
-            ProjectConfig cachedConfig = getConfigFromCache();
-            if (cachedConfig != null) {
-                log.debug("从Redis缓存中获取ProjectConfig配置");
-                if (cachedConfig.getWx() != null) {
-                    return cachedConfig.getWx().getCp();
-                }
-            }
-            ProjectConfig projectConfig = ProjectConfig.getFromDB(sysConfigMapper);
-            if (projectConfig == null) {
-                log.warn("未找到企业微信配置");
-                return null;
-            }
-
-            if (projectConfig.getWx() == null) {
-                log.warn("配置中未找到wx节点");
-                return null;
-            }
+    private Integer agentId;
 
-            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
+     * 设置企业微信应用的Secret
      */
-    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() : "";
-    }
+    private String secret;
 
     /**
-     * 获取所有应用配置(每次查询数据库)
+     * 设置企业微信应用的token
      */
-    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());
-    }
+    private String token;
 
     /**
-     * 检查配置是否完整(每次查询数据库)
+     * 设置企业微信应用的EncodingAESKey
      */
-    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;
+    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);
-        }
-    }
+  @Override
+  public String toString() {
+    return JsonUtils.toJson(this);
+  }
 }

+ 0 - 2
fs-service/src/main/java/com/fs/wx/kf/service/IWeixinKfService.java

@@ -3,8 +3,6 @@ package com.fs.wx.kf.service;
 import com.fs.wx.kf.dto.*;
 import com.fs.wx.kf.vo.*;
 
-import java.io.InputStream;
-
 public interface IWeixinKfService {
     WeixinKfTokenVO getToken(String corpid,String corpsecret);
     WeixinKfMsgVO getMsg(String corpid,String corpsecret,WeixinKfMsgDTO dto);

+ 0 - 3
fs-service/src/main/java/com/fs/wx/kf/service/impl/WeixinKfServiceImpl.java

@@ -5,9 +5,7 @@ import cn.hutool.http.ContentType;
 import cn.hutool.http.Header;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpUtil;
-import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
-import com.alibaba.fastjson.JSONArray;
 import com.fs.wx.kf.dto.*;
 import com.fs.wx.kf.service.IWeixinKfService;
 import com.fs.wx.kf.vo.*;
@@ -15,7 +13,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
 

+ 0 - 1
fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfAddEditAccountVO.java

@@ -3,7 +3,6 @@ package com.fs.wx.kf.vo;
 import lombok.Data;
 
 import java.io.Serializable;
-import java.util.List;
 
 @Data
 public class WeixinKfAddEditAccountVO implements Serializable {

+ 0 - 1
fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfMsgSendVO.java

@@ -3,7 +3,6 @@ package com.fs.wx.kf.vo;
 import lombok.Data;
 
 import java.io.Serializable;
-import java.util.List;
 
 @Data
 public class WeixinKfMsgSendVO implements Serializable {

+ 0 - 1
fs-service/src/main/java/com/fs/wx/kf/vo/WeixinKfUserItemVO.java

@@ -3,7 +3,6 @@ package com.fs.wx.kf.vo;
 import lombok.Data;
 
 import java.io.Serializable;
-import java.util.List;
 
 @Data
 public class WeixinKfUserItemVO implements Serializable {

+ 55 - 251
fs-service/src/main/java/com/fs/wx/miniapp/config/WxMaProperties.java

@@ -1,286 +1,90 @@
 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.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
+
+@ConfigurationProperties(prefix = "wx.miniapp")
 @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) {
-                if (cachedConfig.getWx() != null && cachedConfig.getWx().getMiniapp() != null) {
-                    List<ProjectConfig.Wx.Miniapp.Config> configs = cachedConfig.getWx().getMiniapp().getConfigs();
-                    return configs != null ? configs : new ArrayList<>();
-                }
-                return new ArrayList<>();
-            }
-
-            ProjectConfig projectConfig = ProjectConfig.getFromDB(sysConfigMapper);
-            if (projectConfig == null) {
-                return new ArrayList<>();
-            }
-
-            if (projectConfig.getWx() == null ||
-                    projectConfig.getWx().getMiniapp() == null) {
-                return new ArrayList<>();
-            }
+    private List<Config> configs;
 
-            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());
+        return configs;
     }
 
-    /**
-     * 获取第一个配置(适用于单个小程序)
-     */
-    public Config getFirstConfig() {
-        List<Config> configs = getConfigs();
-        if (configs.isEmpty()) {
-            throw new RuntimeException("未找到微信小程序配置");
-        }
-        return configs.get(0);
+    public void setConfigs(List<Config> configs) {
+        this.configs = configs;
     }
 
-    /**
-     * 根据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 static class Config {
+        /**
+         * 设置微信小程序的appid
+         */
+        private String appid;
 
-    /**
-     * 检查配置是否存在
-     */
-    public boolean isConfigValid() {
-        List<Config> configs = getConfigs();
-        return !configs.isEmpty();
-    }
+        /**
+         * 设置微信小程序的Secret
+         */
+        private String secret;
 
-    /**
-     * 获取配置摘要
-     */
-    public String getConfigSummary() {
-        List<Config> configs = getConfigs();
-        if (configs.isEmpty()) {
-            return "微信小程序配置为空";
-        }
+        /**
+         * 设置微信小程序消息服务器配置的token
+         */
+        private String token;
 
-        List<String> appIds = configs.stream()
-                .map(Config::getAppid)
-                .collect(Collectors.toList());
+        /**
+         * 设置微信小程序消息服务器配置的EncodingAESKey
+         */
+        private String aesKey;
 
-        return String.format("共 %d 个小程序: %s",
-                configs.size(), String.join(", ", appIds));
-    }
+        /**
+         * 消息格式,XML或者JSON
+         */
+        private String msgDataFormat;
 
-    /**
-     * 创建微信小程序服务
-     */
-    public WxMaService createWxMaService(String appid) {
-        Config config = getConfig(appid);
-        if (config == null) {
-            throw new RuntimeException("未找到appid为" + appid + "的配置");
+        public String getAppid() {
+            return 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 void setAppid(String appid) {
+            this.appid = appid;
+        }
 
-    public static class Config {
-        private String appid;
-        private String secret;
-        private String token;
-        private String aesKey;
-        private String msgDataFormat;
+        public String getSecret() {
+            return secret;
+        }
 
-        public String getAppid() { return appid; }
-        public void setAppid(String appid) { this.appid = appid; }
+        public void setSecret(String secret) {
+            this.secret = secret;
+        }
 
-        public String getSecret() { return secret; }
-        public void setSecret(String secret) { this.secret = secret; }
+        public String getToken() {
+            return token;
+        }
 
-        public String getToken() { return token; }
-        public void setToken(String token) { this.token = token; }
+        public void setToken(String token) {
+            this.token = token;
+        }
 
-        public String getAesKey() { return aesKey; }
-        public void setAesKey(String aesKey) { this.aesKey = aesKey; }
+        public String getAesKey() {
+            return aesKey;
+        }
 
-        public String getMsgDataFormat() { return msgDataFormat; }
-        public void setMsgDataFormat(String msgDataFormat) {
-            this.msgDataFormat = msgDataFormat;
+        public void setAesKey(String aesKey) {
+            this.aesKey = aesKey;
         }
 
-        /**
-         * 检查配置是否完整
-         */
-        public boolean isValid() {
-            return appid != null && !appid.isEmpty() &&
-                    secret != null && !secret.isEmpty();
+        public String getMsgDataFormat() {
+            return msgDataFormat;
         }
 
-        /**
-         * 获取配置摘要
-         */
-        public String getSummary() {
-            return String.format("appid: %s", appid);
+        public void setMsgDataFormat(String msgDataFormat) {
+            this.msgDataFormat = msgDataFormat;
         }
     }
+
 }

+ 25 - 0
fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java

@@ -114,4 +114,29 @@ public interface FsWxExpressTaskMapper {
             "</foreach>" +
             "</script>")
     void insertBatch(List<FsWxExpressTask> tasks);
+
+    /**
+     * 查询微信快递任务列表
+     * @param fsWxExpressTask 查询条件
+     * @return 任务列表
+     */
+    @Select("<script>" +
+            "SELECT * FROM fs_wx_express_task " +
+            "<where>" +
+            "  <if test='appid != null and appid != \"\"'> AND appid = #{appid}</if>" +
+            "  <if test='status != null'> AND status = #{status}</if>" +
+            "  <if test='orderCode != null and orderCode != \"\"'> AND order_code = #{orderCode}</if>" +
+            "</where>" +
+            " ORDER BY create_time DESC" +
+            "</script>")
+    List<FsWxExpressTask> selectFsWxExpressTaskList(FsWxExpressTask fsWxExpressTask);
+
+    /**
+     * 根据appid和status查询失败订单数量
+     * @param appid 小程序appId
+     * @param status 任务状态(3=执行失败)
+     * @return 失败订单数量
+     */
+    @Select("SELECT COUNT(*) FROM fs_wx_express_task WHERE appid = #{appid} AND status = #{status}")
+    int countFailedTasksByAppId(@Param("appid") String appid, @Param("status") Integer status);
 }

+ 29 - 0
fs-service/src/main/java/com/fs/wx/order/service/IFsWxExpressTaskService.java

@@ -0,0 +1,29 @@
+package com.fs.wx.order.service;
+
+import com.fs.wx.order.domain.FsWxExpressTask;
+
+import java.util.List;
+
+/**
+ * 微信快递任务Service接口
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+public interface IFsWxExpressTaskService {
+    /**
+     * 查询微信快递任务列表
+     *
+     * @param fsWxExpressTask 微信快递任务
+     * @return 微信快递任务集合
+     */
+    List<FsWxExpressTask> selectFsWxExpressTaskList(FsWxExpressTask fsWxExpressTask);
+
+    /**
+     * 根据appid查询失败订单数量
+     *
+     * @param appid 小程序appId
+     * @return 失败订单数量
+     */
+    int countFailedTasksByAppId(String appid);
+}

+ 23 - 1
fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java

@@ -24,6 +24,7 @@ public class ShippingService {
 
     private final WeChatApiConfig weChatApiConfig;
 
+
     public ShippingService(WeChatApiConfig weChatApiConfig) {
         this.weChatApiConfig = weChatApiConfig;
     }
@@ -83,8 +84,29 @@ public class ShippingService {
                 if (!weChatApiResponse.isSuccess()) {
                     log.warn("微信接口返回业务错误: code={}, message={}", weChatApiResponse.getErrcode(), weChatApiResponse.getErrmsg());
                     if(ObjectUtil.equal(weChatApiResponse.getErrcode(),40001)) {
+                        log.info("token缓存失效,强制刷新token并重试...");
                         accessToken = weChatAuthService.getAccessToken(true);
-                        log.info("token缓存失效,清除token,等待下次执行...");
+                        if (accessToken != null) {
+                            String retryUrl = UriComponentsBuilder.fromHttpUrl(weChatApiConfig.getUploadShippingInfoUrl())
+                                    .queryParam("access_token", accessToken)
+                                    .toUriString();
+                            try {
+                                HttpResponse retryResponse = HttpUtil.createPost(retryUrl)
+                                        .header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
+                                        .body(requestBodyJson)
+                                        .timeout(10000)
+                                        .execute();
+                                try {
+                                    WeChatApiResponse retryResult = JSONUtil.toBean(retryResponse.body(), WeChatApiResponse.class);
+                                    log.info("重试微信接口响应: errcode={}, errmsg={}", retryResult.getErrcode(), retryResult.getErrmsg());
+                                    return retryResult;
+                                } finally {
+                                    retryResponse.close();
+                                }
+                            } catch (Exception retryEx) {
+                                log.error("重试调用微信接口失败", retryEx);
+                            }
+                        }
                     }
                 }
                 return weChatApiResponse;

+ 0 - 3
fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthFactory.java

@@ -5,10 +5,7 @@ import com.fs.pay.mapper.PaymentMiniProgramConfigMapper;
 import com.fs.wx.order.service.impl.InMemoryWeChatAuthServiceImpl;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.SmartInitializingSingleton;
 import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.stereotype.Component;
 
 import java.util.List;
 import java.util.Map;

+ 43 - 0
fs-service/src/main/java/com/fs/wx/order/service/impl/FsWxExpressTaskServiceImpl.java

@@ -0,0 +1,43 @@
+package com.fs.wx.order.service.impl;
+
+import com.fs.wx.order.domain.FsWxExpressTask;
+import com.fs.wx.order.mapper.FsWxExpressTaskMapper;
+import com.fs.wx.order.service.IFsWxExpressTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 微信快递任务Service业务层处理
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Service
+public class FsWxExpressTaskServiceImpl implements IFsWxExpressTaskService {
+    @Autowired
+    private FsWxExpressTaskMapper fsWxExpressTaskMapper;
+
+    /**
+     * 查询微信快递任务列表
+     *
+     * @param fsWxExpressTask 微信快递任务
+     * @return 微信快递任务集合
+     */
+    @Override
+    public List<FsWxExpressTask> selectFsWxExpressTaskList(FsWxExpressTask fsWxExpressTask) {
+        return fsWxExpressTaskMapper.selectFsWxExpressTaskList(fsWxExpressTask);
+    }
+
+    /**
+     * 根据appid查询失败订单数量
+     *
+     * @param appid 小程序appId
+     * @return 失败订单数量
+     */
+    @Override
+    public int countFailedTasksByAppId(String appid) {
+        return fsWxExpressTaskMapper.countFailedTasksByAppId(appid, 3);
+    }
+}

+ 0 - 10
fs-service/src/main/java/com/fs/wx/order/service/impl/LiveExpressToWxService.java

@@ -2,26 +2,16 @@ package com.fs.wx.order.service.impl;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.fs.his.domain.FsStoreOrder;
-import com.fs.his.domain.FsStorePayment;
 import com.fs.his.domain.FsStoreProduct;
-import com.fs.his.mapper.FsStoreOrderMapper;
-import com.fs.his.mapper.FsStorePaymentMapper;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderPayment;
 import com.fs.live.mapper.LiveOrderMapper;
 import com.fs.live.mapper.LiveOrderPaymentMapper;
-import com.fs.live.service.ILiveOrderService;
 import com.fs.wx.order.service.ExpressToWxService;
-import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
-
 import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE;
 
 @Service

+ 2 - 6
fs-service/src/main/java/com/fs/wx/order/service/impl/ShopExpressToWxService.java

@@ -3,10 +3,6 @@ package com.fs.wx.order.service.impl;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.his.domain.FsStoreOrder;
-import com.fs.his.domain.FsStorePayment;
-import com.fs.his.mapper.FsStoreOrderMapper;
-import com.fs.his.mapper.FsStorePaymentMapper;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
@@ -65,13 +61,13 @@ public class ShopExpressToWxService extends ExpressToWxService {
     @Override
     public String getExpressCompany() {
 
-        return fsStoreOrder.getDeliveryCode();
+        return fsStoreOrder.getDeliverySn();
     }
 
     @Override
     public String getExpressNo() {
 
-        return fsStoreOrder.getDeliverySn();
+        return fsStoreOrder.getDeliveryId();
     }
 
     @Override

+ 76 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java

@@ -0,0 +1,76 @@
+package com.fs.wx.sop.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 个微SOP对象 wx_sop
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSop extends BaseEntityTow {
+
+    /** 名称 */
+    @Excel(name = "名称")
+    private String name;
+
+    /** 筛选方式(0标签1群聊) */
+    @Excel(name = "筛选方式(0标签1群聊)")
+    private Integer filterType;
+
+    /** 选择的标签 */
+    @Excel(name = "选择的标签")
+    private String selectTags;
+
+    /** 排查的标签 */
+    @Excel(name = "排查的标签")
+    private String excludeTags;
+
+    /** 模板ID */
+    @Excel(name = "模板ID")
+    private String tempId;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 是否固定营期(0否1是) */
+    @Excel(name = "是否固定营期(0否1是)")
+    private Integer isFixed;
+
+    /** 过期时间(小时) */
+    @Excel(name = "过期时间(小时)")
+    private Integer expiryTime;
+
+    /** 营期开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期开始时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDate startTime;
+
+    /** 执行账号ID,逗号分隔(入库) */
+    @Excel(name = "执行账号ID")
+    private String accountIds;
+
+    /** 执行账号列表(不入库,用于编辑回显) */
+    @TableField(exist = false)
+    private List<Map<String, Object>> selectedQwUsers;
+
+    /** 状态(0停止 1启用 2执行中) */
+    @Excel(name = "状态(0停止 1启用 2执行中)")
+    private Long status;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+}

+ 92 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java

@@ -0,0 +1,92 @@
+package com.fs.wx.sop.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 个微发送记录对象 wx_sop_logs
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSopLogs extends BaseEntityTow {
+
+    /** 消息类型0个人1群 */
+    @Excel(name = "消息类型0个人1群")
+    private Integer type;
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 营期ID */
+    @Excel(name = "营期ID")
+    private Long sopUserId;
+
+    /** 发送类型(字典-wx_send_type) */
+    @Excel(name = "发送类型", readConverterExp = "字=典-wx_send_type")
+    private Integer sendType;
+
+    private LocalDateTime sendTime;
+
+    private String contentJson;
+
+    /** 生成类型(0自动1手动) */
+    @Excel(name = "生成类型(0自动1手动)")
+    private Integer generateType;
+
+    /** 发送账号ID */
+    @Excel(name = "发送账号ID")
+    private Long accountId;
+
+    /** 发送对象ID */
+    @Excel(name = "发送对象ID")
+    private Long wxContactId;
+
+    /** 发送对象名称 */
+    @Excel(name = "发送对象名称")
+    private String wxContactName;
+
+    /** 发送群聊ID */
+    @Excel(name = "发送群聊ID")
+    private Long wxRoomId;
+
+    /** 发送群聊名称 */
+    @Excel(name = "发送群聊名称")
+    private String wxRoomName;
+
+    /** 小程序ID */
+    @Excel(name = "小程序ID")
+    private Long fsUserId;
+
+    /** 发送状态0待发送1发送成功2发送失败3消息作废 */
+    @Excel(name = "发送状态0待发送1发送成功2发送失败3消息作废")
+    private Integer sendStatus;
+
+    /** 发送备注 */
+    @Excel(name = "发送备注")
+    private String sendRemark;
+
+    /** 发送排序 */
+    @Excel(name = "发送排序")
+    private Integer sendSort;
+
+    /** 消息过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "消息过期时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDateTime expirationTime;
+    @TableField(exist = false)
+    private boolean send;
+    @TableField(exist = false)
+    private String wxRemark;
+
+
+}

+ 53 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java

@@ -0,0 +1,53 @@
+package com.fs.wx.sop.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+/**
+ * 个微营期对象 wx_sop_user
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("wx_sop_user")
+public class WxSopUser extends BaseEntityTow {
+
+    /** 类型(0个人1群聊) */
+    @Excel(name = "类型(0个人1群聊)")
+    private Integer type;
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 个微账号ID */
+    @Excel(name = "个微账号ID")
+    private Long accountId;
+
+    /** 个微账号名称 */
+    @Excel(name = "个微账号名称")
+    private String accountName;
+
+    /** 营期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDate startTime;
+
+    /** 群聊ID */
+    @Excel(name = "群聊ID")
+    private String chatId;
+
+    /** 状态(0正常1暂停) */
+    @Excel(name = "状态", readConverterExp = "0=正常1暂停")
+    private Integer status;
+
+
+}

+ 72 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java

@@ -0,0 +1,72 @@
+package com.fs.wx.sop.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 个微营期详情对象 wx_sop_user_info
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("wx_sop_user_info")
+public class WxSopUserInfo extends BaseEntityTow {
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 营期ID */
+    @Excel(name = "营期ID")
+    private Long sopUserId;
+
+    /** 联系人ID */
+    @Excel(name = "联系人ID")
+    private Long wxContactId;
+
+    /** 客户ID */
+    @Excel(name = "客户ID")
+    private Long customerId;
+
+    /** 小程序ID */
+    @Excel(name = "小程序ID")
+    private Long fsUserId;
+
+    /** 是否7天都没有看课 0否 1是 */
+    @Excel(name = "是否7天都没有看课 0否 1是")
+    private Integer isDaysNotStudy;
+
+    /** 总完课天数 */
+    @Excel(name = "总完课天数")
+    private Integer finishCout;
+
+    /** 最近完课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "最近完课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDateTime finishTime;
+
+    /** 连续完课天数 */
+    @Excel(name = "连续完课天数")
+    private Integer finishCourseDays;
+
+    /** 客户评级的等级 */
+    @Excel(name = "客户评级的等级")
+    private Integer grade;
+
+    /** 禁用状态 0 正常 1禁用 */
+    @Excel(name = "禁用状态 0 正常 1禁用")
+    private Integer status;
+
+    /** 客户标签名称 */
+    private String tagNames;
+
+
+}

+ 88 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -0,0 +1,88 @@
+package com.fs.wx.sop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 个微发送记录Mapper接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
+    /**
+     * 查询个微发送记录
+     *
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    @DataSource(DataSourceType.SOP)
+    WxSopLogs selectWxSopLogsById(Long id);
+
+    /**
+     * 查询个微发送记录列表
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
+
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
+
+    /**
+     * 新增个微发送记录
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int insertWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 修改个微发送记录
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int updateWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 删除个微发送记录
+     *
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteWxSopLogsById(Long id);
+
+    /**
+     * 批量删除个微发送记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteWxSopLogsByIds(Long[] ids);
+
+    @DataSource(DataSourceType.SOP)
+    void batchInsertWxSopLogs(List<WxSopLogs> logsToInsert);
+
+    @DataSource(DataSourceType.SOP)
+    List<WxSopLogs> selectByWxId(@Param("id") Long id);
+}

+ 94 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java

@@ -0,0 +1,94 @@
+package com.fs.wx.sop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.sop.params.WxSopTagsParam;
+import com.fs.sop.vo.WxFilterSopCustomersResult;
+import com.fs.wx.sop.domain.WxSop;
+
+import java.util.List;
+
+/**
+ * 个微SOPMapper接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopMapper extends BaseMapper<WxSop>{
+    /**
+     * 查询个微SOP
+     *
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    @DataSource(DataSourceType.SOP)
+    WxSop selectWxSopById(Long id);
+
+    /**
+     * 查询个微SOP列表
+     *
+     * @param wxSop 个微SOP
+     * @return 个微SOP集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSop> selectWxSopList(WxSop wxSop);
+
+    /**
+     * 新增个微SOP
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int insertWxSop(WxSop wxSop);
+
+    /**
+     * 修改个微SOP
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int updateWxSop(WxSop wxSop);
+
+    /**
+     * 删除个微SOP
+     *
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    int deleteWxSopById(Long id);
+
+    /**
+     * 批量删除个微SOP
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopByIds(Long[] ids);
+
+    /**
+     * 根据ID数组查询个微SOP列表
+     *
+     * @param ids 个微SOP主键数组
+     * @return 个微SOP集合
+     */
+    List<WxSop> selectWxSopByIds(Long[] ids);
+
+    /**
+     * 批量更新个微SOP状态
+     *
+     * @param ids 个微SOP主键数组
+     * @param status 状态
+     * @return 结果
+     */
+    int updateStatusWxSopByIds(Long[] ids, Long status);
+
+    /**
+     * 根据筛选条件查询符合条件的客户
+     *
+     * @param param 筛选参数
+     * @return 客户结果列表
+     */
+    @DataSource(DataSourceType.MASTER)
+    List<WxFilterSopCustomersResult> selectFilterWxSopCustomers(WxSopTagsParam param);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java

@@ -0,0 +1,79 @@
+package com.fs.wx.sop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+
+import java.util.List;
+
+/**
+ * 个微营期详情Mapper接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
+    /**
+     * 查询个微营期详情
+     *
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    @DataSource(DataSourceType.SOP)
+    WxSopUserInfo selectWxSopUserInfoById(Long id);
+
+    /**
+     * 查询个微营期详情列表
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 新增个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 修改个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 删除个微营期详情
+     *
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteWxSopUserInfoById(Long id);
+
+    /**
+     * 批量删除个微营期详情
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    @DataSource(DataSourceType.SOP)
+    int deleteWxSopUserInfoByIds(Long[] ids);
+
+    /**
+     * 根据条件查询单个营期成员记录
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情
+     */
+    @DataSource(DataSourceType.SOP)
+    WxSopUserInfo selectWxSopUserInfoByCondition(WxSopUserInfo wxSopUserInfo);
+}

+ 91 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -0,0 +1,91 @@
+package com.fs.wx.sop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.vo.WxSopUserMsgGenVO;
+
+import java.util.List;
+
+/**
+ * 个微营期Mapper接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@DataSource(DataSourceType.SOP)
+public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
+    /**
+     * 查询个微营期
+     *
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    WxSopUser selectWxSopUserById(Long id);
+
+    /**
+     * 查询个微营期列表
+     *
+     * @param wxSopUser 个微营期
+     * @return 个微营期集合
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
+
+    /**
+     * 新增个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int insertWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 修改个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int updateWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 删除个微营期
+     *
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    int deleteWxSopUserById(Long id);
+
+    /**
+     * 批量删除个微营期
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserByIds(Long[] ids);
+
+    /**
+     * 根据SOP ID删除执行账号
+     *
+     * @param sopId SOP主键
+     * @return 结果
+     */
+    int deleteBySopId(Long sopId);
+
+    /**
+     * 查询营期记录(根据条件)
+     *
+     * @param wxSopUser 查询条件
+     * @return 营期记录
+     */
+    WxSopUser selectwxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 查询活跃的个微SOP营期及客户信息(用于消息生成)
+     *
+     * @return 营期客户信息列表
+     */
+    @DataSource(DataSourceType.SOP)
+    List<WxSopUserMsgGenVO> selectActiveWxSopUserForMsgGen();
+}

+ 13 - 0
fs-service/src/main/java/com/fs/wx/sop/params/SendWxSopMsgParam.java

@@ -0,0 +1,13 @@
+package com.fs.wx.sop.params;
+
+import lombok.Data;
+
+@Data
+public class SendWxSopMsgParam {
+    /** 选中的SOP ID数组 */
+    private Long[] sopIds;
+    /** 消息内容JSON,格式: [{"contentType":"1","value":"文本内容"}] */
+    private String setting;
+    /** 发送时间 HH:mm 格式,不填默认立即发送 */
+    private String sendTime;
+}

+ 57 - 0
fs-service/src/main/java/com/fs/wx/sop/params/WxSopLogsParam.java

@@ -0,0 +1,57 @@
+package com.fs.wx.sop.params;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 个微SOP执行记录查询参数
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class WxSopLogsParam {
+    
+    /** SOP ID */
+    private Long sopId;
+
+    /** 个微账号昵称(用于模糊搜索) */
+    private String accountName;
+
+    /** 个微账号ID */
+    private Long accountId;
+
+    /** 个微账号ID列表 */
+    private List<Long> accountIdList;
+
+    /** 客户昵称(用于模糊搜索) */
+    private String wxContactName;
+
+    /** 客户ID */
+    private Long wxContactId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 发送状态 0待发送1发送成功2发送失败3消息作废 */
+    private Integer sendStatus;
+
+    /** 发送类型 */
+    private Integer sendType;
+
+    /** 消息类型 0个人1群 */
+    private Integer type;
+
+    /** 营期ID */
+    private Long sopUserId;
+
+    /** 预计发送开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String scheduleStartTime;
+
+    /** 预计发送结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String scheduleEndTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopExecuteService.java

@@ -0,0 +1,42 @@
+package com.fs.wx.sop.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.wx.sop.domain.WxSop;
+
+/**
+ * 个微SOP执行服务接口
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+public interface IWxSopExecuteService {
+    /**
+     * 根据标签筛选客户并创建营期
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R processTagFilterWxSop(WxSop wxSop);
+
+    /**
+     * 根据群聊筛选客户并创建营期
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R processGroupFilterWxSop(WxSop wxSop);
+
+    /**
+     * 为客户创建营期记录
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R createSopUserLogsWx(WxSop wxSop);
+
+    /**
+     * 处理客户标签变更
+     *
+     */
+    void processCustomerTagsChange(Long customerId);
+}

+ 86 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java

@@ -0,0 +1,86 @@
+package com.fs.wx.sop.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
+
+import java.util.List;
+
+/**
+ * 个微发送记录Service接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopLogsService extends IService<WxSopLogs>{
+    /**
+     * 查询个微发送记录
+     *
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    WxSopLogs selectWxSopLogsById(Long id);
+
+    /**
+     * 查询个微发送记录列表
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录集合
+     */
+    List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
+
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
+
+    /**
+     * 新增个微发送记录
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int insertWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 修改个微发送记录
+     *
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int updateWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 批量删除个微发送记录
+     *
+     * @param ids 需要删除的个微发送记录主键集合
+     * @return 结果
+     */
+    int deleteWxSopLogsByIds(Long[] ids);
+
+    /**
+     * 删除个微发送记录信息
+     *
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    int deleteWxSopLogsById(Long id);
+
+    void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert);
+
+    /**
+     * 个微SOP一键群发
+     *
+     * @param param 群发参数
+     * @return 结果
+     */
+    R sendWxSopMsg(SendWxSopMsgParam param);
+
+    boolean updateMapper(WxSopLogs updateQwSop);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java

@@ -0,0 +1,79 @@
+package com.fs.wx.sop.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.wx.sop.domain.WxSop;
+
+import java.util.List;
+
+/**
+ * 个微SOPService接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopService extends IService<WxSop>{
+    /**
+     * 查询个微SOP
+     *
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    WxSop selectWxSopById(Long id);
+
+    /**
+     * 查询个微SOP详情(含执行账号 companyUserIds)
+     *
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    WxSop selectWxSopDetailById(Long id);
+
+    /**
+     * 查询个微SOP列表
+     *
+     * @param wxSop 个微SOP
+     * @return 个微SOP集合
+     */
+    List<WxSop> selectWxSopList(WxSop wxSop);
+
+    /**
+     * 新增个微SOP
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int insertWxSop(WxSop wxSop);
+
+    /**
+     * 修改个微SOP
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int updateWxSop(WxSop wxSop);
+
+    /**
+     * 批量删除个微SOP
+     *
+     * @param ids 需要删除的个微SOP主键集合
+     * @return 结果
+     */
+    int deleteWxSopByIds(Long[] ids);
+
+    /**
+     * 删除个微SOP信息
+     *
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    int deleteWxSopById(Long id);
+
+    /**
+     * 批量执行个微SOP
+     *
+     * @param ids 个微SOP主键数组
+     * @return 结果
+     */
+    R updateStatusWxSopByIds(Long[] ids);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserInfoService.java

@@ -0,0 +1,62 @@
+package com.fs.wx.sop.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+
+import java.util.List;
+
+/**
+ * 个微营期详情Service接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopUserInfoService extends IService<WxSopUserInfo>{
+    /**
+     * 查询个微营期详情
+     *
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    WxSopUserInfo selectWxSopUserInfoById(Long id);
+
+    /**
+     * 查询个微营期详情列表
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情集合
+     */
+    List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 新增个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 修改个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 批量删除个微营期详情
+     *
+     * @param ids 需要删除的个微营期详情主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserInfoByIds(Long[] ids);
+
+    /**
+     * 删除个微营期详情信息
+     *
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    int deleteWxSopUserInfoById(Long id);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java

@@ -0,0 +1,62 @@
+package com.fs.wx.sop.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSopUser;
+
+import java.util.List;
+
+/**
+ * 个微营期Service接口
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopUserService extends IService<WxSopUser>{
+    /**
+     * 查询个微营期
+     *
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    WxSopUser selectWxSopUserById(Long id);
+
+    /**
+     * 查询个微营期列表
+     *
+     * @param wxSopUser 个微营期
+     * @return 个微营期集合
+     */
+    List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
+
+    /**
+     * 新增个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int insertWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 修改个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int updateWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 批量删除个微营期
+     *
+     * @param ids 需要删除的个微营期主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserByIds(Long[] ids);
+
+    /**
+     * 删除个微营期信息
+     *
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    int deleteWxSopUserById(Long id);
+}

+ 555 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java

@@ -0,0 +1,555 @@
+package com.fs.wx.sop.service.impl;
+
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.DataSourceType;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.sop.params.WxSopTagsParam;
+import com.fs.sop.vo.WxFilterSopCustomersResult;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.mapper.WxSopMapper;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.service.IWxSopExecuteService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 个微SOP执行服务实现类
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Service
+@Slf4j
+public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
+
+    @Autowired
+    private WxSopMapper wxSopMapper;
+
+    @Autowired
+    private WxSopUserMapper wxSopUserMapper;
+
+    @Autowired
+    private WxSopUserInfoMapper wxSopUserInfoMapper;
+
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
+    @Autowired
+    private ISysDictDataService sysDictDataService;
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R processTagFilterWxSop(WxSop wxSop) {
+        try {
+            log.info("====== 开始执行标签筛选SOP ======");
+            log.info("SOP ID: {}, 名称: {}, 筛选标签: {}, 执行账号: {}",
+                    wxSop.getId(), wxSop.getName(), wxSop.getSelectTags(), wxSop.getAccountIds());
+
+            // 构建标签筛选参数
+            WxSopTagsParam wxSopTagsParam = buildWxSopTagsParam(wxSop);
+            log.info("筛选参数: accountIds={}, tags={}, excludeTags={}",
+                    wxSopTagsParam.getAccountIdsSelectList(),
+                    wxSopTagsParam.getTagsIdsSelectList(),
+                    wxSopTagsParam.getOutTagsIdsSelectList());
+
+            // 查询符合条件的客户
+            List<WxFilterSopCustomersResult> customerResults = selectFilterWxSopCustomers(wxSopTagsParam);
+            log.info("查询到的客户数量: {}", customerResults != null ? customerResults.size() : 0);
+
+            if (customerResults != null && !customerResults.isEmpty()) {
+                for (WxFilterSopCustomersResult customer : customerResults) {
+                    log.info("符合条件的客户: ID={}, 名称={}, 账号ID={}",
+                            customer.getId(), customer.getName(), customer.getAccountId());
+                }
+            }
+
+            if (customerResults == null || customerResults.isEmpty()) {
+                log.warn("未找到符合条件的客户,SOP ID: {}", wxSop.getId());
+                return R.error("未找到符合条件的客户");
+            }
+
+            // 为客户创建营期记录
+            createSopUserLogsWxForCustomers(wxSop, customerResults);
+
+            log.info("标签筛选SOP处理完成,SOP ID: {},客户数量: {}", wxSop.getId(), customerResults.size());
+            return R.ok("标签筛选SOP处理完成");
+        } catch (Exception e) {
+            log.error("处理标签筛选SOP时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("处理过程中发生异常: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R processGroupFilterWxSop(WxSop wxSop) {
+        try {
+            // 群聊筛选逻辑
+            // 这里需要根据群聊ID或其他群聊相关信息筛选客户
+            // 暂时留空实现,需要根据具体业务需求完善
+
+            log.info("群聊筛选SOP处理完成,SOP ID: {}", wxSop.getId());
+            return R.ok("群聊筛选SOP处理完成");
+        } catch (Exception e) {
+            log.error("处理群聊筛选SOP时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("处理过程中发生异常: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R createSopUserLogsWx(WxSop wxSop) {
+        try {
+            // 根据SOP的筛选方式进行不同的客户筛选
+            if (wxSop.getFilterType() != null && wxSop.getFilterType() == 0) { // 0: 标签筛选
+                return processTagFilterWxSop(wxSop);
+            } else if (wxSop.getFilterType() != null && wxSop.getFilterType() == 1) { // 1: 群聊筛选
+                return processGroupFilterWxSop(wxSop);
+            } else {
+                return R.error("未知的筛选方式");
+            }
+        } catch (Exception e) {
+            log.error("创建SOP营期记录时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("创建营期记录时发生异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 构建标签筛选参数
+     *
+     * @param wxSop 个微SOP
+     * @return 标签筛选参数
+     */
+    private WxSopTagsParam buildWxSopTagsParam(WxSop wxSop) {
+        WxSopTagsParam param = new WxSopTagsParam();
+
+        // 设置执行账号ID列表
+        if (wxSop.getAccountIds() != null && !wxSop.getAccountIds().isEmpty()) {
+            String[] accountIds = wxSop.getAccountIds().split(",");
+            List<String> accountIdsList = new ArrayList<>();
+            for (String accountId : accountIds) {
+                if (accountId != null && !accountId.trim().isEmpty()) {
+                    accountIdsList.add(accountId.trim());
+                }
+            }
+            param.setAccountIdsSelectList(accountIdsList);
+        }
+
+        // 设置筛选标签
+        if (wxSop.getSelectTags() != null && !wxSop.getSelectTags().isEmpty()) {
+            String[] tags = wxSop.getSelectTags().split(",");
+            List<String> tagsList = new ArrayList<>();
+            for (String tag : tags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    tagsList.add(tag.trim());
+                }
+            }
+            param.setTagsIdsSelectList(tagsList);
+        }
+
+        // 设置排除标签
+        if (wxSop.getExcludeTags() != null && !wxSop.getExcludeTags().isEmpty()) {
+            String[] excludeTags = wxSop.getExcludeTags().split(",");
+            List<String> excludeTagsList = new ArrayList<>();
+            for (String excludeTag : excludeTags) {
+                if (excludeTag != null && !excludeTag.trim().isEmpty()) {
+                    excludeTagsList.add(excludeTag.trim());
+                }
+            }
+            param.setOutTagsIdsSelectList(excludeTagsList);
+        }
+
+        // 设置筛选类型
+        param.setFilterType(wxSop.getFilterType());
+
+        return param;
+    }
+
+    /**
+     * 查询符合条件的客户
+     *
+     * @param param 标签筛选参数
+     * @return 客户结果列表
+     */
+    private List<WxFilterSopCustomersResult> selectFilterWxSopCustomers(WxSopTagsParam param) {
+        log.info("开始查询符合条件的客户...");
+        List<WxFilterSopCustomersResult> results = wxSopMapper.selectFilterWxSopCustomers(param);
+        log.info("客户查询完成,结果数: {}", results != null ? results.size() : 0);
+        return results;
+    }
+
+    /**
+     * 为客户创建营期记录
+     *
+     * @param wxSop 个微SOP
+     * @param customerResults 客户结果列表
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @DataSource(DataSourceType.SOP)
+    public void createSopUserLogsWxForCustomers(WxSop wxSop, List<WxFilterSopCustomersResult> customerResults) {
+        log.info("====== 开始为 {} 个客户创建营期记录 ======", customerResults.size());
+        // 按执行账号分组创建营期
+        for (WxFilterSopCustomersResult customer : customerResults) {
+            try {
+                log.info("处理客户: ID={}, 名称={}, 账号ID={}",
+                        customer.getId(), customer.getName(), customer.getAccountId());
+                Long sopUserId = getOrCreateWxSopUser(wxSop, customer);
+
+                if (sopUserId == null) {
+                    log.warn("创建营期失败,跳过客户:{}", customer.getId());
+                    continue;
+                }
+
+                log.info("营期ID: {}", sopUserId);
+
+                // 2. 检查客户是否已在该营期中
+                WxSopUserInfo queryParam = new WxSopUserInfo();
+                queryParam.setSopId(wxSop.getId());
+                queryParam.setSopUserId(sopUserId);
+                queryParam.setWxContactId(Long.parseLong(customer.getId()));
+
+                WxSopUserInfo existingInfo = wxSopUserInfoMapper.selectWxSopUserInfoByCondition(queryParam);
+                if (existingInfo != null) {
+                    log.info("客户已在营期中,跳过:客户ID={}, 营期ID={}", customer.getId(), sopUserId);
+                    continue;
+                }
+
+                // 3. 创建营期成员记录(wx_sop_user_info)
+                WxSopUserInfo wxSopUserInfo = new WxSopUserInfo();
+                wxSopUserInfo.setSopId(wxSop.getId());
+                wxSopUserInfo.setSopUserId(sopUserId);
+                wxSopUserInfo.setWxContactId(Long.parseLong(customer.getId()));
+                if (customer.getCustomerId() != null && !customer.getCustomerId().isEmpty()) {
+                    try {
+                        wxSopUserInfo.setCustomerId(Long.parseLong(customer.getCustomerId()));
+                    } catch (NumberFormatException e) {
+                        log.warn("客户ID格式错误:{}", customer.getCustomerId());
+                    }
+                }
+
+                // 设置小程序ID(如果有)
+                if (customer.getFsUserId() != null && !customer.getFsUserId().isEmpty()) {
+                    try {
+                        wxSopUserInfo.setFsUserId(Long.parseLong(customer.getFsUserId()));
+                    } catch (NumberFormatException e) {
+                        log.warn("小程序ID格式错误:{}", customer.getFsUserId());
+                    }
+                }
+
+                wxSopUserInfo.setStatus(0); // 正常状态
+
+                int result = wxSopUserInfoMapper.insertWxSopUserInfo(wxSopUserInfo);
+                log.info("成功添加客户到营期:客户ID={}, 营期ID={}, SOP ID={}, 插入结果={}",
+                        customer.getId(), sopUserId, wxSop.getId(), result);
+
+            } catch (Exception e) {
+                log.error("为客户创建营期记录失败,客户ID: {}", customer.getId(), e);
+                // 继续处理其他客户
+            }
+        }
+        log.info("====== 营期记录创建完成 ======");
+    }
+
+    /**
+     * 获取或创建营期主表记录
+     *
+     * @param wxSop 个微SOP
+     * @param customer 客户信息
+     * @return 营期ID
+     */
+    private Long getOrCreateWxSopUser(WxSop wxSop, WxFilterSopCustomersResult customer) {
+        try {
+            // 解析账号ID
+            Long accountId = null;
+            if (customer.getAccountId() != null && !customer.getAccountId().isEmpty()) {
+                accountId = Long.parseLong(customer.getAccountId());
+            } else {
+                log.warn("客户账号ID为空,客户ID: {}", customer.getId());
+                return null;
+            }
+
+            // 确定营期时间
+            LocalDate startTime;
+            if (wxSop.getIsFixed() != null && wxSop.getIsFixed() == 1) {
+                // 固定营期:使用SOP的开始时间
+                startTime = wxSop.getStartTime();
+            } else {
+                // 非固定营期:使用今天
+                startTime = LocalDate.now();
+            }
+
+            // 查询是否已存在营期
+            WxSopUser queryParam = new WxSopUser();
+            queryParam.setSopId(wxSop.getId());
+            queryParam.setType(0); // 个人类型
+            queryParam.setAccountId(accountId);
+            queryParam.setStartTime(startTime);
+
+            WxSopUser existingUser = wxSopUserMapper.selectwxSopUser(queryParam);
+            if (existingUser != null) {
+                return existingUser.getId();
+            }
+
+            // 创建新的营期记录
+            WxSopUser wxSopUser = new WxSopUser();
+            wxSopUser.setType(0); // 个人类型
+            wxSopUser.setSopId(wxSop.getId());
+            wxSopUser.setAccountId(accountId);
+            wxSopUser.setStartTime(startTime);
+            wxSopUser.setStatus(0); // 正常状态
+
+            int result = wxSopUserMapper.insertWxSopUser(wxSopUser);
+            if (result > 0) {
+                log.info("创建营期成功:SOP ID={}, 账号ID={}, 营期时间={}, 营期ID={}",
+                        wxSop.getId(), accountId, startTime, wxSopUser.getId());
+                return wxSopUser.getId();
+            } else {
+                log.error("创建营期失败:SOP ID={}, 账号ID={}", wxSop.getId(), accountId);
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("获取或创建营期失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 处理客户标签变更后的SOP营期动态管理
+     * 根据客户最新标签,自动加入或移出符合条件的营期
+     *
+     * @param customerId 客户ID
+     */
+    @Override
+    public void processCustomerTagsChange(Long customerId) {
+        try {
+            log.info("====== 开始处理客户标签变更,客户ID: {} ======", customerId);
+
+            CrmCustomer customer = crmCustomerMapper.selectCrmCustomerById(customerId);
+            if (customer == null) {
+                log.warn("客户不存在,客户ID: {}", customerId);
+                return;
+            }
+            log.info("客户信息: ID={}, 名称={}, 标签={}", customerId, customer.getCustomerName(), customer.getTags());
+
+            WxSop sopQuery = new WxSop();
+            sopQuery.setStatus(2L);
+            sopQuery.setFilterType(0);
+            List<WxSop> enabledSops = wxSopMapper.selectWxSopList(sopQuery);
+            log.info("查询到 {} 个启用的标签筛选SOP", enabledSops != null ? enabledSops.size() : 0);
+
+            if (enabledSops == null || enabledSops.isEmpty()) {
+                log.info("没有启用的标签筛选SOP,无需处理");
+                return;
+            }
+
+            for (WxSop sop : enabledSops) {
+                processCustomerForSop(customer, sop);
+            }
+
+            log.info("====== 客户标签变更处理完成,客户ID: {} ======", customerId);
+        } catch (Exception e) {
+            log.error("处理客户标签变更时发生异常,客户ID: {}", customerId, e);
+            throw e;
+        }
+    }
+
+
+
+
+    /**
+     * 处理单个客户在单个SOP中的营期管理
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     */
+    public void processCustomerForSop(CrmCustomer customer, WxSop sop) {
+        try {
+            log.info("检查客户是否符合SOP条件 - 客户ID: {}, SOP ID: {}, SOP名称: {}",
+                    customer.getCustomerId(), sop.getId(), sop.getName());
+
+            boolean isMatch = checkCustomerMatchSopTags(customer, sop);
+            log.info("客户是否符合SOP条件: {}", isMatch);
+
+
+            List<WxSopUserInfo> existingInfos = querySopUserInfosByCustomer(sop.getId(), customer.getCustomerId());
+
+            if (isMatch) {
+                // 符合条件:如果不在营期中,则加入
+                if (existingInfos == null || existingInfos.isEmpty()) {
+                    log.info("客户符合条件且不在营期中,准备加入营期");
+                    addCustomerToSop(customer, sop);
+                } else {
+                    log.info("客户已在营期中,无需重复加入");
+                }
+            } else {
+                // 不符合条件:如果在营期中,则移出
+                if (existingInfos != null && !existingInfos.isEmpty()) {
+                    log.info("客户不符合条件但在营期中,准备移出营期,记录数: {}", existingInfos.size());
+                    removeCustomerFromSop(customer, sop, existingInfos);
+                } else {
+                    log.info("客户不符合条件且不在营期中,无需处理");
+                }
+            }
+        } catch (Exception e) {
+            log.error("处理客户在SOP中的营期管理时发生异常,客户ID: {}, SOP ID: {}",
+                    customer.getCustomerId(), sop.getId(), e);
+        }
+    }
+
+    /**
+     * 查询客户在指定SOP下的所有营期成员记录
+     *
+     * @param sopId SOP ID
+     * @param customerId 客户ID
+     * @return 营期成员记录列表
+     */
+    public List<WxSopUserInfo> querySopUserInfosByCustomer(Long sopId, Long customerId) {
+        WxSopUserInfo queryParam = new WxSopUserInfo();
+        queryParam.setSopId(sopId);
+        queryParam.setCustomerId(customerId);
+        return wxSopUserInfoMapper.selectWxSopUserInfoList(queryParam);
+    }
+
+    /**
+     * 检查客户是否符合SOP的标签筛选条件
+     * 注意:客户tags字段与SOP的selectTags/excludeTags统一使用标签值(label)进行比对
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     * @return true-符合条件,false-不符合条件
+     */
+    private boolean checkCustomerMatchSopTags(CrmCustomer customer, WxSop sop) {
+        String customerTags = customer.getTags();
+        if (customerTags == null || customerTags.trim().isEmpty()) {
+            log.info("客户标签为空");
+            // 如果客户没有标签,但SOP要求有筛选标签,则不符合
+            return sop.getSelectTags() == null || sop.getSelectTags().trim().isEmpty();
+        }
+
+        // 客户的标签列表(逗号分隔的标签值)- 需要将dict_label转换为dict_value
+        String[] customerTagArray = customerTags.split(",");
+        List<String> customerTagIdList = new ArrayList<>();
+        for (String tagLabel : customerTagArray) {
+            if (tagLabel != null && !tagLabel.trim().isEmpty()) {
+                // 将标签名(dict_label)转换为标签ID(dict_value)
+                String tagId = com.fs.common.utils.DictUtils.getDictValue("crm_customer_tag", tagLabel.trim());
+                if (tagId != null && !tagId.isEmpty()) {
+                    customerTagIdList.add(tagId);
+                }
+            }
+        }
+        log.info("客户标签ID列表: {}", customerTagIdList);
+
+        // 检查必须包含的标签(selectTags)- 直接使用标签ID比对
+        if (sop.getSelectTags() != null && !sop.getSelectTags().trim().isEmpty()) {
+            String[] selectTags = sop.getSelectTags().split(",");
+            for (String tag : selectTags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    String trimmedTag = tag.trim();
+                    if (!customerTagIdList.contains(trimmedTag)) {
+                        log.info("客户缺少必需标签ID: {}", trimmedTag);
+                        return false;
+                    }
+                }
+            }
+            log.info("客户包含所有必需标签");
+        }
+
+        // 检查排除的标签(excludeTags)- 直接使用标签ID比对
+        if (sop.getExcludeTags() != null && !sop.getExcludeTags().trim().isEmpty()) {
+            String[] excludeTags = sop.getExcludeTags().split(",");
+            for (String tag : excludeTags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    String trimmedTag = tag.trim();
+                    if (customerTagIdList.contains(trimmedTag)) {
+                        log.info("客户包含排除标签ID: {}", trimmedTag);
+                        return false;
+                    }
+                }
+            }
+            log.info("客户不包含排除标签");
+        }
+
+        log.info("客户符合SOP标签筛选条件");
+        return true;
+    }
+
+    /**
+     * 将客户加入SOP营期
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     */
+    @DataSource(DataSourceType.SOP)
+    public void addCustomerToSop(CrmCustomer customer, WxSop sop) {
+        try {
+            WxFilterSopCustomersResult customerResult = new WxFilterSopCustomersResult();
+            customerResult.setId(customer.getCustomerId().toString());
+            customerResult.setName(customer.getCustomerName());
+            if (sop.getAccountIds() != null && !sop.getAccountIds().isEmpty()) {
+                String[] accountIds = sop.getAccountIds().split(",");
+                if (accountIds.length > 0) {
+                    customerResult.setAccountId(accountIds[0].trim());
+                }
+            }
+
+            if (customerResult.getAccountId() == null) {
+                log.warn("无法获取执行账号,跳过加入营期");
+                return;
+            }
+
+            // 获取或创建营期
+            Long sopUserId = getOrCreateWxSopUser(sop, customerResult);
+            if (sopUserId == null) {
+                log.warn("创建营期失败,跳过");
+                return;
+            }
+
+            // 创建营期成员记录
+            WxSopUserInfo wxSopUserInfo = new WxSopUserInfo();
+            wxSopUserInfo.setSopId(sop.getId());
+            wxSopUserInfo.setSopUserId(sopUserId);
+            wxSopUserInfo.setCustomerId(customer.getCustomerId());
+            wxSopUserInfo.setStatus(0); // 正常状态
+            wxSopUserInfo.setTagNames(customer.getTags());
+
+            int result = wxSopUserInfoMapper.insertWxSopUserInfo(wxSopUserInfo);
+            log.info("成功将客户加入营期:客户ID={}, 营期ID={}, SOP ID={}, 插入结果={}",
+                    customer.getCustomerId(), sopUserId, sop.getId(), result);
+        } catch (Exception e) {
+            log.error("将客户加入SOP营期时发生异常", e);
+        }
+    }
+
+    /**
+     * 将客户从SOP营期中移出
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     * @param existingInfos 已存在的营期成员记录
+     */
+    @DataSource(DataSourceType.SOP)
+    public void removeCustomerFromSop(CrmCustomer customer, WxSop sop, List<WxSopUserInfo> existingInfos) {
+        try {
+            for (WxSopUserInfo info : existingInfos) {
+                int result = wxSopUserInfoMapper.deleteWxSopUserInfoById(info.getId());
+                log.info("成功将客户从营期中移出:客户ID={}, 营期成员记录ID={}, SOP ID={}, 删除结果={}",
+                        customer.getCustomerId(), info.getId(), sop.getId(), result);
+            }
+        } catch (Exception e) {
+            log.error("将客户从SOP营期中移出时发生异常", e);
+        }
+    }
+}

+ 317 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -0,0 +1,317 @@
+package com.fs.wx.sop.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
+import com.fs.common.utils.date.DateUtil;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 个微发送记录Service业务层处理
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs> implements IWxSopLogsService {
+    @Autowired
+    private WxSopLogsMapper wxSopLogsMapper;
+
+    @Autowired
+    private WxSopUserMapper wxSopUserMapper;
+
+    @Autowired
+    private WxSopUserInfoMapper wxSopUserInfoMapper;
+    @Autowired
+    private CompanyWxAccountMapper companyWxAccountMapper;
+    @Autowired
+    private WxContactMapper wxContactMapper;
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
+    /**
+     * 查询个微发送记录
+     *
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopLogs selectWxSopLogsById(Long id)
+    {
+        return baseMapper.selectWxSopLogsById(id);
+    }
+
+    /**
+     * 查询个微发送记录列表
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs)
+    {
+        return baseMapper.selectWxSopLogsList(wxSopLogs);
+    }
+
+    /**
+     * 新增个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopLogs(WxSopLogs wxSopLogs)
+    {
+        wxSopLogs.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopLogs(wxSopLogs);
+    }
+
+    /**
+     * 修改个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopLogs(WxSopLogs wxSopLogs)
+    {
+        wxSopLogs.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopLogs(wxSopLogs);
+    }
+
+    /**
+     * 批量删除个微发送记录
+     * 
+     * @param ids 需要删除的个微发送记录主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopLogsByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopLogsByIds(ids);
+    }
+
+    /**
+     * 删除个微发送记录信息
+     * 
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopLogsById(Long id)
+    {
+        return baseMapper.deleteWxSopLogsById(id);
+    }
+
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    @Override
+    public List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param){
+        List<WxSopLogsListVO> list = baseMapper.selectWxSopLogsListBySopId(param);
+        List<Long> longs = PubFun.listToNewList(list, WxSopLogsListVO::getAccountId);
+        List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectBatchIds(longs);
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
+        list.parallelStream().filter(e -> accountMap.containsKey(e.getAccountId())).forEach(e -> {
+           e.setAccountName(accountMap.get(e.getAccountId()).getWxNickName());
+        });
+        return list;
+    }
+
+    @DataSource(DataSourceType.SOP)
+    public void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert) {
+        if(logsToInsert == null || logsToInsert.isEmpty()) return;
+        wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
+    }
+
+    /**
+     * 个微SOP一键群发
+     * 注意:不加 @DataSource(DataSourceType.SOP),因为需要跨库查询
+     * SOP相关Mapper方法自带 @DataSource(DataSourceType.SOP) 注解
+     * wx_contact、crm_customer 在主库,使用默认数据源
+     */
+    @Override
+    public R sendWxSopMsg(SendWxSopMsgParam param) {
+        if (param.getSopIds() == null || param.getSopIds().length == 0) {
+            return R.error("请选择要群发的SOP");
+        }
+
+        // 1. 解析发送时间:前端选了就用前端的,没选就用当前时间
+        String sendTimeStr;
+        if (param.getSendTime() != null && !param.getSendTime().isEmpty()) {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            sendTimeStr = sdf.format(new Date()) + " " + param.getSendTime() + ":00";
+        } else {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sendTimeStr = sdf.format(new Date());
+        }
+
+        List<WxSopLogs> logsToInsert = new ArrayList<>();
+        // 收集所有联系人ID和客户ID,用于批量查询
+        List<Long> allContactIds = new ArrayList<>();
+        // 临时存储:sopUser + userInfos 的关系
+        List<Object[]> sopUserAndInfos = new ArrayList<>();
+
+        // 2. 遍历每个SOP,从SOP数据库查询营期和成员
+        for (Long sopId : param.getSopIds()) {
+            WxSopUser query = new WxSopUser();
+            query.setSopId(sopId);
+            // wxSopUserMapper.selectWxSopUserList 自带 @DataSource(DataSourceType.SOP)
+            List<WxSopUser> sopUsers = wxSopUserMapper.selectWxSopUserList(query);
+
+            if (sopUsers == null || sopUsers.isEmpty()) {
+                continue;
+            }
+
+            for (WxSopUser sopUser : sopUsers) {
+                WxSopUserInfo userInfoQuery = new WxSopUserInfo();
+                userInfoQuery.setSopUserId(sopUser.getId());
+                // wxSopUserInfoMapper.selectWxSopUserInfoList 自带 @DataSource(DataSourceType.SOP)
+                List<WxSopUserInfo> userInfos = wxSopUserInfoMapper.selectWxSopUserInfoList(userInfoQuery);
+
+                if (userInfos == null || userInfos.isEmpty()) {
+                    continue;
+                }
+
+                sopUserAndInfos.add(new Object[]{sopId, sopUser, userInfos});
+
+                for (WxSopUserInfo info : userInfos) {
+                    if (info.getWxContactId() != null) {
+                        allContactIds.add(info.getWxContactId());
+                    }
+                }
+            }
+        }
+
+        if (sopUserAndInfos.isEmpty()) {
+            return R.error("未找到可群发的营期成员");
+        }
+
+        // 3. 从主库批量查询 wx_contact 获取联系人昵称
+        Map<Long, WxContact> contactMap = new HashMap<>();
+        Map<Long, CrmCustomer> customerMap = new HashMap<>();
+        if (!allContactIds.isEmpty()) {
+            List<Long> uniqueContactIds = allContactIds.stream().distinct().collect(Collectors.toList());
+            List<WxContact> contacts = wxContactMapper.selectBatchIds(uniqueContactIds);
+            if (contacts != null) {
+                for (WxContact c : contacts) {
+                    contactMap.put(c.getId(), c);
+                }
+                // 4. 收集customerIds,从主库查询 crm_customer 获取客户标签
+                List<Long> customerIds = contacts.stream()
+                        .filter(c -> c.getCustomerId() != null)
+                        .map(WxContact::getCustomerId)
+                        .distinct()
+                        .collect(Collectors.toList());
+                if (!customerIds.isEmpty()) {
+                    List<CrmCustomer> customers = crmCustomerMapper.selectBatchIds(customerIds);
+                    if (customers != null) {
+                        for (CrmCustomer cust : customers) {
+                            customerMap.put(cust.getCustomerId(), cust);
+                        }
+                    }
+                }
+            }
+        }
+
+        // 5. 遍历构建发送记录,设置联系人昵称
+        // 同时收集需要更新标签的 userInfo
+        List<WxSopUserInfo> userInfosToUpdateTag = new ArrayList<>();
+        for (Object[] arr : sopUserAndInfos) {
+            Long sopId = (Long) arr[0];
+            WxSopUser sopUser = (WxSopUser) arr[1];
+            @SuppressWarnings("unchecked")
+            List<WxSopUserInfo> userInfos = (List<WxSopUserInfo>) arr[2];
+
+            for (WxSopUserInfo userInfo : userInfos) {
+                WxSopLogs log = new WxSopLogs();
+                log.setType(0);
+                log.setSopId(sopId);
+                log.setSopUserId(sopUser.getId());
+                log.setGenerateType(1);
+                log.setAccountId(sopUser.getAccountId());
+                log.setWxContactId(userInfo.getWxContactId());
+                log.setFsUserId(userInfo.getFsUserId());
+                log.setSendStatus(0);
+                log.setSendSort(30000000);
+                log.setContentJson(param.getSetting());
+                log.setSendTime(DateUtil.stringToLocalDateTime(sendTimeStr));
+                log.setCreateTime(new Date());
+
+                // 设置联系人昵称
+                WxContact contact = contactMap.get(userInfo.getWxContactId());
+                if (contact != null) {
+                    log.setWxContactName(contact.getNickName());
+
+                    // 更新 wx_sop_user_info 的标签(如果为空)
+                    if ((userInfo.getTagNames() == null || userInfo.getTagNames().isEmpty())
+                            && contact.getCustomerId() != null) {
+                        CrmCustomer customer = customerMap.get(contact.getCustomerId());
+                        if (customer != null && customer.getTags() != null && !customer.getTags().isEmpty()) {
+                            userInfo.setTagNames(customer.getTags());
+                            userInfosToUpdateTag.add(userInfo);
+                        }
+                    }
+                }
+
+                logsToInsert.add(log);
+            }
+        }
+
+        if (logsToInsert.isEmpty()) {
+            return R.error("未找到可群发的营期成员");
+        }
+
+        // 6. 更新 wx_sop_user_info 的标签信息(SOP数据库)
+        for (WxSopUserInfo info : userInfosToUpdateTag) {
+            WxSopUserInfo updateInfo = new WxSopUserInfo();
+            updateInfo.setId(info.getId());
+            updateInfo.setTagNames(info.getTagNames());
+            wxSopUserInfoMapper.updateWxSopUserInfo(updateInfo);
+        }
+
+        // 7. 批量插入发送记录到SOP数据库
+        batchInsertQwSopLogs(logsToInsert);
+
+        return R.ok("一键群发成功,共发送 " + logsToInsert.size() + " 条消息");
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public boolean updateMapper(WxSopLogs updateQwSop) {
+        return updateById(updateQwSop);
+    }
+}

+ 240 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java

@@ -0,0 +1,240 @@
+package com.fs.wx.sop.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.mapper.WxSopMapper;
+import com.fs.wx.sop.service.IWxSopExecuteService;
+import com.fs.wx.sop.service.IWxSopService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 个微SOPService业务层处理
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements IWxSopService {
+
+    @Autowired
+    private ICompanyWxAccountService companyWxAccountService;
+
+    @Autowired
+    private WxSopMapper wxSopMapper;
+
+    @Autowired
+    private IWxSopExecuteService wxSopExecuteService;
+
+    /**
+     * 查询个微SOP
+     *
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSop selectWxSopById(Long id)
+    {
+        return baseMapper.selectWxSopById(id);
+    }
+
+    @Override
+    public WxSop selectWxSopDetailById(Long id)
+    {
+        WxSop wxSop = baseMapper.selectWxSopById(id);
+        if (wxSop != null) {
+            fillSelectedQwUsers(wxSop);
+        }
+        return wxSop;
+    }
+
+    /**
+     * 填充执行账号 selectedQwUsers(从 accountIds 解析并查询并查询账号详情)
+     */
+    private void fillSelectedQwUsers(WxSop wxSop) {
+        String accountIdsStr = wxSop.getAccountIds();
+        if (StringUtils.isEmpty(accountIdsStr)) {
+            return;
+        }
+        List<Long> accountIds = Arrays.stream(accountIdsStr.split(","))
+            .map(String::trim)
+            .filter(s -> !s.isEmpty())
+            .map(Long::parseLong)
+            .collect(Collectors.toList());
+        if (accountIds.isEmpty()) {
+            return;
+        }
+        List<CompanyWxAccount> accounts = (List<CompanyWxAccount>) companyWxAccountService.listByIds(accountIds);
+        if (accounts != null) {
+            List<Map<String, Object>> selectedQwUsers = new ArrayList<>();
+            for (CompanyWxAccount acc : accounts) {
+                Map<String, Object> m = new HashMap<>();
+                m.put("id", acc.getId());
+                m.put("wxNickName", acc.getWxNickName());
+                m.put("wxNo", acc.getWxNo());
+                m.put("companyUserName", acc.getCompanyUserId() != null ? String.valueOf(acc.getCompanyUserId()) : null);
+                selectedQwUsers.add(m);
+            }
+            wxSop.setSelectedQwUsers(selectedQwUsers);
+        }
+    }
+
+    /**
+     * 查询个微SOP列表
+     *
+     * @param wxSop 个微SOP
+     * @return 个微SOP
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSop> selectWxSopList(WxSop wxSop)
+    {
+        return baseMapper.selectWxSopList(wxSop);
+    }
+
+    /**
+     * 新增个微SOP(含执行账号保存到 wx_sop_user)
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    /**
+     * 新增个微SOP(accountIds 直接入库到 wx_sop.account_ids)
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSop(WxSop wxSop)
+    {
+        if (wxSop.getStatus() == null) {
+            wxSop.setStatus(1L);
+        }
+        wxSop.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSop(wxSop);
+    }
+
+    /**
+     * 修改个微SOP(accountIds 直接入库到 wx_sop.account_ids)
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSop(WxSop wxSop)
+    {
+        wxSop.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSop(wxSop);
+    }
+
+    /**
+     * 批量删除个微SOP
+     *
+     * @param ids 需要删除的个微SOP主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopByIds(ids);
+    }
+
+    /**
+     * 删除个微SOP信息
+     *
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopById(Long id)
+    {
+        return baseMapper.deleteWxSopById(id);
+    }
+
+    /**
+     * 批量执行个微SOP
+     *
+     * @param ids 个微SOP主键数组
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R updateStatusWxSopByIds(Long[] ids) {
+        if (ids == null || ids.length == 0) {
+            return R.error("参数不能为空");
+        }
+
+        // 获取要操作的个微SOP列表
+        List<WxSop> wxSops = baseMapper.selectWxSopByIds(ids);
+        if (wxSops.isEmpty()) {
+            return R.ok().put("suc", new ArrayList<>()).put("err", ids);
+        }
+
+        // 筛选出 status == 1 的 IDs (可执行的)
+        List<Long> toBeSent = wxSops.stream()
+                .filter(wxSop -> wxSop.getStatus() != null && wxSop.getStatus() == 1)
+                .map(WxSop::getId)
+                .collect(Collectors.toList());
+
+        // 筛选出 status != 1 的 IDs (不可执行的)
+        List<Long> areadyList = wxSops.stream()
+                .filter(wxSop -> wxSop.getStatus() == null || wxSop.getStatus() != 1)
+                .map(WxSop::getId)
+                .collect(Collectors.toList());
+
+        // 如果有待执行的SOP,则更新其状态
+        if (!toBeSent.isEmpty()) {
+            int updateCount = baseMapper.updateStatusWxSopByIds(toBeSent.toArray(new Long[0]), 2L); // 设置为执行中状态
+            if (updateCount > 0) {
+                // 对于每个待执行的SOP,根据其筛选方式进行客户筛选和营期创建
+                for (Long sopId : toBeSent) {
+                    WxSop wxSop = baseMapper.selectWxSopById(sopId);
+                    if (wxSop != null) {
+                        // 根据筛选方式进行客户筛选和营期创建
+                        processSopCustomerSelection(wxSop);
+                    }
+                }
+
+                return R.ok().put("suc", toBeSent.toArray(new Long[0])).put("err", areadyList.toArray(new Long[0]));
+            } else {
+                // 即使更新失败,也要返回哪些成功哪些失败
+                return R.ok().put("suc", new ArrayList<>()).put("err", ids);
+            }
+        } else {
+            return R.ok().put("suc", new ArrayList<>()).put("err", areadyList.toArray(new Long[0]));
+        }
+    }
+
+
+
+    /**
+     * 处理SOP的客户筛选和营期创建
+     *
+     * @param wxSop 个微SOP
+     */
+    private void processSopCustomerSelection(WxSop wxSop) {
+        // 根据SOP的筛选方式筛选客户
+        if (wxSop.getFilterType() != null && wxSop.getFilterType() == 0) { // 0: 标签筛选
+            // 执行标签筛选逻辑
+            wxSopExecuteService.processTagFilterWxSop(wxSop);
+        } else if (wxSop.getFilterType() != null && wxSop.getFilterType() == 1) { // 1: 群聊筛选
+            // 执行群聊筛选逻辑
+            wxSopExecuteService.processGroupFilterWxSop(wxSop);
+        }
+    }
+}

+ 102 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java

@@ -0,0 +1,102 @@
+package com.fs.wx.sop.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 个微营期详情Service业务层处理
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, WxSopUserInfo> implements IWxSopUserInfoService {
+
+    /**
+     * 查询个微营期详情
+     *
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopUserInfo selectWxSopUserInfoById(Long id)
+    {
+        return baseMapper.selectWxSopUserInfoById(id);
+    }
+
+    /**
+     * 查询个微营期详情列表
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo)
+    {
+        return baseMapper.selectWxSopUserInfoList(wxSopUserInfo);
+    }
+
+    /**
+     * 新增个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
+    {
+        wxSopUserInfo.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopUserInfo(wxSopUserInfo);
+    }
+
+    /**
+     * 修改个微营期详情
+     *
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
+    {
+        wxSopUserInfo.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopUserInfo(wxSopUserInfo);
+    }
+
+    /**
+     * 批量删除个微营期详情
+     *
+     * @param ids 需要删除的个微营期详情主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserInfoByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopUserInfoByIds(ids);
+    }
+
+    /**
+     * 删除个微营期详情信息
+     *
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserInfoById(Long id)
+    {
+        return baseMapper.deleteWxSopUserInfoById(id);
+    }
+}

+ 115 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java

@@ -0,0 +1,115 @@
+package com.fs.wx.sop.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.service.IWxSopUserService;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 个微营期Service业务层处理
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+@AllArgsConstructor
+public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser> implements IWxSopUserService {
+
+    private final CompanyWxAccountMapper companyWxAccountMapper;
+    /**
+     * 查询个微营期
+     *
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopUser selectWxSopUserById(Long id)
+    {
+        return baseMapper.selectWxSopUserById(id);
+    }
+
+    /**
+     * 查询个微营期列表
+     *
+     * @param wxSopUser 个微营期
+     * @return 个微营期
+     */
+    @Override
+    public List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser){
+        List<WxSopUser> wxSopUsers = baseMapper.selectWxSopUserList(wxSopUser);
+        List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectList(new QueryWrapper<CompanyWxAccount>().in("id", PubFun.listToNewList(wxSopUsers, WxSopUser::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
+        wxSopUsers.parallelStream().filter(e -> accountMap.containsKey(e.getAccountId())).forEach(e -> {
+            CompanyWxAccount companyWxAccount = accountMap.get(e.getAccountId());
+            e.setAccountName(companyWxAccount.getWxNickName());
+        });
+        return wxSopUsers;
+    }
+
+    /**
+     * 新增个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopUser(WxSopUser wxSopUser)
+    {
+        wxSopUser.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopUser(wxSopUser);
+    }
+
+    /**
+     * 修改个微营期
+     *
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopUser(WxSopUser wxSopUser)
+    {
+        wxSopUser.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopUser(wxSopUser);
+    }
+
+    /**
+     * 批量删除个微营期
+     *
+     * @param ids 需要删除的个微营期主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopUserByIds(ids);
+    }
+
+    /**
+     * 删除个微营期信息
+     *
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserById(Long id)
+    {
+        return baseMapper.deleteWxSopUserById(id);
+    }
+}

+ 110 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java

@@ -0,0 +1,110 @@
+package com.fs.wx.sop.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 个微SOP执行记录列表VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class WxSopLogsListVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** SOP ID */
+    private Long sopId;
+
+    /** 营期ID */
+    private Long sopUserId;
+
+    /** 个微账号ID */
+    @Excel(name = "个微账号ID")
+    private Long accountId;
+
+    /** 个微账号昵称 */
+    @Excel(name = "个微账号昵称")
+    private String accountName;
+
+    /** 客户ID */
+    private Long wxContactId;
+
+    /** 客户昵称 */
+    @Excel(name = "客户昵称")
+    private String wxContactName;
+
+    /** 客户标签 */
+    @Excel(name = "客户标签")
+    private String tagNames;
+
+    /** 群聊ID */
+    private Long wxRoomId;
+
+    /** 群聊名称 */
+    @Excel(name = "群聊名称")
+    private String wxRoomName;
+
+    /** 消息类型 0个人1群 */
+    @Excel(name = "消息类型")
+    private Integer type;
+
+    /** 发送类型 */
+    @Excel(name = "发送类型")
+    private Integer sendType;
+
+    /** 生成类型 0自动1手动 */
+    @Excel(name = "生成类型")
+    private Integer generateType;
+
+    /** 发送状态 0待发送1发送成功2发送失败3消息作废 */
+    @Excel(name = "发送状态")
+    private Integer sendStatus;
+
+    /** 发送备注 */
+    @Excel(name = "发送备注")
+    private String sendRemark;
+
+    /** 发送排序 */
+    private Integer sendSort;
+
+    /** 消息过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "消息过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date expirationTime;
+
+    /** 生成时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "生成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 实际发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "实际发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date realSendTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 小程序ID */
+    private Long fsUserId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 预计发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime sendTime;
+
+    /** 消息内容JSON */
+    private String contentJson;
+}

+ 11 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopMsgVo.java

@@ -0,0 +1,11 @@
+package com.fs.wx.sop.vo;
+
+import lombok.Data;
+
+@Data
+public class WxSopMsgVo {
+    private Integer contentType;
+    private String value;
+    private Integer sendStatus;
+    private String sendRemarks;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopUserMsgGenVO.java

@@ -0,0 +1,27 @@
+package com.fs.wx.sop.vo;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 个微SOP消息生成——营期客户视图对象
+ * 用于查询需要生成消息的活跃营期及客户信息
+ */
+@Data
+public class WxSopUserMsgGenVO {
+
+    private Long sopUserId;
+    private Integer type;
+    private Long sopId;
+    private Long accountId;
+    private LocalDate startTime;
+
+    private String tempId;
+    private Long companyId;
+
+    private Long infoId;
+    private Long wxContactId;
+    private Long customerId;
+    private Long fsUserId;
+}

+ 0 - 1
fs-service/src/main/java/com/fs/wx/utils/WxPayUtils.java

@@ -9,7 +9,6 @@ import com.wechat.pay.java.core.http.*;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.OkHttpClient;
 import okhttp3.Response;
-import org.apache.commons.lang3.StringUtils;
 
 import javax.crypto.Cipher;
 import java.io.IOException;

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

@@ -7,7 +7,7 @@ logging:
     org.springframework.web: debug
     com.github.binarywang.demo.wx.cp: DEBUG
     me.chanjar.weixin: DEBUG
-#wx:
+wx:
 #  miniapp:
 #    configs:
 #      - appid: wx29d26f63f836be7f
@@ -15,13 +15,13 @@ logging:
 #        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
 #        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
 #        msgDataFormat: JSON
-#  cp:
-#    corpId: wwb2a1055fb6c9a7c2
-#    appConfigs:
-#      - agentId: 1000005
-#        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
-#        token: PPKOdAlCoMO
-#        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+  cp:
+    corpId: wwb2a1055fb6c9a7c2
+    appConfigs:
+      - agentId: 1000005
+        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
+        token: PPKOdAlCoMO
+        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
 #  pay:
 #    appId: wx73f85f8d62769119 #微信公众号或者小程序等的appid
 #    mchId: 1611402045 #微信支付商户号

+ 33 - 0
fs-service/src/main/resources/mapper/sop/QwSopTempRulesMapper.xml

@@ -151,6 +151,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select id from qw_sop_temp_rules where temp_id = #{tempId}  and  is_official = 1
     </select>
 
+    <resultMap type="com.fs.sop.vo.QwSopTempRulesWithDayVO" id="QwSopTempRulesWithDayResult">
+        <result property="id"    column="id"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="dayId"    column="day_id"    />
+        <result property="name"    column="name"    />
+        <result property="time"    column="time"    />
+        <result property="isOfficial"    column="is_official"    />
+        <result property="contentType"    column="content_type"    />
+        <result property="type"    column="type"    />
+        <result property="courseType"    column="course_type"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="videoId"    column="video_id"    />
+        <result property="aiTouch"    column="ai_touch"    />
+        <result property="addTag"    column="add_tag"    />
+        <result property="delTag"    column="del_tag"    />
+        <result property="sorts"    column="sorts"    />
+        <result property="isAtAll"    column="is_at_all"    />
+        <result property="liveId"    column="live_id"    />
+        <result property="dayNum"    column="day_num"    />
+        <result property="textContent" column="ct_content" />
+    </resultMap>
+
+    <select id="listByTempIdWithDayNum" resultMap="QwSopTempRulesWithDayResult">
+        select tr.*, td.day_num,
+               (select tc.content from qw_sop_temp_content tc
+                where tc.rules_id = tr.id and tc.content_type = 1
+                   limit 1) as ct_content
+        from qw_sop_temp_rules tr
+            left join qw_sop_temp_day td on tr.day_id = td.id
+        where tr.temp_id = #{id}
+        order by td.day_num, tr.time
+    </select>
+
     <update id="updateTempRulesOfficialBatch" >
         update qw_sop_temp_rules
         set is_official = #{official}

+ 124 - 3
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -20,7 +20,6 @@ import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.company.service.impl.call.node.AiQwAddWxTaskNode;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
 import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
-import com.fs.course.config.RedisKeyScanner;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -36,8 +35,14 @@ import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qwApi.domain.QwLinkCreateResult;
 import com.fs.qwApi.param.QwLinkCreateParam;
 import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.mapper.QwSopTempRulesMapper;
+import com.fs.sop.vo.QwSopTempRulesWithDayVO;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.vo.WxSopUserMsgGenVO;
 import com.fs.wxcid.dto.friend.AddContactParam;
 import com.fs.wxcid.service.FriendService;
 import com.fs.wxcid.vo.AddContactVo;
@@ -48,7 +53,6 @@ import com.fs.wxwork.dto.WxWorkResponseDTO;
 import com.fs.wxwork.service.WxWorkService;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
@@ -57,10 +61,11 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.*;
@@ -105,6 +110,9 @@ public class WxTaskService {
     private final CompanyVoiceRoboticCallLogSendmsgServiceImpl companyVoiceRoboticCallLogSendmsgService;
     private final QwApiService qwApiService;
     private final RedisCache redisCache2;
+    private final WxSopUserMapper wxSopUserMapper;
+    private final QwSopTempRulesMapper qwSopTempRulesMapper;
+    private final WxSopLogsMapper wxSopLogsMapper;
 //    private final ExecutorService cidExcutor = new ThreadPoolExecutor(
 //            32,
 //            64,
@@ -1867,4 +1875,117 @@ public class WxTaskService {
         }
     }
 
+    /**
+     * 个微SOP消息生成(文本类型)
+     * 每小时触发,查询活跃的WxSOP营期及客户,生成文本消息待发送记录
+     *
+     * @param currentTime 当前整点时间
+     */
+    public void generateWxSopMsgByTime(LocalDateTime currentTime) {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 个微SOP文本消息生成开始, currentTime: {} ======", currentTime);
+
+        List<WxSopUserMsgGenVO> msgGenList = wxSopUserMapper.selectActiveWxSopUserForMsgGen();
+        if (msgGenList == null || msgGenList.isEmpty()) {
+            log.info("个微SOP消息生成: 没有需要处理的活跃营期。");
+            return;
+        }
+        log.info("个微SOP消息生成: 查询到 {} 条营期客户记录。", msgGenList.size());
+
+        Map<String, List<QwSopTempRulesWithDayVO>> rulesCache = new HashMap<>();
+        List<WxSopLogs> logsToInsert = new ArrayList<>();
+
+        for (WxSopUserMsgGenVO vo : msgGenList) {
+            if (vo.getTempId() == null || vo.getTempId().isEmpty()) {
+                continue;
+            }
+
+            LocalDate startDate = vo.getStartTime();
+            LocalDate currentDate = currentTime.toLocalDate();
+            long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate);
+            long targetDay = daysBetween + 1;
+
+            List<QwSopTempRulesWithDayVO> rulesList = rulesCache.computeIfAbsent(vo.getTempId(),
+                    qwSopTempRulesMapper::listByTempIdWithDayNum);
+            if (rulesList == null || rulesList.isEmpty()) {
+                continue;
+            }
+
+            List<QwSopTempRulesWithDayVO> dayRules = rulesList.stream()
+                    .filter(r -> r.getDayNum() != null && r.getDayNum() == targetDay)
+                    .filter(r -> r.getContentType() != null && r.getContentType() == 1)
+                    .collect(Collectors.toList());
+
+            if (dayRules.isEmpty()) {
+                continue;
+            }
+
+            for (QwSopTempRulesWithDayVO rule : dayRules) {
+                LocalTime ruleTime;
+                try {
+                    ruleTime = LocalTime.parse(rule.getTime());
+                } catch (Exception e) {
+                    log.warn("个微SOP消息生成: 解析时间失败, ruleId: {}, time: {}", rule.getId(), rule.getTime());
+                    continue;
+                }
+                LocalDateTime ruleDateTime = LocalDateTime.of(currentDate, ruleTime);
+                if (ruleDateTime.isBefore(currentTime) && ruleDateTime.plusHours(1).isBefore(currentTime)) {
+                    continue;
+                }
+
+                LocalDateTime startRange = currentTime.plusMinutes(60);
+                LocalDateTime endRange = startRange.plusMinutes(60);
+                if (ruleDateTime.isBefore(startRange) || !ruleDateTime.isBefore(endRange)) {
+                    continue;
+                }
+
+                if (rule.getTextContent() == null || rule.getTextContent().isEmpty()) {
+                    continue;
+                }
+
+                WxSopLogs sopLogs = new WxSopLogs();
+                sopLogs.setType(vo.getType());
+                sopLogs.setSopId(vo.getSopId());
+                sopLogs.setSopUserId(vo.getSopUserId());
+                sopLogs.setSendType(2);
+                sopLogs.setGenerateType(0);
+                sopLogs.setAccountId(vo.getAccountId());
+                sopLogs.setWxContactId(vo.getWxContactId());
+                sopLogs.setFsUserId(vo.getFsUserId());
+                sopLogs.setSendStatus(0);
+                sopLogs.setSendSort(10000000);
+                sopLogs.setSendTime(ruleDateTime);
+
+                String contentJson = buildTextContentJson(rule.getTextContent());
+                sopLogs.setContentJson(contentJson);
+                sopLogs.setCreateTime(new Date());
+                sopLogs.setUpdateTime(new Date());
+
+                logsToInsert.add(sopLogs);
+            }
+        }
+
+        if (!logsToInsert.isEmpty()) {
+            wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
+            log.info("个微SOP消息生成: 批量写入 {} 条文本消息。", logsToInsert.size());
+        }
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== 个微SOP文本消息生成完成, 耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+    }
+
+    private String buildTextContentJson(String text) {
+        if (text == null || text.isEmpty()) {
+            return null;
+        }
+        com.alibaba.fastjson.JSONObject item = new com.alibaba.fastjson.JSONObject();
+        item.put("contentType", "1");
+        item.put("value", text);
+        com.alibaba.fastjson.JSONArray settingsArray = new com.alibaba.fastjson.JSONArray();
+        settingsArray.add(item);
+        com.alibaba.fastjson.JSONObject wrapper = new com.alibaba.fastjson.JSONObject();
+        wrapper.put("settings", settingsArray);
+        return com.alibaba.fastjson.JSON.toJSONString(wrapper);
+    }
+
 }

+ 20 - 0
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -4,9 +4,12 @@ import com.fs.app.service.WxTaskService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.time.LocalDateTime;
+
 /**
  * 企业微信SOP定时任务管理类
  * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
@@ -123,4 +126,21 @@ public class WxTask {
         }
 
     }
+
+    /**
+     * 个微SOP消息生成任务(文本类型)
+     * 每小时的第5分钟执行,仿照企微selectSopUserLogsListByTime模式
+     * 查询活跃的wx_sop_user及客户,生成仅文本类型(contentType=1)的待发送消息
+     */
+    @Async
+    @Scheduled(cron = "0 5 * * * ?")
+    public void generateWxSopMsgByTime() {
+        LocalDateTime currentTime = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
+        log.info("个微SOP消息生成任务执行时间: {}", currentTime);
+        try {
+//            taskService.generateWxSopMsgByTime(currentTime);
+        } catch (Exception e) {
+            log.error("个微SOP消息生成任务失败: {}", e.getMessage(), e);
+        }
+    }
 }